@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,72 @@
1
+ /**
2
+ * Collection dialog host.
3
+ *
4
+ * One dialog element for the whole page, mounted lazily and reused. Four
5
+ * places open it — the collections page toolbar, a directory row's menu, a
6
+ * collection page's own menu, and the navigation manager — and they all go
7
+ * through {@link openCollectionDialog}, because creating and editing are one
8
+ * surface.
9
+ */
10
+
11
+ import "./components/jant-collection-dialog.js";
12
+ import type { JantCollectionDialog } from "./components/jant-collection-dialog.js";
13
+ import type {
14
+ CollectionDialogLabels,
15
+ CollectionDialogResult,
16
+ } from "./components/collection-dialog-types.js";
17
+
18
+ const LABELS_ATTRIBUTE = "data-collection-dialog-labels";
19
+
20
+ let labels: CollectionDialogLabels | null = null;
21
+ let element: JantCollectionDialog | null = null;
22
+
23
+ /**
24
+ * The dialog's strings, rendered into the page by whichever surface can open
25
+ * it. Read once and cached: they are the same on every surface of a page.
26
+ */
27
+ function readLabels(): CollectionDialogLabels | null {
28
+ if (labels) return labels;
29
+ const holder = document.querySelector<HTMLElement>(`[${LABELS_ATTRIBUTE}]`);
30
+ const raw = holder?.getAttribute(LABELS_ATTRIBUTE);
31
+ if (!raw) return null;
32
+ try {
33
+ labels = JSON.parse(raw) as CollectionDialogLabels;
34
+ return labels;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function ensureElement(
41
+ dialogLabels: CollectionDialogLabels,
42
+ ): JantCollectionDialog {
43
+ if (element?.isConnected) {
44
+ element.labels = dialogLabels;
45
+ return element;
46
+ }
47
+ element = document.createElement(
48
+ "jant-collection-dialog",
49
+ ) as JantCollectionDialog;
50
+ element.labels = dialogLabels;
51
+ document.body.appendChild(element);
52
+ return element;
53
+ }
54
+
55
+ /**
56
+ * Open the collection dialog.
57
+ *
58
+ * @param options.collectionId - Edit this one; omit to create a new one
59
+ * @returns What happened, and the collection when one was saved
60
+ * @example
61
+ * const { changed } = await openCollectionDialog({ collectionId: "col_…" });
62
+ */
63
+ export async function openCollectionDialog(
64
+ options: {
65
+ collectionId?: string;
66
+ } = {},
67
+ ): Promise<CollectionDialogResult> {
68
+ const dialogLabels = readLabels();
69
+ if (!dialogLabels) return { changed: false };
70
+
71
+ return ensureElement(dialogLabels).open(options);
72
+ }
@@ -9,16 +9,14 @@ const SITE_HEADER_REQUEST_HEADER = "X-Jant-Site-Header";
9
9
  const INCLUDE_SITE_HEADER_RESPONSE = "include";
10
10
 
11
11
  /**
12
- * Add a Collection to the end of the primary navigation and refresh the
13
- * rendered site header when the server returns a fragment.
12
+ * Create a navigation item and refresh the rendered site header when the
13
+ * server returns a fragment.
14
14
  *
15
- * @param collectionId - TypeID of the Collection to add
15
+ * @param body - Request body for `POST /api/nav-items`
16
16
  * @returns The created navigation item ID when present
17
- * @example
18
- * const navItemId = await addCollectionToNavigation("col_01abc");
19
17
  */
20
- export async function addCollectionToNavigation(
21
- collectionId: string,
18
+ async function createNavItem(
19
+ body: Record<string, unknown>,
22
20
  ): Promise<string | undefined> {
23
21
  const response = await fetch("/api/nav-items", {
24
22
  method: "POST",
@@ -27,18 +25,55 @@ export async function addCollectionToNavigation(
27
25
  Accept: "application/json",
28
26
  [SITE_HEADER_REQUEST_HEADER]: INCLUDE_SITE_HEADER_RESPONSE,
29
27
  },
30
- body: JSON.stringify({
31
- type: "collection",
32
- collectionId,
33
- placement: "header",
34
- }),
28
+ body: JSON.stringify(body),
35
29
  });
36
- const body = await readJsonObject(response);
30
+ const responseBody = await readJsonObject(response);
37
31
 
38
32
  if (!response.ok) {
39
- throw new Error(getJsonString(body, "error") ?? `HTTP ${response.status}`);
33
+ throw new Error(
34
+ getJsonString(responseBody, "error") ?? `HTTP ${response.status}`,
35
+ );
40
36
  }
41
37
 
42
- applySiteHeaderHtml(getJsonString(body, "headerHtml"));
43
- return getJsonString(body, "id");
38
+ applySiteHeaderHtml(getJsonString(responseBody, "headerHtml"));
39
+ return getJsonString(responseBody, "id");
40
+ }
41
+
42
+ /**
43
+ * Add a Collection to the end of the primary navigation and refresh the
44
+ * rendered site header when the server returns a fragment.
45
+ *
46
+ * @param collectionId - TypeID of the Collection to add
47
+ * @returns The created navigation item ID when present
48
+ * @example
49
+ * const navItemId = await addCollectionToNavigation("col_01abc");
50
+ */
51
+ export async function addCollectionToNavigation(
52
+ collectionId: string,
53
+ ): Promise<string | undefined> {
54
+ return createNavItem({
55
+ type: "collection",
56
+ collectionId,
57
+ placement: "header",
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Add a smart collection to the end of the primary navigation. Placement
63
+ * matches {@link addCollectionToNavigation} — the two kinds are one thing to
64
+ * the reader, so the same action puts them in the same place.
65
+ *
66
+ * @param smartCollectionId - TypeID of the smart collection to add
67
+ * @returns The created navigation item ID when present
68
+ * @example
69
+ * const navItemId = await addSmartCollectionToNavigation("smc_01abc");
70
+ */
71
+ export async function addSmartCollectionToNavigation(
72
+ smartCollectionId: string,
73
+ ): Promise<string | undefined> {
74
+ return createNavItem({
75
+ type: "smart_collection",
76
+ smartCollectionId,
77
+ placement: "header",
78
+ });
44
79
  }
@@ -1,5 +1,6 @@
1
1
  import { getCollectionsDirectoryPath } from "../lib/collection-paths.js";
2
2
  import { NAVIGATION_SETTINGS_PATH } from "../lib/settings-paths.js";
3
+ import { openCollectionDialog } from "./collection-dialog-host.js";
3
4
  import { addCollectionToNavigation } from "./collection-navigation.js";
4
5
  import { showConfirmDialog } from "./confirm.js";
5
6
  import { showToast, showToastWithAction } from "./toast.js";
@@ -95,6 +96,27 @@ document
95
96
  }
96
97
  };
97
98
 
99
+ const handleEdit = async () => {
100
+ closeMenu(false);
101
+ const { changed, collection } = await openCollectionDialog({
102
+ collectionId,
103
+ });
104
+ if (!changed) return;
105
+
106
+ // The address can have moved, and the old one stops resolving the moment
107
+ // it does — so the page follows it rather than reloading into a 404.
108
+ // Only the last segment changes: whatever site or language prefix this
109
+ // reader arrived under is theirs to keep.
110
+ const nextPath = collection?.slug
111
+ ? window.location.pathname.replace(/[^/]*$/, collection.slug)
112
+ : null;
113
+ if (nextPath && nextPath !== window.location.pathname) {
114
+ window.location.href = `${nextPath}${window.location.search}`;
115
+ return;
116
+ }
117
+ window.location.reload();
118
+ };
119
+
98
120
  const handleDelete = async () => {
99
121
  closeMenu(false);
100
122
 
@@ -197,6 +219,10 @@ document
197
219
  if (!actionEl || !root.contains(actionEl)) return;
198
220
 
199
221
  const action = actionEl.dataset.collectionPageAction;
222
+ if (action === "edit") {
223
+ event.preventDefault();
224
+ void handleEdit();
225
+ }
200
226
  if (action === "delete") {
201
227
  event.preventDefault();
202
228
  void handleDelete();
@@ -0,0 +1,329 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ vi.mock("../../toast.js", () => ({
6
+ showToast: vi.fn(),
7
+ showToastWithAction: vi.fn(),
8
+ }));
9
+
10
+ vi.mock("../../lazy-slugify.js", () => ({
11
+ slugify: (text: string) =>
12
+ Promise.resolve(
13
+ text
14
+ .toLowerCase()
15
+ .trim()
16
+ .replace(/[^\w\s-]/g, "")
17
+ .replace(/[\s_-]+/g, "-")
18
+ .replace(/^-+|-+$/g, ""),
19
+ ),
20
+ preloadSlug: () => {},
21
+ }));
22
+
23
+ // happy-dom has no contenteditable, so the description editor is stood in for.
24
+ let editorContent: string | undefined;
25
+ vi.mock("../../tiptap/create-editor.js", () => ({
26
+ createSettingsEditor: (opts: { element: HTMLElement; content?: string }) => {
27
+ editorContent = opts.content;
28
+ opts.element.innerHTML =
29
+ '<div class="ProseMirror" contenteditable="true"></div>';
30
+ return {
31
+ getJSON: () => ({ type: "doc", content: [] }),
32
+ destroy: () => {},
33
+ };
34
+ },
35
+ jsonToMarkdown: () => "",
36
+ }));
37
+
38
+ import "../jant-collection-dialog.js";
39
+ import type { JantCollectionDialog } from "../jant-collection-dialog.js";
40
+ import type { CollectionDialogLabels } from "../collection-dialog-types.js";
41
+
42
+ /**
43
+ * The dialog is now the only place a collection is written, so what it has to
44
+ * hold is what the editor page used to: an address derived from the title and
45
+ * checked against the server, a warning before an existing link moves, and a
46
+ * save that reports where the collection ended up.
47
+ */
48
+
49
+ const labels: CollectionDialogLabels = {
50
+ createHeading: "New Collection",
51
+ editHeading: "Edit Collection",
52
+ title: "Title",
53
+ titlePlaceholder: "My Collection",
54
+ link: "Collection link",
55
+ linkHelp: "This is the last part of the collection link.",
56
+ editLink: "Edit link",
57
+ resetLink: "Reset link",
58
+ linkTaken: "This link is taken. Choose another.",
59
+ linkInvalid: "Use lowercase letters, numbers, and hyphens only.",
60
+ linkReserved: "This link is reserved. Choose something else.",
61
+ linkTooLong: "Keep this link under 200 characters.",
62
+ linkMovesWarning: "Changing the link breaks the old one immediately.",
63
+ description: "Description (optional)",
64
+ descriptionPlaceholder: "What's this collection about?",
65
+ orderBy: "Order by",
66
+ sortOptions: {
67
+ newest: "Newest first",
68
+ oldest: "Oldest first",
69
+ rating_desc: "Highest rated",
70
+ },
71
+ cancel: "Cancel",
72
+ save: "Save",
73
+ saved: "Collection saved.",
74
+ saveFailed: "Couldn't save. Try again in a moment.",
75
+ loadFailed: "Could not open this collection. Try again.",
76
+ titleAndLinkRequired: "A collection needs a title and a link.",
77
+ };
78
+
79
+ /** Responses the dialog's fetches get, keyed by the path they hit. */
80
+ let responses: Record<string, { ok?: boolean; body: unknown }>;
81
+ let requests: Array<{ url: string; method: string; body: unknown }>;
82
+
83
+ function mountDialog(): JantCollectionDialog {
84
+ const element = document.createElement(
85
+ "jant-collection-dialog",
86
+ ) as JantCollectionDialog;
87
+ element.labels = labels;
88
+ document.body.appendChild(element);
89
+ return element;
90
+ }
91
+
92
+ /** Let the debounced address check and the slugify round trip settle. */
93
+ async function settle(element: JantCollectionDialog) {
94
+ await vi.advanceTimersByTimeAsync(400);
95
+ await element.updateComplete;
96
+ }
97
+
98
+ function findButton(
99
+ element: JantCollectionDialog,
100
+ text: string,
101
+ ): HTMLButtonElement | undefined {
102
+ return [...element.querySelectorAll("button")].find(
103
+ (button) => button.textContent?.trim() === text,
104
+ );
105
+ }
106
+
107
+ async function typeTitle(element: JantCollectionDialog, value: string) {
108
+ const input = element.querySelector<HTMLInputElement>("[data-field='title']");
109
+ if (!input) throw new Error("Expected the title field");
110
+ input.value = value;
111
+ input.dispatchEvent(new Event("input", { bubbles: true }));
112
+ await settle(element);
113
+ }
114
+
115
+ beforeEach(() => {
116
+ document.body.innerHTML = "";
117
+ vi.useFakeTimers();
118
+ editorContent = undefined;
119
+
120
+ responses = {
121
+ "/api/collections/slug": { body: { slug: "reading", available: true } },
122
+ "/api/collections": { body: { id: "col_new", slug: "reading" } },
123
+ "/api/collections/col_books": {
124
+ body: {
125
+ id: "col_books",
126
+ slug: "books",
127
+ title: "Books",
128
+ description: "Notes from books",
129
+ sortOrder: "oldest",
130
+ },
131
+ },
132
+ };
133
+ requests = [];
134
+
135
+ vi.stubGlobal(
136
+ "fetch",
137
+ vi.fn(async (url: string, init?: globalThis.RequestInit) => {
138
+ const path = url.split("?")[0] ?? url;
139
+ requests.push({
140
+ url,
141
+ method: init?.method ?? "GET",
142
+ body: init?.body ? JSON.parse(String(init.body)) : undefined,
143
+ });
144
+ const response = responses[path] ?? { body: {} };
145
+ return {
146
+ ok: response.ok ?? true,
147
+ status: response.ok === false ? 400 : 200,
148
+ json: async () => response.body,
149
+ } as Response;
150
+ }),
151
+ );
152
+
153
+ // happy-dom's <dialog> has no modal implementation.
154
+ HTMLDialogElement.prototype.showModal = function showModal() {
155
+ this.open = true;
156
+ };
157
+ HTMLDialogElement.prototype.close = function close() {
158
+ this.open = false;
159
+ };
160
+ });
161
+
162
+ describe("JantCollectionDialog", () => {
163
+ it("opens on the create heading with nothing filled in", async () => {
164
+ const element = mountDialog();
165
+ void element.open({});
166
+ await settle(element);
167
+
168
+ expect(element.textContent).toContain("New Collection");
169
+ expect(
170
+ element.querySelector<HTMLInputElement>("[data-field='title']")?.value,
171
+ ).toBe("");
172
+ // The link only appears once there is one to show.
173
+ expect(element.querySelector(".collection-quick-link-box")).toBeNull();
174
+ });
175
+
176
+ it("derives the address from the title and asks whether it is free", async () => {
177
+ const element = mountDialog();
178
+ void element.open({});
179
+ await settle(element);
180
+ await typeTitle(element, "Reading");
181
+
182
+ expect(
183
+ element.querySelector<HTMLElement>(".collection-quick-link-preview")
184
+ ?.textContent,
185
+ ).toContain("/reading");
186
+ expect(
187
+ requests.some(
188
+ (request) =>
189
+ request.url.includes("/api/collections/slug") &&
190
+ request.url.includes("slug=reading"),
191
+ ),
192
+ ).toBe(true);
193
+ });
194
+
195
+ it("refuses to save onto an address something else already holds", async () => {
196
+ responses["/api/collections/slug"] = {
197
+ body: { slug: "reading", available: false },
198
+ };
199
+ const element = mountDialog();
200
+ void element.open({});
201
+ await settle(element);
202
+ await typeTitle(element, "Reading");
203
+
204
+ expect(element.textContent).toContain(
205
+ "This link is taken. Choose another.",
206
+ );
207
+ expect(findButton(element, "Save")?.disabled).toBe(true);
208
+ });
209
+
210
+ it("loads an existing collection, and warns before its link moves", async () => {
211
+ const element = mountDialog();
212
+ void element.open({ collectionId: "col_books" });
213
+ await settle(element);
214
+
215
+ expect(element.textContent).toContain("Edit Collection");
216
+ expect(
217
+ element.querySelector<HTMLInputElement>("[data-field='title']")?.value,
218
+ ).toBe("Books");
219
+ const sortSelect = element.querySelector(
220
+ "[data-field='sort']",
221
+ ) as HTMLSelectElement | null;
222
+ expect(sortSelect?.value).toBe("oldest");
223
+ expect(editorContent).toBe("Notes from books");
224
+ expect(element.textContent).not.toContain(
225
+ "Changing the link breaks the old one immediately.",
226
+ );
227
+
228
+ findButton(element, "Edit link")?.click();
229
+ await element.updateComplete;
230
+ const slugInput = element.querySelector<HTMLInputElement>(
231
+ "[data-field='slug']",
232
+ );
233
+ if (!slugInput) throw new Error("Expected the link field");
234
+ slugInput.value = "reading";
235
+ slugInput.dispatchEvent(new Event("input", { bubbles: true }));
236
+ await settle(element);
237
+
238
+ expect(element.textContent).toContain(
239
+ "Changing the link breaks the old one immediately.",
240
+ );
241
+ });
242
+
243
+ it("creates a collection and reports where it landed", async () => {
244
+ const element = mountDialog();
245
+ const result = element.open({});
246
+ await settle(element);
247
+ await typeTitle(element, "Reading");
248
+
249
+ findButton(element, "Save")?.click();
250
+ await settle(element);
251
+
252
+ const save = requests.find((request) => request.method === "POST");
253
+ expect(save?.url).toContain("/api/collections");
254
+ expect(save?.body).toEqual({
255
+ slug: "reading",
256
+ title: "Reading",
257
+ sortOrder: "newest",
258
+ });
259
+ await expect(result).resolves.toEqual({
260
+ changed: true,
261
+ collection: { id: "col_new", slug: "reading", title: "Reading" },
262
+ });
263
+ });
264
+
265
+ it("saves an edit with PUT, and clears a description that was emptied", async () => {
266
+ responses["/api/collections/col_books"] = {
267
+ body: {
268
+ id: "col_books",
269
+ slug: "books",
270
+ title: "Books",
271
+ description: "Notes from books",
272
+ sortOrder: "newest",
273
+ },
274
+ };
275
+ const element = mountDialog();
276
+ void element.open({ collectionId: "col_books" });
277
+ await settle(element);
278
+
279
+ findButton(element, "Save")?.click();
280
+ await settle(element);
281
+
282
+ const save = requests.find((request) => request.method === "PUT");
283
+ expect(save?.url).toContain("/api/collections/col_books");
284
+ // The stubbed editor round-trips to an empty document, so the edit is
285
+ // clearing the description — which only an explicit null can do.
286
+ expect(save?.body).toEqual({
287
+ slug: "books",
288
+ title: "Books",
289
+ sortOrder: "newest",
290
+ description: null,
291
+ });
292
+ });
293
+
294
+ it("keeps a failed save open, with the reason the server gave", async () => {
295
+ responses["/api/collections"] = {
296
+ ok: false,
297
+ body: { error: "That link is reserved." },
298
+ };
299
+ const element = mountDialog();
300
+ void element.open({});
301
+ await settle(element);
302
+ await typeTitle(element, "Reading");
303
+
304
+ findButton(element, "Save")?.click();
305
+ await settle(element);
306
+
307
+ expect(element.textContent).toContain("That link is reserved.");
308
+ expect(
309
+ element.querySelector<HTMLDialogElement>(".collection-dialog")?.open,
310
+ ).toBe(true);
311
+ });
312
+
313
+ it("closes on Escape without saving", async () => {
314
+ const element = mountDialog();
315
+ const result = element.open({});
316
+ await settle(element);
317
+ await typeTitle(element, "Reading");
318
+
319
+ element
320
+ .querySelector<HTMLInputElement>("[data-field='title']")
321
+ ?.dispatchEvent(
322
+ new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
323
+ );
324
+ await element.updateComplete;
325
+
326
+ await expect(result).resolves.toEqual({ changed: false });
327
+ expect(requests.some((request) => request.method === "POST")).toBe(false);
328
+ });
329
+ });