@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
@@ -0,0 +1,207 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { describe, it, expect, afterEach } from "vitest";
4
+
5
+ import "../jant-locale-picker.js";
6
+ import type { JantLocalePicker } from "../jant-locale-picker.js";
7
+
8
+ const locales = [
9
+ { tag: "en", native: "English", english: "English", coverage: 1 },
10
+ {
11
+ tag: "zh-Hans",
12
+ native: "简体中文",
13
+ english: "Simplified Chinese",
14
+ coverage: 1,
15
+ },
16
+ { tag: "ja", native: "日本語", english: "Japanese", coverage: 0 },
17
+ { tag: "fi", native: "suomi", english: "Finnish", coverage: 0 },
18
+ ];
19
+
20
+ const labels = { search: "Search…", empty: "No matches." };
21
+
22
+ async function createPicker(options?: {
23
+ value?: string;
24
+ exclude?: string[];
25
+ target?: HTMLSelectElement;
26
+ }): Promise<JantLocalePicker> {
27
+ if (options?.target) document.body.appendChild(options.target);
28
+ const el = document.createElement("jant-locale-picker") as JantLocalePicker;
29
+ el.locales = locales;
30
+ el.labels = labels;
31
+ el.value = options?.value ?? "en";
32
+ if (options?.exclude) el.exclude = options.exclude;
33
+ if (options?.target) el.htmlFor = options.target.id;
34
+ document.body.appendChild(el);
35
+ await el.updateComplete;
36
+ return el;
37
+ }
38
+
39
+ function trigger(el: JantLocalePicker): HTMLButtonElement {
40
+ const button = el.querySelector<HTMLButtonElement>("[data-language-trigger]");
41
+ if (!button) throw new Error("trigger missing");
42
+ return button;
43
+ }
44
+
45
+ function optionLabels(el: JantLocalePicker): string[] {
46
+ return [...el.querySelectorAll('[role="option"]')].map(
47
+ (node) => node.textContent?.trim() ?? "",
48
+ );
49
+ }
50
+
51
+ async function press(el: JantLocalePicker, key: string) {
52
+ document.dispatchEvent(
53
+ new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }),
54
+ );
55
+ await el.updateComplete;
56
+ }
57
+
58
+ function makeSelect(id: string, value: string): HTMLSelectElement {
59
+ const select = document.createElement("select");
60
+ select.id = id;
61
+ for (const locale of locales) {
62
+ const option = document.createElement("option");
63
+ option.value = locale.tag;
64
+ option.textContent = locale.native;
65
+ select.appendChild(option);
66
+ }
67
+ select.value = value;
68
+ return select;
69
+ }
70
+
71
+ afterEach(() => {
72
+ document.body.innerHTML = "";
73
+ });
74
+
75
+ describe("JantLocalePicker", () => {
76
+ it("shows the selected language on the trigger", async () => {
77
+ const el = await createPicker({ value: "zh-Hans" });
78
+ expect(trigger(el).textContent).toContain("简体中文");
79
+ });
80
+
81
+ it("filters on native name, English name, or tag", async () => {
82
+ const el = await createPicker();
83
+ trigger(el).click();
84
+ await el.updateComplete;
85
+ expect(optionLabels(el)).toHaveLength(locales.length);
86
+
87
+ const search = el.querySelector<HTMLInputElement>("[data-language-search]");
88
+ if (!search) throw new Error("search missing");
89
+
90
+ search.value = "finn";
91
+ search.dispatchEvent(new Event("input", { bubbles: true }));
92
+ await el.updateComplete;
93
+ expect(optionLabels(el).join()).toContain("suomi");
94
+ expect(optionLabels(el)).toHaveLength(1);
95
+
96
+ search.value = "zh";
97
+ search.dispatchEvent(new Event("input", { bubbles: true }));
98
+ await el.updateComplete;
99
+ expect(optionLabels(el).join()).toContain("简体中文");
100
+ });
101
+
102
+ it("leaves out excluded languages", async () => {
103
+ const el = await createPicker({ exclude: ["ja", "fi"] });
104
+ trigger(el).click();
105
+ await el.updateComplete;
106
+ expect(optionLabels(el)).toHaveLength(2);
107
+ });
108
+
109
+ it("says so when nothing matches", async () => {
110
+ const el = await createPicker();
111
+ trigger(el).click();
112
+ await el.updateComplete;
113
+
114
+ const search = el.querySelector<HTMLInputElement>("[data-language-search]");
115
+ if (search) {
116
+ search.value = "zzzz";
117
+ search.dispatchEvent(new Event("input", { bubbles: true }));
118
+ }
119
+ await el.updateComplete;
120
+ expect(el.textContent).toContain(labels.empty);
121
+ });
122
+
123
+ it("moves through the list with the arrow keys and picks with Enter", async () => {
124
+ const el = await createPicker();
125
+ const chosen: string[] = [];
126
+ el.addEventListener("locale-select", (event) => {
127
+ chosen.push((event as CustomEvent<{ tag: string }>).detail.tag);
128
+ });
129
+
130
+ trigger(el).click();
131
+ await el.updateComplete;
132
+
133
+ await press(el, "ArrowDown");
134
+ await press(el, "Enter");
135
+
136
+ expect(chosen).toEqual(["zh-Hans"]);
137
+ expect(el.value).toBe("zh-Hans");
138
+ });
139
+
140
+ it("wraps around at the end of the list", async () => {
141
+ const el = await createPicker();
142
+ trigger(el).click();
143
+ await el.updateComplete;
144
+
145
+ // Starts on the selected language (index 0), so one step back wraps.
146
+ await press(el, "ArrowUp");
147
+ const active = el.querySelector("[data-language-option][data-active]");
148
+ expect(active?.textContent).toContain("suomi");
149
+ });
150
+
151
+ it("closes on Escape and on an outside click", async () => {
152
+ const el = await createPicker();
153
+
154
+ trigger(el).click();
155
+ await el.updateComplete;
156
+ expect(el.querySelector("[data-language-search]")).not.toBeNull();
157
+
158
+ await press(el, "Escape");
159
+ expect(el.querySelector("[data-language-search]")).toBeNull();
160
+
161
+ trigger(el).click();
162
+ await el.updateComplete;
163
+ document.body.dispatchEvent(new Event("click", { bubbles: true }));
164
+ await el.updateComplete;
165
+ expect(el.querySelector("[data-language-search]")).toBeNull();
166
+ });
167
+
168
+ it("does not let Enter escape to a surrounding form", async () => {
169
+ const el = await createPicker();
170
+ let escaped = 0;
171
+ document.body.addEventListener("keydown", () => {
172
+ escaped++;
173
+ });
174
+
175
+ trigger(el).click();
176
+ await el.updateComplete;
177
+ await press(el, "Enter");
178
+
179
+ expect(escaped).toBe(0);
180
+ });
181
+
182
+ it("writes the choice to the form control it drives", async () => {
183
+ const select = makeSelect("content-language", "en");
184
+ const el = await createPicker({ target: select });
185
+ const events: string[] = [];
186
+ select.addEventListener("input", () => events.push("input"));
187
+ select.addEventListener("change", () => events.push("change"));
188
+
189
+ trigger(el).click();
190
+ await el.updateComplete;
191
+ el.querySelectorAll<HTMLButtonElement>('[role="option"]')[2]?.click();
192
+ await el.updateComplete;
193
+
194
+ expect(select.value).toBe("ja");
195
+ expect(events).toEqual(["input", "change"]);
196
+ });
197
+
198
+ it("takes the plain control out of the layout once it upgrades", async () => {
199
+ const select = makeSelect("content-language", "ja");
200
+ const el = await createPicker({ value: "", target: select });
201
+
202
+ expect(select.hidden).toBe(true);
203
+ // With no value of its own the picker adopts the control's, so it opens on
204
+ // whatever the form would have posted.
205
+ expect(trigger(el).textContent).toContain("日本語");
206
+ });
207
+ });
@@ -49,13 +49,6 @@ const collectionFormLabels: CollectionFormLabels = {
49
49
  quickHint: "More options are available after you create it.",
50
50
  quickSubmitLabel: "Create Collection",
51
51
  createdLabel: "Collection created.",
52
- descriptionLabel: "Description",
53
- descriptionPlaceholder: "What's this collection about?",
54
- sortOrderLabel: "Sort Order",
55
- sortNewest: "Newest first",
56
- sortOldest: "Oldest first",
57
- sortRatingDesc: "Highest rated",
58
- submitLabel: "Save",
59
52
  cancelLabel: "Cancel",
60
53
  };
