@jant/core 0.6.16 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -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 };
@@ -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
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Smart collection page owner menu.
3
+ *
4
+ * The same menu shape a collection page has, item for item — Edit, Add to
5
+ * navigation, Edit navigation, Delete — with one difference: Edit opens the
6
+ * dialog rather than navigating to an editor page, because a smart collection
7
+ * has no editor page. There is no Edit button beside the title either; a
8
+ * collection page has none, and the menu is where a page keeps its owner
9
+ * actions.
10
+ */
11
+
12
+ import { getCollectionsDirectoryPath } from "../lib/collection-paths.js";
13
+ import { addSmartCollectionToNavigation } from "./collection-navigation.js";
14
+ import { NAVIGATION_SETTINGS_PATH } from "../lib/settings-paths.js";
15
+ import { showConfirmDialog } from "./confirm.js";
16
+ import { showToast, showToastWithAction } from "./toast.js";
17
+ import { openSmartCollectionDialog } from "./smart-collection-dialog-host.js";
18
+
19
+ interface PageActionLabels {
20
+ addToNavigation: string;
21
+ addingToNavigation: string;
22
+ addedToNavigation: string;
23
+ editNavigation: string;
24
+ addToNavigationFailed: string;
25
+ confirmDelete: string;
26
+ deleteCollection: string;
27
+ cancel: string;
28
+ saveFailed: string;
29
+ deleted: string;
30
+ }
31
+
32
+ const parseLabels = (value: string | undefined): PageActionLabels => {
33
+ const empty: PageActionLabels = {
34
+ addToNavigation: "",
35
+ addingToNavigation: "",
36
+ addedToNavigation: "",
37
+ editNavigation: "",
38
+ addToNavigationFailed: "",
39
+ confirmDelete: "",
40
+ deleteCollection: "",
41
+ cancel: "",
42
+ saveFailed: "",
43
+ deleted: "",
44
+ };
45
+ if (!value) return empty;
46
+ try {
47
+ return { ...empty, ...(JSON.parse(value) as Partial<PageActionLabels>) };
48
+ } catch {
49
+ return empty;
50
+ }
51
+ };
52
+
53
+ document
54
+ .querySelectorAll<HTMLElement>("[data-smart-collection-page-actions]")
55
+ .forEach((root) => {
56
+ if (root.dataset.smartCollectionPageActionsInitialized === "true") return;
57
+
58
+ const labels = parseLabels(root.dataset.collectionPageLabels);
59
+ const smartCollectionId = root.dataset.smartCollectionId;
60
+ const redirectUrl =
61
+ root.dataset.collectionPageRedirectUrl || getCollectionsDirectoryPath();
62
+ const trigger = root.querySelector<HTMLElement>(
63
+ "[data-collection-page-action='toggle-menu']",
64
+ );
65
+ const menu = root.querySelector<HTMLElement>("[data-collection-page-menu]");
66
+ const addToNavigationButton = root.querySelector<HTMLButtonElement>(
67
+ "[data-collection-page-action='add-to-navigation']",
68
+ );
69
+ const editNavigationLink = root.querySelector<HTMLAnchorElement>(
70
+ "[data-collection-page-edit-navigation]",
71
+ );
72
+
73
+ if (!smartCollectionId || !trigger || !menu) return;
74
+
75
+ const closeMenu = (focusTrigger = false) => {
76
+ if (menu.hidden) return;
77
+ menu.hidden = true;
78
+ trigger.setAttribute("aria-expanded", "false");
79
+ if (focusTrigger) trigger.focus();
80
+ };
81
+
82
+ const openMenu = (focusFirstItem = false) => {
83
+ menu.hidden = false;
84
+ trigger.setAttribute("aria-expanded", "true");
85
+ if (focusFirstItem) {
86
+ menu.querySelector<HTMLElement>("[role='menuitem']")?.focus();
87
+ }
88
+ };
89
+
90
+ const handleEdit = async () => {
91
+ closeMenu(false);
92
+ const changed = await openSmartCollectionDialog({ smartCollectionId });
93
+ // The address, the title, the conditions, and the count can all have
94
+ // moved, so the page is re-read rather than patched in place.
95
+ if (changed) window.location.reload();
96
+ };
97
+
98
+ const handleDelete = async () => {
99
+ closeMenu(false);
100
+ const confirmed = await showConfirmDialog({
101
+ message: labels.confirmDelete,
102
+ confirmLabel: labels.deleteCollection,
103
+ cancelLabel: labels.cancel,
104
+ tone: "danger",
105
+ });
106
+ if (!confirmed) return;
107
+
108
+ try {
109
+ const res = await fetch(`/api/smart-collections/${smartCollectionId}`, {
110
+ method: "DELETE",
111
+ });
112
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
113
+ showToast(labels.deleted);
114
+ window.location.href = redirectUrl;
115
+ } catch {
116
+ showToast(labels.saveFailed, "error");
117
+ }
118
+ };
119
+
120
+ const handleAddToNavigation = async () => {
121
+ if (!addToNavigationButton || addToNavigationButton.disabled) return;
122
+
123
+ closeMenu(false);
124
+ const label = addToNavigationButton.querySelector<HTMLElement>(
125
+ ".collections-page-menu-item-label",
126
+ );
127
+ addToNavigationButton.disabled = true;
128
+ if (label) label.textContent = labels.addingToNavigation;
129
+
130
+ try {
131
+ await addSmartCollectionToNavigation(smartCollectionId);
132
+
133
+ addToNavigationButton.hidden = true;
134
+ if (editNavigationLink) editNavigationLink.hidden = false;
135
+ root.dataset.collectionInNavigation = "true";
136
+ showToastWithAction(labels.addedToNavigation, {
137
+ label: labels.editNavigation,
138
+ href:
139
+ editNavigationLink?.getAttribute("href") ??
140
+ NAVIGATION_SETTINGS_PATH,
141
+ });
142
+ } catch {
143
+ showToast(labels.addToNavigationFailed, "error");
144
+ } finally {
145
+ addToNavigationButton.disabled = false;
146
+ if (label) label.textContent = labels.addToNavigation;
147
+ }
148
+ };
149
+
150
+ trigger.addEventListener("click", (event) => {
151
+ event.preventDefault();
152
+ event.stopPropagation();
153
+ if (menu.hidden) openMenu(false);
154
+ else closeMenu(false);
155
+ });
156
+
157
+ trigger.addEventListener("keydown", (event) => {
158
+ if (
159
+ event.key === "Enter" ||
160
+ event.key === " " ||
161
+ event.key === "ArrowDown"
162
+ ) {
163
+ event.preventDefault();
164
+ if (menu.hidden) openMenu(true);
165
+ else closeMenu(false);
166
+ }
167
+ if (event.key === "Escape") {
168
+ event.preventDefault();
169
+ closeMenu(true);
170
+ }
171
+ });
172
+
173
+ menu.addEventListener("keydown", (event) => {
174
+ if (event.key === "Escape") {
175
+ event.preventDefault();
176
+ closeMenu(true);
177
+ }
178
+ });
179
+
180
+ root.addEventListener("click", (event) => {
181
+ const target = event.target as HTMLElement | null;
182
+ const actionEl = target?.closest<HTMLElement>(
183
+ "[data-collection-page-action]",
184
+ );
185
+ if (!actionEl || !root.contains(actionEl)) return;
186
+
187
+ const action = actionEl.dataset.collectionPageAction;
188
+ if (action === "edit") {
189
+ event.preventDefault();
190
+ void handleEdit();
191
+ }
192
+ if (action === "delete") {
193
+ event.preventDefault();
194
+ void handleDelete();
195
+ }
196
+ if (action === "add-to-navigation") {
197
+ event.preventDefault();
198
+ void handleAddToNavigation();
199
+ }
200
+ });
201
+
202
+ document.addEventListener("click", (event) => {
203
+ if (!(event.target instanceof Node)) return;
204
+ if (!root.contains(event.target)) closeMenu(false);
205
+ });
206
+
207
+ root.dataset.smartCollectionPageActionsInitialized = "true";
208
+ });