@jant/core 0.6.15 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -1,9 +1,18 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
- import { afterEach, describe, expect, it } from "vitest";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ const lazyEntries = vi.hoisted(() => ({
6
+ ensureCompose: vi.fn(async () => {}),
7
+ }));
8
+
9
+ vi.mock("../lazy-entries.js", () => lazyEntries);
10
+
4
11
  import {
12
+ ensureComposeDialog,
5
13
  getReplyRefreshTarget,
6
14
  getReplyTargetArticle,
15
+ readComposeDialogLabels,
7
16
  } from "../compose-launch.js";
8
17
 
9
18
  function getArticle(): HTMLElement {
@@ -167,3 +176,55 @@ describe("getReplyTargetArticle", () => {
167
176
  );
168
177
  });
169
178
  });
179
+
180
+ describe("ensureComposeDialog", () => {
181
+ afterEach(() => {
182
+ document.body.innerHTML = "";
183
+ lazyEntries.ensureCompose.mockClear();
184
+ });
185
+
186
+ it("loads the composer's bundle before handing the element back", async () => {
187
+ document.body.innerHTML = `<jant-compose-dialog></jant-compose-dialog>`;
188
+
189
+ const dialog = await ensureComposeDialog();
190
+
191
+ expect(lazyEntries.ensureCompose).toHaveBeenCalledTimes(1);
192
+ expect(dialog).toBe(document.querySelector("jant-compose-dialog"));
193
+ });
194
+
195
+ it("loads nothing on a page without a composer", async () => {
196
+ expect(await ensureComposeDialog()).toBeNull();
197
+ expect(lazyEntries.ensureCompose).not.toHaveBeenCalled();
198
+ });
199
+ });
200
+
201
+ describe("readComposeDialogLabels", () => {
202
+ afterEach(() => {
203
+ document.body.innerHTML = "";
204
+ });
205
+
206
+ it("reads the server-rendered attribute before the element upgrades", () => {
207
+ document.body.innerHTML = `<jant-compose-dialog labels='{"addCollection":"Add Collection"}'></jant-compose-dialog>`;
208
+
209
+ expect(readComposeDialogLabels()?.addCollection).toBe("Add Collection");
210
+ });
211
+
212
+ it("prefers the upgraded element's property", () => {
213
+ document.body.innerHTML = `<jant-compose-dialog labels='{"addCollection":"stale"}'></jant-compose-dialog>`;
214
+ const dialog = document.querySelector(
215
+ "jant-compose-dialog",
216
+ ) as HTMLElement & {
217
+ labels?: unknown;
218
+ };
219
+ dialog.labels = { addCollection: "Add Collection" };
220
+
221
+ expect(readComposeDialogLabels()?.addCollection).toBe("Add Collection");
222
+ });
223
+
224
+ it("returns null without a composer or with unreadable labels", () => {
225
+ expect(readComposeDialogLabels()).toBeNull();
226
+
227
+ document.body.innerHTML = `<jant-compose-dialog labels="{not json"></jant-compose-dialog>`;
228
+ expect(readComposeDialogLabels()).toBeNull();
229
+ });
230
+ });
@@ -1,6 +1,16 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ // The composer's bundle loads on first use; the harness element below stands
6
+ // in for the upgraded dialog, so loading is a no-op here.
7
+ vi.mock("../lazy-entries.js", () => ({
8
+ ensureCompose: async () => {},
9
+ ensureSettings: async () => {},
10
+ ensureManage: async () => {},
11
+ loadEntriesForPage: () => {},
12
+ }));
13
+
4
14
  import { __testOnly as composeDiscoveryTestOnly } from "../compose-discovery.js";
5
15
  import { __testOnly as composeShortcutsTestOnly } from "../compose-shortcuts.js";
6
16
 
@@ -8,6 +18,7 @@ type ComposeHarness = HTMLElement & {
8
18
  openNew: (options?: unknown) => Promise<void>;
9
19
  openReply: (...args: unknown[]) => Promise<void>;
10
20
  openEdit: (id: string) => Promise<void>;
21
+ openDraft: (id: string) => Promise<void>;
11
22
  };