61
54
 
@@ -95,7 +88,19 @@ const labels: NavManagerLabels = {
95
88
  "Choose a titled note that isn't already in navigation, or create a new page.",
96
89
  addPage: "Add Page",
97
90
  searchPages: "Search pages",
91
+ searchPagesHint: "Search pages, or paste an address",
98
92
  recentPages: "Recently updated",
93
+ addressMatch: "At that address",
94
+ addressAlreadyAdded:
95
+ "Already in navigation. Drag it in the list above to move it.",
96
+ addressNotFound: "Nothing at {address}. Check it, or search by title.",
97
+ addressUnpublished: "That page is a draft. Publish it, then add it.",
98
+ addressPrivate: "That page is private, so nobody could open it from a menu.",
99
+ addressUntitled: "That page has no title yet, so a menu has nothing to show.",
100
+ addressExternal:
101
+ "That address is on another site. Navigation holds it as a link.",
102
+ addressLinkOnly: "Navigation holds that address as a link.",
103
+ addressAddAsLink: "Add as link",
99
104
  searchingPages: "Searching pages…",
100
105
  noMatchingPages: "No matching pages available to add.",
101
106
  noPages: "No pages available.",
@@ -122,6 +127,7 @@ const labels: NavManagerLabels = {
122
127
  back: "Back",
123
128
  headerSection: "Header",
124
129
  moreSection: "More",
130
+ previewMore: "More",
125
131
  moreEmptyHint: "Move links here to hide them under More.",
126
132
  placementSaved: "Navigation placement updated.",
127
133
  cancel: "Cancel",
@@ -448,14 +454,9 @@ describe("JantNavManager", () => {
448
454
  body: JSON.stringify({ title: "Books", slug: "books" }),
449
455
  }),
450
456
  );
451
- const editLink = requireElement(
452
- el.querySelector<HTMLAnchorElement>(
453
- 'a[href="/collections/books/edit?returnTo=%2Fsettings%2Fnavigation"]',
454
- ),
455
- "expected collection edit link",
456
- );
457
- expect(editLink.target).toBe("_blank");
458
- expect(editLink.rel).toContain("noopener");
457
+ // The full editor is the collection dialog opened on top of this one,
458
+ // not a link out to a page that no longer exists.
459
+ expect(findButton(el, "Edit Collection").tagName).toBe("BUTTON");
459
460
  expect(el.collections).toContainEqual({
460
461
  id: "col-books",
461
462
  title: "Books",
@@ -556,6 +557,23 @@ describe("JantNavManager", () => {
556
557
  );
557
558
  });
558
559
 
560
+ it("labels the preview overflow button with the site header's own wording", async () => {
561
+ const el = await createElement();
562
+ el.labels = { ...labels, moreSection: "Overflow", previewMore: "More" };
563
+ await el.updateComplete;
564
+
565
+ const trigger = requireElement(
566
+ el.querySelector<HTMLElement>("[data-preview-more-trigger]"),
567
+ "expected preview more trigger",
568
+ );
569
+ const sectionHeading = Array.from(el.querySelectorAll("section > h2")).map(
570
+ (heading) => heading.textContent?.trim(),
571
+ );
572
+
573
+ expect(trigger.textContent?.trim()).toBe("More");
574
+ expect(sectionHeading).toContain("Overflow");
575
+ });
576
+
559
577
  it("opens and dismisses the preview More popover", async () => {
560
578
  const el = await createElement();
561
579
  const trigger = requireElement(
@@ -611,6 +629,33 @@ describe("JantNavManager", () => {
611
629
  expect(getPreviewHeaderLabels(el)).toContain("Feed");
612
630
  });
613
631
 
632
+ // `subscribe` addresses a page that 404s without feeds, so it has to drop out
633
+ // of the preview alongside `rss` — the server's projection drops both.
634
+ it("hides a saved Subscribe item from the preview too", async () => {
635
+ const el = await createElement();
636
+ el.items = [
637
+ ...items,
638
+ {
639
+ id: "nav-subscribe",
640
+ type: "system",
641
+ systemKey: "subscribe",
642
+ label: "Subscribe",
643
+ displayLabel: "Subscribe",
644
+ url: "/subscribe",
645
+ placement: "header",
646
+ },
647
+ ];
648
+ el.rssFeedsEnabled = false;
649
+ await el.updateComplete;
650
+
651
+ expect(getPreviewHeaderLabels(el)).not.toContain("Subscribe");
652
+ expect(el.querySelector('[data-nav-id="nav-subscribe"]')).not.toBeNull();
653
+
654
+ el.rssFeedsEnabled = true;
655
+ await el.updateComplete;
656
+ expect(getPreviewHeaderLabels(el)).toContain("Subscribe");
657
+ });
658
+
614
659
  it("adds a suggested collection link through the nav items API", async () => {
615
660
  const el = await createElement();
616
661
  el.suggestedLinks = suggestedLinks;
@@ -1020,4 +1065,178 @@ describe("JantNavManager", () => {
1020
1065
  expect(editPageLink.target).toBe("_blank");
1021
1066
  expect(editPageLink.rel).toContain("noopener");
1022
1067
  });
1068
+
1069
+ describe("pasting an address into the page picker", () => {
1070
+ /** Type into the picker and wait out the debounce. */
1071
+ async function search(el: JantNavManager, value: string) {
1072
+ const input = requireElement(
1073
+ el.querySelector<HTMLInputElement>("#nav-page-search"),
1074
+ "page search input",
1075
+ );
1076
+ input.value = value;
1077
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1078
+ await new Promise((resolve) => setTimeout(resolve, 250));
1079
+ await flushAsyncWork();
1080
+ await el.updateComplete;
1081
+ }
1082
+
1083
+ async function openPicker(
1084
+ resolution: unknown,
1085
+ ): Promise<{ el: JantNavManager; fetchMock: ReturnType<typeof vi.fn> }> {
1086
+ const el = await createElement();
1087
+ const fetchMock = vi
1088
+ .fn()
1089
+ .mockImplementation((input: string, init?: globalThis.RequestInit) => {
1090
+ if (input.startsWith("/api/nav-items/pages?")) {
1091
+ return Promise.resolve({
1092
+ ok: true,
1093
+ status: 200,
1094
+ json: async () => ({ pages: [] }),
1095
+ });
1096
+ }
1097
+ if (input.startsWith("/api/nav-items/resolve?")) {
1098
+ return Promise.resolve({
1099
+ ok: true,
1100
+ status: 200,
1101
+ json: async () => ({ resolution }),
1102
+ });
1103
+ }
1104
+ if (input === "/api/nav-items" && init?.method === "POST") {
1105
+ return Promise.resolve({
1106
+ ok: true,
1107
+ status: 201,
1108
+ json: async () => ({
1109
+ id: "nav-added",
1110
+ type: "page",
1111
+ postId: "pst_about",
1112
+ label: "About me",
1113
+ url: "/about-me",
1114
+ placement: "header",
1115
+ }),
1116
+ });
1117
+ }
1118
+ throw new Error(`Unexpected fetch: ${input}`);
1119
+ });
1120
+ vi.stubGlobal("fetch", fetchMock);
1121
+
1122
+ findButton(el, "Add Page").click();
1123
+ await flushAsyncWork();
1124
+ await el.updateComplete;
1125
+ return { el, fetchMock };
1126
+ }
1127
+
1128
+ it("looks the address up instead of searching titles", async () => {
1129
+ const { el, fetchMock } = await openPicker({
1130
+ kind: "page",
1131
+ address: "/about-me",
1132
+ page: {
1133
+ id: "pst_about",
1134
+ title: "About me",
1135
+ slug: "about-me",
1136
+ updatedAt: 123,
1137
+ },
1138
+ });
1139
+
1140
+ await search(el, "https://example.com/about-me");
1141
+
1142
+ expect(fetchMock).toHaveBeenCalledWith(
1143
+ `/api/nav-items/resolve?url=${encodeURIComponent("https://example.com/about-me")}`,
1144
+ expect.objectContaining({
1145
+ headers: expect.objectContaining({ Accept: "application/json" }),
1146
+ }),
1147
+ );
1148
+ expect(el.textContent).toContain("At that address");
1149
+
1150
+ findButton(el, "About me").click();
1151
+ await flushAsyncWork();
1152
+ await el.updateComplete;
1153
+
1154
+ expect(fetchMock).toHaveBeenCalledWith(
1155
+ "/api/nav-items",
1156
+ expect.objectContaining({
1157
+ method: "POST",
1158
+ body: JSON.stringify({
1159
+ type: "page",
1160
+ postId: "pst_about",
1161
+ placement: "header",
1162
+ }),
1163
+ }),
1164
+ );
1165
+ });
1166
+
1167
+ it("says why a page cannot be added, rather than showing nothing", async () => {
1168
+ const { el } = await openPicker({
1169
+ kind: "unpublished",
1170
+ address: "/draft-page",
1171
+ });
1172
+
1173
+ await search(el, "/draft-page");
1174
+
1175
+ expect(el.textContent).toContain("That page is a draft");
1176
+ expect(el.textContent).not.toContain("No matching pages");
1177
+ });
1178
+
1179
+ it("names the address it could not find", async () => {
1180
+ const { el } = await openPicker({
1181
+ kind: "not_found",
1182
+ address: "/typo",
1183
+ });
1184
+
1185
+ await search(el, "/typo");
1186
+
1187
+ expect(el.textContent).toContain("Nothing at /typo.");
1188
+ });
1189
+
1190
+ it("hands an off-site address to the link form, filled in", async () => {
1191
+ const { el } = await openPicker({
1192
+ kind: "external",
1193
+ address: "https://example.org/hello",
1194
+ });
1195
+
1196
+ await search(el, "https://example.org/hello");
1197
+ expect(el.textContent).toContain("That address is on another site");
1198
+
1199
+ findButton(el, "Add as link").click();
1200
+ await el.updateComplete;
1201
+
1202
+ // The picker is out of the way and the URL is already in the form, so
1203
+ // the only thing left to type is the label.
1204
+ expect(el.querySelector("#nav-page-dialog")).toBeNull();
1205
+ expect(
1206
+ requireElement(
1207
+ el.querySelector<HTMLInputElement>("#nav-link-url"),
1208
+ "link url input",
1209
+ ).value,
1210
+ ).toBe("https://example.org/hello");
1211
+ });
1212
+
1213
+ it("does not offer a page that is already in navigation", async () => {
1214
+ const { el } = await openPicker({
1215
+ kind: "page",
1216
+ address: "/about",
1217
+ page: {
1218
+ id: "pst_nav_1",
1219
+ title: "About",
1220
+ slug: "about",
1221
+ updatedAt: 123,
1222
+ },
1223
+ });
1224
+ el.items = [
1225
+ {
1226
+ id: "nav-1",
1227
+ type: "page",
1228
+ postId: "pst_nav_1",
1229
+ label: "About",
1230
+ url: "/about",
1231
+ placement: "header",
1232
+ },
1233
+ ];
1234
+ await el.updateComplete;
1235
+
1236
+ await search(el, "/about");
1237
+
1238
+ expect(el.textContent).toContain("Already in navigation");
1239
+ expect(el.querySelector(".nav-page-result")).toBeNull();
1240
+ });
1241
+ });
1023
1242
  });