@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
@@ -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
 
@@ -24,6 +34,11 @@ function dispatchShortcut(
24
34
  return event;
25
35
  }
26
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
+
27
42
  function createComposeHarness(): ComposeHarness {
28
43
  const composeEl = document.createElement(
29
44
  "jant-compose-dialog",
@@ -108,7 +123,7 @@ describe("compose shortcuts", () => {
108
123
  vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
109
124
  });
110
125
 
111
- it("opens a collection-scoped composer on collection pages with n", () => {
126
+ it("opens a collection-scoped composer on collection pages with n", async () => {
112
127
  const composeEl = createComposeHarness();
113
128
 
114
129
  const collectionPage = document.createElement("div");
@@ -118,6 +133,8 @@ describe("compose shortcuts", () => {
118
133
 
119
134
  const event = dispatchShortcut(document, "n");
120
135
 
136
+ await flush();
137
+
121
138
  expect(event.defaultPrevented).toBe(true);
122
139
  expect(composeEl.openNew).toHaveBeenCalledWith({ collectionId: "col-2" });
123
140
  });
@@ -126,7 +143,8 @@ describe("compose shortcuts", () => {
126
143
  createComposeHarness();
127
144
 
128
145
  dispatchShortcut(document, "n");
129
- await Promise.resolve();
146
+
147
+ await flush();
130
148
 
131
149
  expect(globalThis.fetch).toHaveBeenCalledWith(
132
150
  composeDiscoveryTestOnly.COMPOSE_OPEN_SHORTCUT_DISCOVERY_API_PATH,
@@ -139,7 +157,7 @@ describe("compose shortcuts", () => {
139
157
  });
140
158
  });
141
159
 
142
- it("ignores n while focus is inside an input", () => {
160
+ it("ignores n while focus is inside an input", async () => {
143
161
  const composeEl = createComposeHarness();
144
162
 
145
163
  const input = document.createElement("textarea");
@@ -147,10 +165,12 @@ describe("compose shortcuts", () => {
147
165
 
148
166
  dispatchShortcut(input, "n");
149
167
 
168
+ await flush();
169
+
150
170
  expect(composeEl.openNew).not.toHaveBeenCalled();
151
171
  });
152
172
 
153
- 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 () => {
154
174
  const composeEl = createComposeHarness();
155
175
 
156
176
  const postView = document.createElement("div");
@@ -201,6 +221,8 @@ describe("compose shortcuts", () => {
201
221
 
202
222
  const event = dispatchShortcut(document, "r");
203
223
 
224
+ await flush();
225
+
204
226
  expect(event.defaultPrevented).toBe(true);
205
227
  expect(composeEl.openReply).toHaveBeenCalledTimes(1);
206
228
 
@@ -224,7 +246,7 @@ describe("compose shortcuts", () => {
224
246
  );
225
247
  });
226
248
 
227
- 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 () => {
228
250
  const composeEl = createComposeHarness();
229
251
  const { hoveredArticle } = renderThreadDetailPage();
230
252
  const originalQuerySelector = document.querySelector.bind(document);
@@ -240,6 +262,8 @@ describe("compose shortcuts", () => {
240
262
 
241
263
  const event = dispatchShortcut(document, "r");
242
264
 
265
+ await flush();
266
+
243
267
  expect(event.defaultPrevented).toBe(true);
244
268
  expect(composeEl.openReply).toHaveBeenCalledWith(
245
269
  "post-latest",
@@ -265,7 +289,8 @@ describe("compose shortcuts", () => {
265
289
  );
266
290
 
267
291
  const event = dispatchShortcut(document, "e");
268
- await Promise.resolve();
292
+
293
+ await flush();
269
294
 
270
295
  expect(event.defaultPrevented).toBe(true);
271
296
  expect(composeEl.openEdit).toHaveBeenCalledWith("post-hovered");
@@ -279,7 +304,8 @@ describe("compose shortcuts", () => {
279
304
  globalThis.history.pushState({}, "", "/about?edit=1");
280
305
 
281
306
  composeShortcutsTestOnly.openEditFromQueryParam();
282
- await Promise.resolve();
307
+
308
+ await flush();
283
309
 
284
310
  expect(composeEl.openEdit).toHaveBeenCalledWith("post-current");
285
311
  expect(window.location.pathname).toBe("/about");
@@ -296,7 +322,8 @@ describe("compose shortcuts", () => {
296
322
  globalThis.history.pushState({}, "", "/preview/draft-post?edit=1");
297
323
 
298
324
  composeShortcutsTestOnly.openEditFromQueryParam();
299
- await Promise.resolve();
325
+
326
+ await flush();
300
327
 
301
328
  expect(composeEl.openDraft).toHaveBeenCalledWith("post-current");
302
329
  expect(composeEl.openEdit).not.toHaveBeenCalled();
@@ -304,7 +331,7 @@ describe("compose shortcuts", () => {
304
331
  expect(window.location.search).toBe("");
305
332
  });
306
333
 
307
- it("keeps using the hovered post for reply shortcuts in the timeline", () => {
334
+ it("keeps using the hovered post for reply shortcuts in the timeline", async () => {
308
335
  const composeEl = createComposeHarness();
309
336
  const article = document.createElement("article");
310
337
  article.dataset.post = "";
@@ -332,6 +359,8 @@ describe("compose shortcuts", () => {
332
359
 
333
360
  const event = dispatchShortcut(document, "r");
334
361
 
362
+ await flush();
363
+
335
364
  expect(event.defaultPrevented).toBe(true);
336
365
  expect(composeEl.openReply).toHaveBeenCalledTimes(1);
337
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();