12
23
 
13
24
  function dispatchShortcut(
@@ -23,6 +34,11 @@ function dispatchShortcut(
23
34
  return event;
24
35
  }
25
36
 
37
+ /** Let a shortcut's trip through the lazy loader settle. */
38
+ async function flush(): Promise<void> {
39
+ await new Promise((resolve) => setTimeout(resolve, 0));
40
+ }
41
+
26
42
  function createComposeHarness(): ComposeHarness {
27
43
  const composeEl = document.createElement(
28
44
  "jant-compose-dialog",
@@ -30,6 +46,7 @@ function createComposeHarness(): ComposeHarness {
30
46
  composeEl.openNew = vi.fn(async () => {});
31
47
  composeEl.openReply = vi.fn(async () => {});
32
48
  composeEl.openEdit = vi.fn(async () => {});
49
+ composeEl.openDraft = vi.fn(async () => {});
33
50
  document.body.appendChild(composeEl);
34
51
  return composeEl;
35
52
  }
@@ -106,7 +123,7 @@ describe("compose shortcuts", () => {
106
123
  vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
107
124
  });
108
125
 
109
- it("opens a collection-scoped composer on collection pages with n", () => {
126
+ it("opens a collection-scoped composer on collection pages with n", async () => {
110
127
  const composeEl = createComposeHarness();
111
128
 
112
129
  const collectionPage = document.createElement("div");
@@ -116,6 +133,8 @@ describe("compose shortcuts", () => {
116
133
 
117
134
  const event = dispatchShortcut(document, "n");
118
135
 
136
+ await flush();
137
+
119
138
  expect(event.defaultPrevented).toBe(true);
120
139
  expect(composeEl.openNew).toHaveBeenCalledWith({ collectionId: "col-2" });
121
140
  });
@@ -124,7 +143,8 @@ describe("compose shortcuts", () => {
124
143
  createComposeHarness();
125
144
 
126
145
  dispatchShortcut(document, "n");
127
- await Promise.resolve();
146
+
147
+ await flush();
128
148
 
129
149
  expect(globalThis.fetch).toHaveBeenCalledWith(
130
150
  composeDiscoveryTestOnly.COMPOSE_OPEN_SHORTCUT_DISCOVERY_API_PATH,
@@ -137,7 +157,7 @@ describe("compose shortcuts", () => {
137
157
  });
138
158
  });
139
159
 
140
- it("ignores n while focus is inside an input", () => {
160
+ it("ignores n while focus is inside an input", async () => {
141
161
  const composeEl = createComposeHarness();
142
162
 
143
163
  const input = document.createElement("textarea");
@@ -145,10 +165,12 @@ describe("compose shortcuts", () => {
145
165
 
146
166
  dispatchShortcut(input, "n");
147
167
 
168
+ await flush();
169
+
148
170
  expect(composeEl.openNew).not.toHaveBeenCalled();
149
171
  });
150
172
 
151
- it("opens a reply composer for the latest thread post on detail pages with r", () => {
173
+ it("opens a reply composer for the latest thread post on detail pages with r", async () => {
152
174
  const composeEl = createComposeHarness();
153
175
 
154
176
  const postView = document.createElement("div");
@@ -199,6 +221,8 @@ describe("compose shortcuts", () => {
199
221
 
200
222
  const event = dispatchShortcut(document, "r");
201
223
 
224
+ await flush();
225
+
202
226
  expect(event.defaultPrevented).toBe(true);
203
227
  expect(composeEl.openReply).toHaveBeenCalledTimes(1);
204
228
 
@@ -222,7 +246,7 @@ describe("compose shortcuts", () => {
222
246
  );
223
247
  });
224
248
 
225
- it("targets the latest thread post for reply shortcuts even when an older post is hovered", () => {
249
+ it("targets the latest thread post for reply shortcuts even when an older post is hovered", async () => {
226
250
  const composeEl = createComposeHarness();
227
251
  const { hoveredArticle } = renderThreadDetailPage();
228
252
  const originalQuerySelector = document.querySelector.bind(document);
@@ -238,6 +262,8 @@ describe("compose shortcuts", () => {
238
262
 
239
263
  const event = dispatchShortcut(document, "r");
240
264
 
265
+ await flush();
266
+
241
267
  expect(event.defaultPrevented).toBe(true);
242
268
  expect(composeEl.openReply).toHaveBeenCalledWith(
243
269
  "post-latest",
@@ -263,7 +289,8 @@ describe("compose shortcuts", () => {
263
289
  );
264
290
 
265
291
  const event = dispatchShortcut(document, "e");
266
- await Promise.resolve();
292
+
293
+ await flush();
267
294
 
268
295
  expect(event.defaultPrevented).toBe(true);
269
296
  expect(composeEl.openEdit).toHaveBeenCalledWith("post-hovered");
@@ -277,14 +304,34 @@ describe("compose shortcuts", () => {
277
304
  globalThis.history.pushState({}, "", "/about?edit=1");
278
305
 
279
306
  composeShortcutsTestOnly.openEditFromQueryParam();
280
- await Promise.resolve();
307
+
308
+ await flush();
281
309
 
282
310
  expect(composeEl.openEdit).toHaveBeenCalledWith("post-current");
283
311
  expect(window.location.pathname).toBe("/about");
284
312
  expect(window.location.search).toBe("");
285
313
  });
286
314
 
287
- it("keeps using the hovered post for reply shortcuts in the timeline", () => {
315
+ it("loads the current draft through the drafts workflow on preview pages", async () => {
316
+ const composeEl = createComposeHarness();
317
+ document.body.setAttribute("data-authenticated", "true");
318
+ const previewBar = document.createElement("aside");
319
+ previewBar.dataset.previewStatus = "";
320
+ document.body.appendChild(previewBar);
321
+ renderThreadDetailPage();
322
+ globalThis.history.pushState({}, "", "/preview/draft-post?edit=1");
323
+
324
+ composeShortcutsTestOnly.openEditFromQueryParam();
325
+
326
+ await flush();
327
+
328
+ expect(composeEl.openDraft).toHaveBeenCalledWith("post-current");
329
+ expect(composeEl.openEdit).not.toHaveBeenCalled();
330
+ expect(window.location.pathname).toBe("/preview/draft-post");
331
+ expect(window.location.search).toBe("");
332
+ });
333
+
334
+ it("keeps using the hovered post for reply shortcuts in the timeline", async () => {
288
335
  const composeEl = createComposeHarness();
289
336
  const article = document.createElement("article");
290
337
  article.dataset.post = "";
@@ -312,6 +359,8 @@ describe("compose shortcuts", () => {
312
359
 
313
360
  const event = dispatchShortcut(document, "r");
314
361
 
362
+ await flush();
363
+
315
364
  expect(event.defaultPrevented).toBe(true);
316
365
  expect(composeEl.openReply).toHaveBeenCalledTimes(1);
317
366
 
@@ -0,0 +1,90 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ const launch = vi.hoisted(() => ({
6
+ openNewCompose: vi.fn(async () => {}),
7
+ openReplyForArticle: vi.fn(async () => {}),
8
+ openEditForPost: vi.fn(async () => {}),
9
+ }));
10
+
11
+ vi.mock("../compose-launch.js", () => launch);
12
+
13
+ import "../compose-triggers.js";
14
+
15
+ function click(selector: string): void {
16
+ const el = document.querySelector<HTMLElement>(selector);
17
+ if (!el) throw new Error(`expected ${selector} in DOM`);
18
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
19
+ }
20
+
21
+ describe("compose triggers", () => {
22
+ beforeEach(() => {
23
+ document.body.innerHTML = "";
24
+ launch.openNewCompose.mockClear();
25
+ launch.openReplyForArticle.mockClear();
26
+ launch.openEditForPost.mockClear();
27
+ });
28
+
29
+ it("opens a new post from a [data-compose-open] button", () => {
30
+ document.body.innerHTML = `<button data-compose-open><span>New</span></button>`;
31
+
32
+ click("button span");
33
+
34
+ expect(launch.openNewCompose).toHaveBeenCalledWith(undefined);
35
+ });
36
+
37
+ it("scopes the new post to the button's collection", () => {
38
+ document.body.innerHTML = `<button data-compose-open data-compose-collection-id="col_1">New</button>`;
39
+
40
+ click("button");
41
+
42
+ expect(launch.openNewCompose).toHaveBeenCalledWith({
43
+ collectionId: "col_1",
44
+ });
45
+ });
46
+
47
+ it("replies to the post around a [data-reply-trigger]", () => {
48
+ document.body.innerHTML = `
49
+ <article data-post data-post-id="pst_1">
50
+ <button data-reply-trigger>Reply</button>
51
+ </article>`;
52
+
53
+ click("[data-reply-trigger]");
54
+
55
+ const article = document.querySelector("article");
56
+ expect(launch.openReplyForArticle).toHaveBeenCalledWith(article);
57
+ });
58
+
59
+ it("ignores a reply trigger outside any post", () => {
60
+ document.body.innerHTML = `<button data-reply-trigger>Reply</button>`;
61
+
62
+ click("[data-reply-trigger]");
63
+
64
+ expect(launch.openReplyForArticle).not.toHaveBeenCalled();
65
+ });
66
+
67
+ it("continues an inline draft from its badge, by its own or its post's id", () => {
68
+ document.body.innerHTML = `
69
+ <span data-draft-continue data-post-id="pst_draft">Draft</span>
70
+ <article data-post data-post-id="pst_2">
71
+ <span data-draft-continue>Draft</span>
72
+ </article>`;
73
+
74
+ click("[data-draft-continue][data-post-id]");
75
+ click("article [data-draft-continue]");
76
+
77
+ expect(launch.openEditForPost).toHaveBeenNthCalledWith(1, "pst_draft");
78
+ expect(launch.openEditForPost).toHaveBeenNthCalledWith(2, "pst_2");
79
+ });
80
+
81
+ it("leaves unrelated clicks alone", () => {
82
+ document.body.innerHTML = `<button>Nothing</button>`;
83
+
84
+ click("button");
85
+
86
+ expect(launch.openNewCompose).not.toHaveBeenCalled();
87
+ expect(launch.openReplyForArticle).not.toHaveBeenCalled();
88
+ expect(launch.openEditForPost).not.toHaveBeenCalled();
89
+ });
90
+ });
@@ -0,0 +1,162 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
4
+ import "../copy-field.js";
5
+
6
+ const FEED_URL = "https://example.com/feed";
7
+ const COPIED = "Feed URL copied.";
8
+ const FAILED = "Could not copy. Select the address and copy it.";
9
+
10
+ function requireElement<T>(element: T | null | undefined, message: string): T {
11
+ if (!element) throw new Error(message);
12
+ return element;
13
+ }
14
+
15
+ function renderField({ hiddenButton = false } = {}): {
16
+ input: HTMLInputElement;
17
+ button: HTMLButtonElement;
18
+ } {
19
+ document.body.innerHTML = `
20
+ <div id="toast-container"></div>
21
+ <div data-copy-field-root>
22
+ <div class="relative">
23
+ <input type="text" readonly value="${FEED_URL}" data-copy-field-value />
24
+ <button
25
+ type="button"
26
+ ${hiddenButton ? "hidden" : ""}
27
+ data-copy-field="${COPIED}"
28
+ data-copy-field-failed="${FAILED}"
29
+ >Copy</button>
30
+ </div>
31
+ </div>
32
+ `;
33
+
34
+ return {
35
+ input: requireElement(
36
+ document.querySelector<HTMLInputElement>("[data-copy-field-value]"),
37
+ "expected the address input",
38
+ ),
39
+ button: requireElement(
40
+ document.querySelector<HTMLButtonElement>("[data-copy-field]"),
41
+ "expected the copy button",
42
+ ),
43
+ };
44
+ }
45
+
46
+ function toastText(): string {
47
+ return document.getElementById("toast-container")?.textContent ?? "";
48
+ }
49
+
50
+ async function settle(): Promise<void> {
51
+ await Promise.resolve();
52
+ await Promise.resolve();
53
+ }
54
+
55
+ describe("copy field", () => {
56
+ afterEach(() => {
57
+ vi.restoreAllMocks();
58
+ });
59
+
60
+ describe("copying", () => {
61
+ beforeEach(() => {
62
+ document.body.innerHTML = "";
63
+ });
64
+
65
+ it("copies the address and reports it", async () => {
66
+ const writeText = vi.fn().mockResolvedValue(undefined);
67
+ Object.defineProperty(globalThis.navigator, "clipboard", {
68
+ value: { writeText },
69
+ configurable: true,
70
+ });
71
+
72
+ const { button } = renderField();
73
+ button.click();
74
+ await settle();
75
+
76
+ expect(writeText).toHaveBeenCalledWith(FEED_URL);
77
+ expect(toastText()).toContain(COPIED);
78
+ });
79
+
80
+ // The Clipboard API is absent on insecure origins and can be denied by
81
+ // permission. Failing loudly is what points the reader at selecting the
82
+ // address by hand instead.
83
+ it("reports a failure when the clipboard is unavailable", async () => {
84
+ Object.defineProperty(globalThis.navigator, "clipboard", {
85
+ value: undefined,
86
+ configurable: true,
87
+ });
88
+
89
+ const { button } = renderField();
90
+ button.click();
91
+ await settle();
92
+
93
+ expect(toastText()).toContain("Could not copy");
94
+ });
95
+
96
+ it("reports a failure when the clipboard refuses", async () => {
97
+ const writeText = vi.fn().mockRejectedValue(new Error("denied"));
98
+ Object.defineProperty(globalThis.navigator, "clipboard", {
99
+ value: { writeText },
100
+ configurable: true,
101
+ });
102
+
103
+ const { button } = renderField();
104
+ button.click();
105
+ await settle();
106
+
107
+ expect(toastText()).toContain("Could not copy");
108
+ });
109
+
110
+ it("finds fields added after load", async () => {
111
+ const writeText = vi.fn().mockResolvedValue(undefined);
112
+ Object.defineProperty(globalThis.navigator, "clipboard", {
113
+ value: { writeText },
114
+ configurable: true,
115
+ });
116
+
117
+ // Nothing re-runs against new markup; the listener is on the document.
118
+ const { button } = renderField();
119
+ button.click();
120
+ await settle();
121
+
122
+ expect(writeText).toHaveBeenCalledWith(FEED_URL);
123
+ });
124
+ });
125
+
126
+ describe("manual selection fallback", () => {
127
+ beforeEach(() => {
128
+ document.body.innerHTML = "";
129
+ });
130
+
131
+ it("selects the whole address on click", () => {
132
+ const { input } = renderField();
133
+ const select = vi.spyOn(input, "select");
134
+
135
+ input.dispatchEvent(new Event("click", { bubbles: true }));
136
+
137
+ expect(select).toHaveBeenCalledTimes(1);
138
+ });
139
+
140
+ it("selects the whole address on focus", () => {
141
+ const { input } = renderField();
142
+ const select = vi.spyOn(input, "select");
143
+
144
+ input.dispatchEvent(new Event("focus"));
145
+
146
+ expect(select).toHaveBeenCalledTimes(1);
147
+ });
148
+ });
149
+
150
+ // The server ships the button hidden so that without this script the field
151
+ // degrades to a selectable input rather than a button that does nothing.
152
+ describe("revealing the button", () => {
153
+ it("reveals a button the server rendered hidden", () => {
154
+ const { button } = renderField({ hiddenButton: true });
155
+ expect(button.hidden).toBe(true);
156
+
157
+ document.dispatchEvent(new Event("DOMContentLoaded"));
158
+
159
+ expect(button.hidden).toBe(false);
160
+ });
161
+ });
162
+ });
@@ -3,9 +3,47 @@
3
3
  import { beforeEach, describe, expect, it } from "vitest";
4
4
  import { initCustomUrlMenus } from "../custom-url-menu.js";
5
5
 
6
- function renderMenus() {
6
+ /**
7
+ * happy-dom has no popover API, so the top layer is stubbed: `showPopover`
8
+ * and `hidePopover` drive a flag that `:popover-open` then reports, the same
9
+ * contract the browser offers.
10
+ */
11
+ function stubTopLayer(menu: HTMLElement) {
12
+ let shown = false;
13
+ const nativeMatches = menu.matches.bind(menu);
14
+
15
+ Object.assign(menu, {
16
+ showPopover: () => {
17
+ shown = true;
18
+ },
19
+ hidePopover: () => {
20
+ shown = false;
21
+ },
22
+ matches: (selector: string) =>
23
+ selector === ":popover-open" ? shown : nativeMatches(selector),
24
+ });
25
+
26
+ return () => shown;
27
+ }
28
+
29
+ /** happy-dom lays nothing out, so anchoring math needs stated geometry. */
30
+ function stubRect(el: HTMLElement, rect: Partial<globalThis.DOMRect>) {
31
+ el.getBoundingClientRect = () =>
32
+ ({
33
+ top: 0,
34
+ bottom: 0,
35
+ left: 0,
36
+ right: 0,
37
+ width: 0,
38
+ height: 0,
39
+ ...rect,
40
+ }) as globalThis.DOMRect;
41
+ }
42
+
43
+ function renderMenus({ topLayer = false } = {}) {
7
44
  document.body.innerHTML = `
8
45
  <div data-custom-url-actions>
46
+ <code data-row-blank-space>/first</code>
9
47
  <button
10
48
  type="button"
11
49
  data-custom-url-action="toggle-menu"
@@ -13,7 +51,7 @@ function renderMenus() {
13
51
  >
14
52
  More actions
15
53
  </button>
16
- <div data-custom-url-menu role="menu" hidden>
54
+ <div data-custom-url-menu role="menu" popover="manual" hidden>
17
55
  <button type="button" role="menuitem" data-custom-url-action="delete">
18
56
  Delete
19
57
  </button>
@@ -27,7 +65,7 @@ function renderMenus() {
27
65
  >
28
66
  More actions
29
67
  </button>
30
- <div data-custom-url-menu role="menu" hidden>
68
+ <div data-custom-url-menu role="menu" popover="manual" hidden>
31
69
  <button type="button" role="menuitem" data-custom-url-action="delete">
32
70
  Delete
33
71
  </button>
@@ -35,17 +73,24 @@ function renderMenus() {
35
73
  </div>
36
74
  `;
37
75
 
76
+ const menus = document.querySelectorAll<HTMLElement>(
77
+ "[data-custom-url-menu]",
78
+ );
79
+ // Stubbed before init: the script reads popover support once, up front.
80
+ const isInTopLayer = topLayer ? stubTopLayer(menus[0]!) : () => false;
81
+
38
82
  initCustomUrlMenus();
39
83
 
40
84
  const triggers = document.querySelectorAll<HTMLButtonElement>(
41
85
  "[data-custom-url-action='toggle-menu']",
42
86
  );
43
- const menus = document.querySelectorAll<HTMLElement>(
44
- "[data-custom-url-menu]",
45
- );
46
87
  const menuItems = document.querySelectorAll<HTMLElement>("[role='menuitem']");
47
88
 
48
89
  return {
90
+ isInTopLayer,
91
+ firstRowBlankSpace: document.querySelector<HTMLElement>(
92
+ "[data-row-blank-space]",
93
+ ),
49
94
  firstTrigger: triggers[0],
50
95
  secondTrigger: triggers[1],
51
96
  firstMenu: menus[0],
@@ -72,6 +117,30 @@ describe("custom URL menus", () => {
72
117
  expect(firstTrigger?.getAttribute("aria-expanded")).toBe("false");
73
118
  });
74
119
 
120
+ it("closes when clicking blank space in its own row", () => {
121
+ const { firstTrigger, firstMenu, firstRowBlankSpace } = renderMenus();
122
+
123
+ firstTrigger?.click();
124
+ expect(firstMenu?.hidden).toBe(false);
125
+
126
+ firstRowBlankSpace?.dispatchEvent(
127
+ new MouseEvent("click", { bubbles: true }),
128
+ );
129
+
130
+ expect(firstMenu?.hidden).toBe(true);
131
+ expect(firstTrigger?.getAttribute("aria-expanded")).toBe("false");
132
+ });
133
+
134
+ it("stays open when clicking inside the menu", () => {
135
+ const { firstTrigger, firstMenu } = renderMenus();
136
+
137
+ firstTrigger?.click();
138
+ firstMenu?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
139
+
140
+ expect(firstMenu?.hidden).toBe(false);
141
+ expect(firstTrigger?.getAttribute("aria-expanded")).toBe("true");
142
+ });
143
+
75
144
  it("closes on Escape and returns focus to the trigger", () => {
76
145
  const { firstTrigger, firstMenu, firstDeleteButton } = renderMenus();
77
146
 
@@ -87,6 +156,58 @@ describe("custom URL menus", () => {
87
156
  expect(document.activeElement).toBe(firstTrigger);
88
157
  });
89
158
 
159
+ it("shows in the top layer and anchors itself to the trigger", () => {
160
+ const { firstTrigger, firstMenu, isInTopLayer } = renderMenus({
161
+ topLayer: true,
162
+ });
163
+
164
+ firstTrigger?.click();
165
+
166
+ expect(isInTopLayer()).toBe(true);
167
+ // The row list clips its children, so an open menu escapes to the top
168
+ // layer and is positioned against the viewport instead of the row.
169
+ expect(firstMenu?.style.top).not.toBe("");
170
+ expect(firstMenu?.style.left).not.toBe("");
171
+
172
+ document.body.dispatchEvent(new MouseEvent("click", { bubbles: true }));
173
+
174
+ expect(isInTopLayer()).toBe(false);
175
+ expect(firstMenu?.hidden).toBe(true);
176
+ });
177
+
178
+ // The viewport is 1024x768 under happy-dom.
179
+ it("anchors below the trigger when the viewport has room", () => {
180
+ const { firstTrigger, firstMenu } = renderMenus({ topLayer: true });
181
+ stubRect(firstTrigger!, { top: 100, bottom: 126, right: 500 });
182
+ stubRect(firstMenu!, { width: 140, height: 120 });
183
+
184
+ firstTrigger?.click();
185
+
186
+ expect(firstMenu?.style.top).toBe("132px");
187
+ expect(firstMenu?.style.left).toBe("360px");
188
+ });
189
+
190
+ it("flips above the trigger when the viewport bottom is too close", () => {
191
+ const { firstTrigger, firstMenu } = renderMenus({ topLayer: true });
192
+ stubRect(firstTrigger!, { top: 700, bottom: 726, right: 500 });
193
+ stubRect(firstMenu!, { width: 140, height: 120 });
194
+
195
+ firstTrigger?.click();
196
+
197
+ expect(firstMenu?.style.top).toBe("574px");
198
+ expect(firstMenu?.style.left).toBe("360px");
199
+ });
200
+
201
+ it("keeps the menu inside the left viewport edge", () => {
202
+ const { firstTrigger, firstMenu } = renderMenus({ topLayer: true });
203
+ stubRect(firstTrigger!, { top: 100, bottom: 126, right: 60 });
204
+ stubRect(firstMenu!, { width: 140, height: 120 });
205
+
206
+ firstTrigger?.click();
207
+
208
+ expect(firstMenu?.style.left).toBe("8px");
209
+ });
210
+
90
211
  it("closes the previous menu when another one opens", () => {
91
212
  const { firstTrigger, secondTrigger, firstMenu, secondMenu } =
92
213
  renderMenus();