@jant/core 0.6.16 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,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
+ });