@jant/core 0.6.15 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -6,7 +6,8 @@
6
6
  * mouse or the keyboard has no obvious way to scroll it. This module:
7
7
  *
8
8
  * - Toggles `.can-scroll-start` / `.can-scroll-end` on the wrap so CSS can
9
- * fade the edge that has hidden content and reveal the matching arrow.
9
+ * fade the edge that has hidden content and reveal the matching arrow,
10
+ * re-measuring on scroll and on any size change of the strip or its items.
10
11
  * - Wires the prev/next arrow buttons to scroll the strip by one page.
11
12
  * - Scrolls the strip with Arrow / Home / End keys while it is focused.
12
13
  */
@@ -41,6 +42,39 @@ function scrollByStep(scroller: HTMLElement, direction: 1 | -1): void {
41
42
  });
42
43
  }
43
44
 
45
+ /**
46
+ * Re-measure whenever the strip or its items change size.
47
+ *
48
+ * Scroll events alone are not enough: they only fire while the strip actually
49
+ * overflows, so a hint measured against a narrower strip — a smaller window at
50
+ * load time, a zoom change, an item that resolves its width late — latches on
51
+ * forever. The arrow keeps pointing at content that is already fully visible,
52
+ * and no scroll can ever happen to correct it.
53
+ */
54
+ function watchLayout(wrap: HTMLElement, scroller: HTMLElement): void {
55
+ if (!("ResizeObserver" in globalThis)) return;
56
+
57
+ let raf = 0;
58
+ const schedule = (): void => {
59
+ cancelAnimationFrame(raf);
60
+ raf = requestAnimationFrame(() => updateHints(wrap));
61
+ };
62
+
63
+ // The strip covers viewport and column width changes; its items cover
64
+ // content width changes. Re-observe when a morph swaps the items out.
65
+ const observer = new globalThis.ResizeObserver(schedule);
66
+ const observeAll = (): void => {
67
+ observer.disconnect();
68
+ observer.observe(scroller);
69
+ for (const item of scroller.children) observer.observe(item);
70
+ };
71
+ observeAll();
72
+ new globalThis.MutationObserver(() => {
73
+ observeAll();
74
+ schedule();
75
+ }).observe(scroller, { childList: true });
76
+ }
77
+
44
78
  function initWrap(wrap: HTMLElement): void {
45
79
  // Guard against double-init (initAll + the MutationObserver can overlap).
46
80
  if (wrap.dataset.scrollHintReady === "1") return;
@@ -48,11 +82,13 @@ function initWrap(wrap: HTMLElement): void {
48
82
  if (!scroller) return;
49
83
  wrap.dataset.scrollHintReady = "1";
50
84
 
51
- // Initial check + keep edge hints in sync while scrolling.
85
+ // Initial check + keep edge hints in sync while scrolling and while the
86
+ // strip is resized or its items change.
52
87
  updateHints(wrap);
53
88
  scroller.addEventListener("scroll", () => updateHints(wrap), {
54
89
  passive: true,
55
90
  });
91
+ watchLayout(wrap, scroller);
56
92
 
57
93
  // Prev/next arrow buttons — the mouse affordance.
58
94
  wrap
@@ -117,3 +153,8 @@ if (document.readyState === "loading") {
117
153
  initAll();
118
154
  observer.observe(document.body, { childList: true, subtree: true });
119
155
  }
156
+
157
+ export const __testOnly = {
158
+ initWrap,
159
+ updateHints,
160
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Lazy access to mediabunny.
3
+ *
4
+ * mediabunny is ≈900 KB minified (≈170 KB gzipped) and only does anything once
5
+ * a video or audio file is picked for upload, so the processors fetch it on
6
+ * demand instead of shipping it in `client-auth.js`. Same pattern as `heic-to`
7
+ * in `compose-bridge.ts`; the build guard in `vite.shared.ts` keeps it out of
8
+ * the entry chunks.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const { Input, BlobSource, ALL_FORMATS } = await loadMediabunny();
13
+ * const input = new Input({ source: new BlobSource(file), formats: ALL_FORMATS });
14
+ * ```
15
+ */
16
+
17
+ type Mediabunny = typeof import("mediabunny");
18
+
19
+ let loading: Promise<Mediabunny> | undefined;
20
+
21
+ /**
22
+ * Load mediabunny once and share the module across callers.
23
+ *
24
+ * @returns The mediabunny module namespace
25
+ */
26
+ export function loadMediabunny(): Promise<Mediabunny> {
27
+ loading ??= import("mediabunny");
28
+ return loading;
29
+ }
@@ -0,0 +1,41 @@
1
+ import "./components/jant-post-picker.js";
2
+ import type {
3
+ JantPostPicker,
4
+ PostPickerItem,
5
+ PostPickerOptions,
6
+ } from "./components/jant-post-picker.js";
7
+
8
+ const POST_PICKER_TAG = "jant-post-picker";
9
+
10
+ /**
11
+ * Ensure the shared post picker is mounted once.
12
+ *
13
+ * @returns The mounted picker element
14
+ */
15
+ export function ensurePostPicker(): JantPostPicker {
16
+ const existing = document.querySelector<JantPostPicker>(POST_PICKER_TAG);
17
+ if (existing) return existing;
18
+
19
+ const picker = document.createElement(POST_PICKER_TAG) as JantPostPicker;
20
+ document.body.appendChild(picker);
21
+ return picker;
22
+ }
23
+
24
+ /**
25
+ * Open the shared picker and resolve to the post the author chose.
26
+ *
27
+ * @param options - Copy, and the search that produces the candidates
28
+ * @returns The picked post's ID, or null when dismissed
29
+ * @example
30
+ * const id = await pickPost({
31
+ * heading: "Link a translation",
32
+ * placeholder: "Search your posts…",
33
+ * emptyHint: "Nothing matched.",
34
+ * search: (q) => fetchCandidates(q),
35
+ * });
36
+ */
37
+ export function pickPost(options: PostPickerOptions): Promise<string | null> {
38
+ return ensurePostPicker().pick(options);
39
+ }
40
+
41
+ export type { PostPickerItem, PostPickerOptions };
@@ -0,0 +1,135 @@
1
+ /**
2
+ * In-place refresh of a rendered Post.
3
+ *
4
+ * Reply submission, draft publishing, and anything else that changes a Post
5
+ * server-side needs the same thing afterwards: re-render exactly the surface
6
+ * the Post is shown on — a thread page, a feed item, or a standalone card —
7
+ * without a full page load.
8
+ */
9
+
10
+ import { hydratePartial } from "./hydrate-partial.js";
11
+ import { getReplyRefreshTarget } from "./compose-launch.js";
12
+
13
+ async function fetchPartialHtml(path: string): Promise<string | null> {
14
+ const res = await fetch(path, {
15
+ headers: { Accept: "text/html" },
16
+ });
17
+ if (!res.ok) return null;
18
+ return res.text();
19
+ }
20
+
21
+ export async function refreshTimelineThreadView(
22
+ threadRootId: string,
23
+ ): Promise<boolean> {
24
+ try {
25
+ const timelineItem = document.querySelector<HTMLElement>(
26
+ `[data-timeline-item][data-thread-root-id="${threadRootId}"]`,
27
+ );
28
+ const content = timelineItem?.querySelector<HTMLElement>(
29
+ "[data-timeline-item-content]",
30
+ );
31
+ if (!content) return false;
32
+
33
+ const html = await fetchPartialHtml(
34
+ `/_/timeline-item/${encodeURIComponent(threadRootId)}`,
35
+ );
36
+ if (!html) return false;
37
+
38
+ content.innerHTML = html;
39
+ // Swapped-in markup carries interactions whose per-element setup only runs
40
+ // on DOMContentLoaded (thread "Show more" toggle, feed video autoplay, audio
41
+ // waveform); re-initialize them or they stay inert until a full reload.
42
+ hydratePartial(content);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ export async function refreshPostCardView(postId: string): Promise<boolean> {
50
+ try {
51
+ const timelineItem = document
52
+ .querySelector<HTMLElement>(`article[data-post-id="${postId}"]`)
53
+ ?.closest<HTMLElement>("[data-timeline-item]");
54
+ const html = await fetchPartialHtml(
55
+ `/_/post-card/${encodeURIComponent(postId)}`,
56
+ );
57
+ if (!html) return false;
58
+
59
+ if (timelineItem) {
60
+ const content = timelineItem.querySelector<HTMLElement>(
61
+ "[data-timeline-item-content]",
62
+ );
63
+ if (!content) return false;
64
+ content.innerHTML = html;
65
+ hydratePartial(content);
66
+ return true;
67
+ }
68
+
69
+ const article = document.querySelector<HTMLElement>(
70
+ `article[data-post-id="${postId}"]`,
71
+ );
72
+ if (!article) return false;
73
+
74
+ article.outerHTML = html;
75
+ // outerHTML detaches `article`; re-query the replacement to hydrate it.
76
+ const nextArticle = document.querySelector<HTMLElement>(
77
+ `article[data-post-id="${postId}"]`,
78
+ );
79
+ if (nextArticle) hydratePartial(nextArticle);
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ export async function refreshPostPageView(postId: string): Promise<boolean> {
87
+ try {
88
+ const container = document.querySelector<HTMLElement>(
89
+ `[data-post-view][data-post-view-id="${postId}"]`,
90
+ );
91
+ if (!container) return false;
92
+
93
+ const html = await fetchPartialHtml(
94
+ `/_/post-view/${encodeURIComponent(postId)}`,
95
+ );
96
+ if (!html) return false;
97
+
98
+ container.outerHTML = html;
99
+ // outerHTML detaches `container`; re-query the replacement to hydrate it
100
+ // (see refreshTimelineThreadView).
101
+ const next = document.querySelector<HTMLElement>(
102
+ `[data-post-view][data-post-view-id="${postId}"]`,
103
+ );
104
+ if (next) hydratePartial(next);
105
+ return true;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Re-renders whichever surface an article belongs to, reusing the same
113
+ * targeting the reply flow uses.
114
+ *
115
+ * @param article - The rendered `article[data-post]` element to refresh
116
+ * @returns Whether a surface was found and re-rendered
117
+ * @example
118
+ * ```ts
119
+ * if (!(await refreshArticleView(article))) globalThis.location.reload();
120
+ * ```
121
+ */
122
+ export async function refreshArticleView(
123
+ article: HTMLElement,
124
+ ): Promise<boolean> {
125
+ const target = getReplyRefreshTarget(article);
126
+ if (!target) return false;
127
+
128
+ if (target.kind === "timeline-item") {
129
+ return refreshTimelineThreadView(target.id);
130
+ }
131
+ if (target.kind === "post-view") {
132
+ return refreshPostPageView(target.id);
133
+ }
134
+ return refreshPostCardView(target.id);
135
+ }
@@ -4,10 +4,16 @@ import {
4
4
  toPublicAssetPath,
5
5
  } from "../lib/asset-path.js";
6
6
  import { toPublicHref } from "../lib/url.js";
7
+ import { isPerLanguageSurface } from "../lib/per-language-surfaces.js";
7
8
 
8
9
  type FetchInput = Parameters<typeof fetch>[0];
9
10
 
10
- function sitePathPrefix(): string {
11
+ /**
12
+ * The deployment's public path prefix, as the server stamped it on the page.
13
+ *
14
+ * @returns Prefix like `/blog`, or an empty string when the site is rooted
15
+ */
16
+ export function sitePathPrefix(): string {
11
17
  return document.documentElement.dataset.sitePathPrefix || "";
12
18
  }
13
19
 
@@ -15,6 +21,27 @@ function assetBasePath(): string {
15
21
  return document.documentElement.dataset.assetBasePath || ASSET_BASE_PATH;
16
22
  }
17
23
 
24
+ function viewBasePath(): string {
25
+ const base = document.documentElement.dataset.viewBasePath;
26
+ return base === undefined ? sitePathPrefix() : base;
27
+ }
28
+
29
+ /**
30
+ * Content language of the view the reader is in, as the server stamped it.
31
+ *
32
+ * The primary language is served at the root and stamps nothing, so `undefined`
33
+ * means "this site's default view", not "unknown". Send it to an API that
34
+ * narrows by language so the numbers it returns match the ones the page was
35
+ * rendered with.
36
+ *
37
+ * @returns BCP 47 tag like `en`, or undefined in the root view
38
+ * @example
39
+ * viewLang(); // "ja" on /ja/collections, undefined on /collections
40
+ */
41
+ export function viewLang(): string | undefined {
42
+ return document.documentElement.dataset.viewLang || undefined;
43
+ }
44
+
18
45
  export function publicPath(path: string): string {
19
46
  if (isAssetPath(path)) {
20
47
  return toPublicAssetPath(path, assetBasePath());
@@ -22,6 +49,52 @@ export function publicPath(path: string): string {
22
49
  return toPublicHref(path, sitePathPrefix());
23
50
  }
24
51
 
52
+ /**
53
+ * Build a link to a target the caller knows exists once per language.
54
+ *
55
+ * The client-side counterpart of the server's `toViewPath`: it always carries
56
+ * the current view's language prefix. Reach for it when the call site knows
57
+ * what it is linking to — a collection row knows its target is a collection
58
+ * page, which is served per language even though it sits at `/{slug}` in the
59
+ * same root namespace as language-neutral post permalinks.
60
+ *
61
+ * When the target's kind is not known at the call site, use {@link navPath}.
62
+ *
63
+ * @param path - Internal app path such as `/collections` or a collection slug
64
+ * @returns Public path prefixed by the current view's language and site prefix
65
+ * @example
66
+ * viewPath("/collections"); // "/collections", or "/en/collections" under /en
67
+ */
68
+ export function viewPath(path: string): string {
69
+ if (isAssetPath(path)) {
70
+ return toPublicAssetPath(path, assetBasePath());
71
+ }
72
+ return toPublicHref(path, viewBasePath());
73
+ }
74
+
75
+ /**
76
+ * Build a link to a target whose kind the call site does not know.
77
+ *
78
+ * The command palette navigates to settings pages, posts, and archives through
79
+ * one code path, so it cannot assert what any given target is. This keeps the
80
+ * reader in their language view for the surfaces that demonstrably exist per
81
+ * language, and leaves everything else at its single site-wide address — so a
82
+ * jump to `/settings` never becomes `/en/settings`, which is a 404.
83
+ *
84
+ * Paths in the root namespace (`/{slug}`) are left alone, because a post and a
85
+ * collection are indistinguishable there and posts have one address by design.
86
+ * A call site that knows better should say so with {@link viewPath}.
87
+ *
88
+ * @param path - Internal app path, query string included if there is one
89
+ * @returns Public path, carrying the view's language prefix where one applies
90
+ * @example
91
+ * navPath("/archive?media=any"); // "/en/archive?media=any" under /en
92
+ * navPath("/settings"); // "/settings" — the dashboard is one place
93
+ */
94
+ export function navPath(path: string): string {
95
+ return isPerLanguageSurface(path) ? viewPath(path) : publicPath(path);
96
+ }
97
+
25
98
  function normalizeFetchInput(input: FetchInput | URL): FetchInput | URL {
26
99
  if (typeof input === "string") {
27
100
  if (/^https?:\/\//.test(input)) {
@@ -19,26 +19,22 @@ import { showToast } from "./toast.js";
19
19
  function parseSettingsInitialData(data: unknown): SettingsInitialData | null {
20
20
  const siteName = getJsonString(data, "siteName");
21
21
  const siteDescription = getJsonString(data, "siteDescription");
22
- const siteLanguage = getJsonString(data, "siteLanguage");
23
- // Tolerate older payloads without the key: empty = follow content language.
24
- const dashboardLanguage = getJsonString(data, "dashboardLanguage") ?? "";
25
- const cjkSerifFont = getJsonString(data, "cjkSerifFont");
26
22
  const mainRssFeed = getJsonString(data, "mainRssFeed");
27
23
  const timeZone = getJsonString(data, "timeZone");
28
24
  const siteFooter = getJsonString(data, "siteFooter");
29
25
  const showJantBrandingOnHome = getJsonBoolean(data, "showJantBrandingOnHome");
30
26
  const noindex = getJsonBoolean(data, "noindex");
27
+ const discover = getJsonString(data, "discover");
31
28
 
32
29
  if (
33
30
  siteName === undefined ||
34
31
  siteDescription === undefined ||
35
- siteLanguage === undefined ||
36
- cjkSerifFont === undefined ||
37
32
  mainRssFeed === undefined ||
38
33
  timeZone === undefined ||
39
34
  siteFooter === undefined ||
40
35
  showJantBrandingOnHome === undefined ||
41
- noindex === undefined
36
+ noindex === undefined ||
37
+ discover === undefined
42
38
  ) {
43
39
  return null;
44
40
  }
@@ -46,14 +42,12 @@ function parseSettingsInitialData(data: unknown): SettingsInitialData | null {
46
42
  return {
47
43
  siteName,
48
44
  siteDescription,
49
- siteLanguage,
50
- dashboardLanguage,
51
- cjkSerifFont,
52
45
  mainRssFeed,
53
46
  timeZone,
54
47
  siteFooter,
55
48
  showJantBrandingOnHome,
56
49
  noindex,
50
+ discover,
57
51
  };
58
52
  }
59
53
 
@@ -8,9 +8,18 @@
8
8
  const moreControllers = new WeakMap();
9
9
  const drawerControllers = new WeakMap();
10
10
 
11
- function initMoreDropdown(root) {
12
- const trigger = root.querySelector(".site-header-more-btn");
13
- const popover = root.querySelector(".site-header-more-popover");
11
+ /** Header dropdowns, in the order they appear: the "More" nav overflow and,
12
+ * on a multilingual site, the language switcher. Both behave identically —
13
+ * click to toggle, outside click or Escape to close, opening one closes the
14
+ * other via the shared `basecoat:popover` event. */
15
+ const DROPDOWNS = [
16
+ { trigger: ".site-header-more-btn", popover: ".site-header-more-popover" },
17
+ { trigger: ".site-header-lang-btn", popover: ".site-header-lang-popover" },
18
+ ];
19
+
20
+ function initDropdown(root, triggerSelector, popoverSelector) {
21
+ const trigger = root.querySelector(triggerSelector);
22
+ const popover = root.querySelector(popoverSelector);
14
23
 
15
24
  if (!trigger || !popover) return;
16
25
  if (trigger.dataset.moreInitialized === "true") return;
@@ -129,8 +138,10 @@ export function initSiteHeaderNav(root = document) {
129
138
  // --- Freshness indicators ---
130
139
  initNavFreshness(root);
131
140
 
132
- // --- More dropdown ---
133
- initMoreDropdown(root);
141
+ // --- Header dropdowns ---
142
+ for (const { trigger, popover } of DROPDOWNS) {
143
+ initDropdown(root, trigger, popover);
144
+ }
134
145
 
135
146
  // --- Mobile drawer ---
136
147
  if (!hamburger || !drawer || !backdrop) return;
@@ -210,7 +221,8 @@ export function initSiteHeaderNav(root = document) {
210
221
  }
211
222
 
212
223
  export function destroySiteHeaderNav(root = document) {
213
- for (const trigger of root.querySelectorAll(".site-header-more-btn")) {
224
+ const triggerSelector = DROPDOWNS.map((d) => d.trigger).join(", ");
225
+ for (const trigger of root.querySelectorAll(triggerSelector)) {
214
226
  const controller = moreControllers.get(trigger);
215
227
  controller?.abort();
216
228
  moreControllers.delete(trigger);
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Smart collection dialog host.
3
+ *
4
+ * One dialog element for the whole page, mounted lazily and reused. Three
5
+ * places open it — the collections page menu, a directory row's menu, and the
6
+ * smart collection page's own menu — and they all go through
7
+ * {@link openSmartCollectionDialog}, because creating and editing are one
8
+ * surface.
9
+ */
10
+
11
+ import "./components/jant-smart-collection-dialog.js";
12
+ import type { JantSmartCollectionDialog } from "./components/jant-smart-collection-dialog.js";
13
+ import type {
14
+ SmartCollectionDialogLabels,
15
+ SmartCollectionDialogState,
16
+ } from "./components/smart-collection-dialog-types.js";
17
+ import { setCollectionVocabulary } from "./components/smart-collection-conditions.js";
18
+ import { publicPath } from "./runtime-paths.js";
19
+
20
+ const LABELS_ATTRIBUTE = "data-smart-collection-dialog-labels";
21
+
22
+ let labels: SmartCollectionDialogLabels | null = null;
23
+ let element: JantSmartCollectionDialog | null = null;
24
+ let collections: Promise<SiteCollection[]> | null = null;
25
+
26
+ interface SiteCollection {
27
+ id: string;
28
+ slug: string;
29
+ title: string;
30
+ }
31
+
32
+ /**
33
+ * The dialog's strings, rendered into the page by whichever surface can open
34
+ * it. Read once and cached: they are the same on every surface of a page, and
35
+ * the collection vocabulary is written into this object once it loads.
36
+ */
37
+ function readLabels(): SmartCollectionDialogLabels | null {
38
+ if (labels) return labels;
39
+ const holder = document.querySelector<HTMLElement>(`[${LABELS_ATTRIBUTE}]`);
40
+ const raw = holder?.getAttribute(LABELS_ATTRIBUTE);
41
+ if (!raw) return null;
42
+ try {
43
+ labels = JSON.parse(raw) as SmartCollectionDialogLabels;
44
+ return labels;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Load the site's collections so the collection condition can offer them.
52
+ *
53
+ * Fetched at most once per page, and only when the dialog is actually opened —
54
+ * most page loads never open it. The names are written into the labels on every
55
+ * open rather than only on the first: an opened-once flag would leave the
56
+ * second open with a collection menu that lists nothing.
57
+ */
58
+ async function ensureCollectionVocabulary(
59
+ target: SmartCollectionDialogLabels,
60
+ ): Promise<void> {
61
+ collections ??= fetchCollections();
62
+ const loaded = await collections;
63
+ setCollectionVocabulary(loaded);
64
+ for (const collection of loaded) {
65
+ target.values[`collection.${collection.slug}`] = collection.title;
66
+ }
67
+ }
68
+
69
+ async function fetchCollections(): Promise<SiteCollection[]> {
70
+ try {
71
+ const res = await fetch(publicPath("/api/collections"));
72
+ if (!res.ok) return [];
73
+ const json = (await res.json()) as { collections?: SiteCollection[] };
74
+ return json.collections ?? [];
75
+ } catch {
76
+ // The condition is still offered; it simply lists nothing to choose.
77
+ return [];
78
+ }
79
+ }
80
+
81
+ function ensureElement(
82
+ dialogLabels: SmartCollectionDialogLabels,
83
+ ): JantSmartCollectionDialog {
84
+ if (element?.isConnected) {
85
+ element.labels = dialogLabels;
86
+ return element;
87
+ }
88
+ element = document.createElement(
89
+ "jant-smart-collection-dialog",
90
+ ) as JantSmartCollectionDialog;
91
+ element.labels = dialogLabels;
92
+ document.body.appendChild(element);
93
+ return element;
94
+ }
95
+
96
+ /**
97
+ * Open the smart collection dialog.
98
+ *
99
+ * @param options.smartCollectionId - Edit this one; omit to create a new one
100
+ * @param options.prefill - Starting values, shown to the author before anything
101
+ * is saved
102
+ * @returns Whether anything was created, changed, or deleted
103
+ * @example
104
+ * await openSmartCollectionDialog({ smartCollectionId: "smc_…" });
105
+ */
106
+ export async function openSmartCollectionDialog(options: {
107
+ smartCollectionId?: string;
108
+ prefill?: SmartCollectionDialogState;
109
+ }): Promise<boolean> {
110
+ const dialogLabels = readLabels();
111
+ if (!dialogLabels) return false;
112
+
113
+ await ensureCollectionVocabulary(dialogLabels);
114
+ const dialog = ensureElement(dialogLabels);
115
+ return dialog.open(options);
116
+ }