@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
@@ -3,7 +3,6 @@
3
3
  import { afterEach, describe, expect, it } from "vitest";
4
4
  import { Editor } from "@tiptap/core";
5
5
  import { createMarkdownContentExtensions } from "../../../lib/markdown-manager.js";
6
- import { ExitableMarks } from "../exitable-marks.js";
7
6
  import { toggleMarkAndExit } from "../bubble-menu.js";
8
7
 
9
8
  const editors: Editor[] = [];
@@ -13,7 +12,7 @@ function createEditor(content: string): Editor {
13
12
  document.body.appendChild(element);
14
13
  const editor = new Editor({
15
14
  element,
16
- extensions: [...createMarkdownContentExtensions(), ExitableMarks],
15
+ extensions: createMarkdownContentExtensions(),
17
16
  content,
18
17
  });
19
18
  editor.view.dispatch(editor.state.tr);
@@ -150,6 +150,32 @@ describe("isCodeEditorHtml", () => {
150
150
  });
151
151
 
152
152
  describe("MarkdownClipboard", () => {
153
+ it("keeps pasted plain text that opens with a word and a period", () => {
154
+ const element = document.createElement("div");
155
+ document.body.appendChild(element);
156
+ const editor = createTiptapEditor({ element });
157
+ editors.push(editor);
158
+
159
+ dispatchMarkdownPaste(editor, "PS. 补充一句");
160
+
161
+ expect(editor.getJSON().content).toEqual([
162
+ { type: "paragraph", content: [{ type: "text", text: "PS. 补充一句" }] },
163
+ ]);
164
+ });
165
+
166
+ it("turns pasted plain numbered lines into an ordered list", () => {
167
+ const element = document.createElement("div");
168
+ document.body.appendChild(element);
169
+ const editor = createTiptapEditor({ element });
170
+ editors.push(editor);
171
+
172
+ dispatchMarkdownPaste(editor, "1. First\n2. Second");
173
+
174
+ const list = editor.getJSON().content?.[0];
175
+ expect(list?.type).toBe("orderedList");
176
+ expect(list?.content).toHaveLength(2);
177
+ });
178
+
153
179
  it("prefers an explicit text/markdown flavor over HTML and plain text", () => {
154
180
  const element = document.createElement("div");
155
181
  document.body.appendChild(element);
@@ -7,14 +7,23 @@
7
7
  */
8
8
 
9
9
  import { Extension, type Editor } from "@tiptap/core";
10
- import { Plugin, PluginKey, Selection, TextSelection } from "@tiptap/pm/state";
10
+ import {
11
+ AllSelection,
12
+ Plugin,
13
+ PluginKey,
14
+ Selection,
15
+ TextSelection,
16
+ } from "@tiptap/pm/state";
11
17
  import { liftTarget } from "@tiptap/pm/transform";
12
18
  import type { EditorView } from "@tiptap/pm/view";
13
19
  import { isLinkToolbarInputActive } from "./link-toolbar.js";
14
20
  import type { FormattingToolbarMode } from "./toolbar-mode.js";
15
21
  import {
16
22
  getFixedFloatingContainerRect,
23
+ getFloatingArrowOffset,
17
24
  getFloatingPosition,
25
+ getRangeAnchorRect,
26
+ getVisibleClipRect,
18
27
  } from "./floating-position.js";
19
28
 
20
29
  const bubbleMenuKey = new PluginKey("bubbleMenu");
@@ -315,20 +324,22 @@ export const BubbleMenu = Extension.create({
315
324
  if (!el) return;
316
325
  el.style.display = "flex";
317
326
 
318
- // Position above selection center
327
+ // Anchor on the first visible line of the selection, centered on its
328
+ // horizontal span. Select-all lands on the text you are looking at
329
+ // rather than at the top of a document scrolled out of view.
319
330
  const { from, to } = view.state.selection;
320
- const start = view.coordsAtPos(from);
321
- const end = view.coordsAtPos(to);
322
331
  const dialog = view.dom.closest("dialog");
332
+ const containerRect = getFixedFloatingContainerRect(dialog);
333
+ const anchorRect = getRangeAnchorRect(
334
+ view,
335
+ from,
336
+ to,
337
+ getVisibleClipRect(view.dom, containerRect),
338
+ );
323
339
  const rect = el.getBoundingClientRect();
324
340
  const layout = getFloatingPosition({
325
- anchorRect: {
326
- left: start.left,
327
- right: end.right,
328
- top: Math.min(start.top, end.top),
329
- bottom: Math.max(start.bottom, end.bottom),
330
- },
331
- containerRect: getFixedFloatingContainerRect(dialog),
341
+ anchorRect,
342
+ containerRect,
332
343
  floatingWidth: rect.width,
333
344
  floatingHeight: rect.height,
334
345
  preferredPlacement: "top",
@@ -338,6 +349,16 @@ export const BubbleMenu = Extension.create({
338
349
 
339
350
  el.style.left = `${layout.left}px`;
340
351
  el.style.top = `${layout.top}px`;
352
+ el.dataset.placement = layout.placement;
353
+ el.style.setProperty(
354
+ "--floating-arrow-x",
355
+ `${getFloatingArrowOffset({
356
+ anchorRect,
357
+ containerRect,
358
+ floatingLeft: layout.left,
359
+ floatingWidth: rect.width,
360
+ })}px`,
361
+ );
341
362
 
342
363
  syncActive();
343
364
  }
@@ -358,13 +379,23 @@ export const BubbleMenu = Extension.create({
358
379
  function shouldShow(view: EditorView): boolean {
359
380
  const { state } = view;
360
381
  const { selection } = state;
361
- // Only show for non-empty text selections (not node selections)
362
- if (!(selection instanceof TextSelection) || selection.empty)
363
- return false;
364
- if (!selection.$from.parent.isTextblock) return false;
382
+ if (selection.empty) return false;
365
383
  // Hide when link input popup is open
366
384
  if (isLinkToolbarInputActive()) return false;
367
- return true;
385
+
386
+ // Select-all reports an AllSelection, not a TextSelection. Clearing
387
+ // formatting off a pasted draft and quoting a whole note are exactly
388
+ // what a document-wide selection is for, so it gets the same toolbar as
389
+ // dragging over everything — as long as there is text to format.
390
+ if (selection instanceof AllSelection) {
391
+ return (
392
+ state.doc.textBetween(selection.from, selection.to, " ").trim() !== ""
393
+ );
394
+ }
395
+
396
+ // Otherwise only non-empty text selections (not node selections)
397
+ if (!(selection instanceof TextSelection)) return false;
398
+ return selection.$from.parent.isTextblock;
368
399
  }
369
400
 
370
401
  return [
@@ -12,7 +12,6 @@ import { PasteMedia } from "./paste-media.js";
12
12
  import type { PasteMediaOptions } from "./paste-media.js";
13
13
  import { BubbleMenu } from "./bubble-menu.js";
14
14
  import { LinkToolbar } from "./link-toolbar.js";
15
- import { ExitableMarks } from "./exitable-marks.js";
16
15
  import { CodeBlockIndent } from "./tab-indent.js";
17
16
  import { StructuralKeymap } from "./structural-keymap.js";
18
17
  import { ContinuousLists } from "./continuous-lists.js";
@@ -88,7 +87,6 @@ export function createSettingsEditorExtensions(
88
87
  ReclaimModEnter,
89
88
  LinkInputRules,
90
89
  MarkdownClipboard,
91
- ExitableMarks,
92
90
  InsertParagraphAround,
93
91
  StructuralKeymap,
94
92
  ContinuousLists,
@@ -133,7 +131,6 @@ export function createEditorExtensions(
133
131
  toolbarMode: options.toolbarMode ?? "default",
134
132
  }),
135
133
  LinkToolbar,
136
- ExitableMarks,
137
134
  InsertParagraphAround,
138
135
  StructuralKeymap,
139
136
  ContinuousLists,
@@ -1,11 +1,13 @@
1
- interface FloatingAnchorRect {
1
+ import type { EditorView } from "@tiptap/pm/view";
2
+
3
+ export interface FloatingAnchorRect {
2
4
  left: number;
3
5
  right: number;
4
6
  top: number;
5
7
  bottom: number;
6
8
  }
7
9
 
8
- interface FloatingContainerRect {
10
+ export interface FloatingContainerRect {
9
11
  left: number;
10
12
  top: number;
11
13
  width: number;
@@ -165,3 +167,215 @@ export function getFloatingPosition(
165
167
  maxHeight,
166
168
  };
167
169
  }
170
+
171
+ /** Overflow values that clip a descendant out of view. */
172
+ const CLIPPING_OVERFLOW = new Set([
173
+ "auto",
174
+ "scroll",
175
+ "hidden",
176
+ "clip",
177
+ "overlay",
178
+ ]);
179
+
180
+ function intersectRects(
181
+ a: FloatingContainerRect,
182
+ b: FloatingContainerRect,
183
+ ): FloatingContainerRect {
184
+ const left = Math.max(a.left, b.left);
185
+ const top = Math.max(a.top, b.top);
186
+ const right = Math.min(a.left + a.width, b.left + b.width);
187
+ const bottom = Math.min(a.top + a.height, b.top + b.height);
188
+ return { left, top, width: right - left, height: bottom - top };
189
+ }
190
+
191
+ /**
192
+ * Narrows a floating container down to the region where the editor's content
193
+ * is actually on screen, by intersecting it with every clipping ancestor.
194
+ *
195
+ * The composer's editor sits inside its own scroll region, so a selection can
196
+ * run well past what the reader can see. Anchoring to the raw container would
197
+ * point the surface at text hidden behind the scroll edge; this keeps the
198
+ * anchor inside the scrollport.
199
+ *
200
+ * @param element - Editor element whose visible region is wanted
201
+ * @param containerRect - Bounds of the fixed-position floating container
202
+ * @returns Visible region in viewport coordinates
203
+ *
204
+ * @example
205
+ * const clip = getVisibleClipRect(view.dom, getFixedFloatingContainerRect(dialog));
206
+ */
207
+ export function getVisibleClipRect(
208
+ element: HTMLElement,
209
+ containerRect: FloatingContainerRect,
210
+ ): FloatingContainerRect {
211
+ let clip = containerRect;
212
+ let node = element.parentElement;
213
+
214
+ while (node && node !== document.body) {
215
+ const style = getComputedStyle(node);
216
+ if (
217
+ CLIPPING_OVERFLOW.has(style.overflowY) ||
218
+ CLIPPING_OVERFLOW.has(style.overflowX)
219
+ ) {
220
+ const rect = node.getBoundingClientRect();
221
+ const next = intersectRects(clip, {
222
+ left: rect.left,
223
+ top: rect.top,
224
+ width: rect.width,
225
+ height: rect.height,
226
+ });
227
+ // A zero-size intersection means the ancestor reported no layout at all
228
+ // (detached nodes, test environments). Keep the wider region instead of
229
+ // collapsing the anchor to nothing.
230
+ if (next.width > 0 && next.height > 0) clip = next;
231
+ }
232
+ node = node.parentElement;
233
+ }
234
+
235
+ return clip;
236
+ }
237
+
238
+ /**
239
+ * Reduces the line boxes of a selection to the rectangle a floating surface
240
+ * should point at.
241
+ *
242
+ * Horizontally this is the union of everything visible, so a selection that
243
+ * covers whole blocks centers on the text column rather than on wherever the
244
+ * first and last lines happen to break. Vertically it is the *first* visible
245
+ * line only: a selection running past the bottom of the screen still gets an
246
+ * anchor the reader can see, and flipping the surface below that line lands it
247
+ * inside the selection instead of somewhere off-screen.
248
+ *
249
+ * @param rects - Client rects of the selection, in viewport coordinates
250
+ * @param clipRect - Region the anchor must stay inside
251
+ * @returns Anchor rectangle, or null when nothing is visible
252
+ *
253
+ * @example
254
+ * const anchor = getVisibleAnchorRect(range.getClientRects(), clipRect);
255
+ */
256
+ export function getVisibleAnchorRect(
257
+ rects: readonly FloatingAnchorRect[],
258
+ clipRect: FloatingContainerRect,
259
+ ): FloatingAnchorRect | null {
260
+ const clipRight = clipRect.left + clipRect.width;
261
+ const clipBottom = clipRect.top + clipRect.height;
262
+
263
+ let left = Number.POSITIVE_INFINITY;
264
+ let right = Number.NEGATIVE_INFINITY;
265
+ let top = Number.POSITIVE_INFINITY;
266
+ let bottom = Number.POSITIVE_INFINITY;
267
+ let found = false;
268
+
269
+ for (const rect of rects) {
270
+ const rectTop = Math.max(rect.top, clipRect.top);
271
+ const rectBottom = Math.min(rect.bottom, clipBottom);
272
+ if (rectBottom <= rectTop) continue;
273
+ const rectLeft = Math.max(rect.left, clipRect.left);
274
+ const rectRight = Math.min(rect.right, clipRight);
275
+ if (rectRight < rectLeft) continue;
276
+
277
+ left = Math.min(left, rectLeft);
278
+ right = Math.max(right, rectRight);
279
+ top = Math.min(top, rectTop);
280
+ // Every kept rect ends below `top`, so the smallest bottom belongs to the
281
+ // shortest box starting there — the first line, even when the browser also
282
+ // reports a full-height rect for an enclosing block.
283
+ bottom = Math.min(bottom, rectBottom);
284
+ found = true;
285
+ }
286
+
287
+ return found ? { left, right, top, bottom } : null;
288
+ }
289
+
290
+ function measureRangeRects(
291
+ view: EditorView,
292
+ from: number,
293
+ to: number,
294
+ ): FloatingAnchorRect[] {
295
+ if (typeof document === "undefined" || !document.createRange) return [];
296
+
297
+ try {
298
+ const start = view.domAtPos(from);
299
+ const end = view.domAtPos(to);
300
+ const range = document.createRange();
301
+ range.setStart(start.node, start.offset);
302
+ range.setEnd(end.node, end.offset);
303
+ const rects = range.getClientRects?.();
304
+ return rects ? Array.from(rects) : [];
305
+ } catch {
306
+ // Positions that do not resolve to a DOM range (node selections, widgets)
307
+ // fall back to the caller's endpoint coordinates.
308
+ return [];
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Resolves the rectangle a floating surface should point at for a document
314
+ * range.
315
+ *
316
+ * Measuring the caret at each end is only correct while the range stays on one
317
+ * line: as soon as it wraps, the "anchor" mixes the first line's left edge with
318
+ * the last line's right edge and centers on a point inside neither. This
319
+ * measures the range's real line boxes instead.
320
+ *
321
+ * @param view - Editor view owning the range
322
+ * @param from - Range start position
323
+ * @param to - Range end position
324
+ * @param clipRect - Region the anchor must stay inside
325
+ * @returns Anchor rectangle in viewport coordinates
326
+ *
327
+ * @example
328
+ * const anchor = getRangeAnchorRect(view, from, to, clipRect);
329
+ */
330
+ export function getRangeAnchorRect(
331
+ view: EditorView,
332
+ from: number,
333
+ to: number,
334
+ clipRect: FloatingContainerRect,
335
+ ): FloatingAnchorRect {
336
+ const measured = getVisibleAnchorRect(
337
+ measureRangeRects(view, from, to),
338
+ clipRect,
339
+ );
340
+ if (measured) return measured;
341
+
342
+ const start = view.coordsAtPos(from);
343
+ const end = view.coordsAtPos(to);
344
+ const endpoints: FloatingAnchorRect = {
345
+ left: Math.min(start.left, end.left),
346
+ right: Math.max(start.right, end.right),
347
+ top: start.top,
348
+ bottom: start.bottom,
349
+ };
350
+ return getVisibleAnchorRect([endpoints], clipRect) ?? endpoints;
351
+ }
352
+
353
+ /**
354
+ * Horizontal offset of a floating surface's pointer arrow, measured from the
355
+ * surface's own left edge.
356
+ *
357
+ * The surface is clamped to the container, so a centered arrow drifts off the
358
+ * anchor near the edges of the screen. This keeps it on the anchor without
359
+ * letting it slide past the rounded corners.
360
+ *
361
+ * @param options - Anchor, container, and the surface's placed geometry
362
+ * @returns Offset in pixels from the surface's left edge
363
+ *
364
+ * @example
365
+ * el.style.setProperty("--floating-arrow-x", `${getFloatingArrowOffset({...})}px`);
366
+ */
367
+ export function getFloatingArrowOffset(options: {
368
+ anchorRect: FloatingAnchorRect;
369
+ containerRect: FloatingContainerRect;
370
+ floatingLeft: number;
371
+ floatingWidth: number;
372
+ inset?: number;
373
+ }): number {
374
+ const inset = options.inset ?? 10;
375
+ const anchorCenter =
376
+ (options.anchorRect.left + options.anchorRect.right) / 2 -
377
+ options.containerRect.left;
378
+ const min = Math.min(inset, options.floatingWidth / 2);
379
+ const max = Math.max(options.floatingWidth - inset, min);
380
+ return clamp(anchorCenter - options.floatingLeft, min, max);
381
+ }
@@ -204,7 +204,9 @@ export async function rehostInlineImage(
204
204
  * Scans the editor's document for image srcs that match pending registry entries
205
205
  * (`blob:` placeholders from the insert flow, or remote/`data:` placeholders
206
206
  * from the paste-rehost flow). For each match, takes ownership from the original
207
- * editor and sets up replacement/removal watchers on the new editor.
207
+ * editor and sets up replacement/removal watchers on the new editor. Content can
208
+ * pass through several editors before it is submitted, so an upload adopted once
209
+ * can be adopted again.
208
210
  *
209
211
  * Call this immediately after `setContent` with JSON that may contain pending
210
212
  * placeholders (e.g. after fullscreen close transfers content back to compose).
@@ -223,12 +225,14 @@ export function adoptPendingInlineImageUploads(
223
225
  const src = node.attrs.src as string;
224
226
  if (typeof src !== "string") return;
225
227
 
226
- const uploaded = inflightUploads.get(src);
228
+ const uploaded = inflightUploads.get(src) ?? adoptedUploads.get(src);
227
229
  if (!uploaded) return;
228
230
 
229
231
  // Take ownership — prevents original editor's finally from revoking the URL.
230
232
  // Move to adoptedUploads so resolveInlineImageUrls can still find the promise
231
- // if the user submits before the upload completes.
233
+ // if the user submits before the upload completes. Content can be handed on
234
+ // more than once (fullscreen → compose → a format switch), so an entry
235
+ // already sitting in adoptedUploads is adopted again rather than skipped.
232
236
  inflightUploads.delete(src);
233
237
  adoptedUploads.set(src, uploaded);
234
238
 
@@ -15,7 +15,10 @@ import type { EditorState } from "@tiptap/pm/state";
15
15
  import type { EditorView } from "@tiptap/pm/view";
16
16
  import {
17
17
  getFixedFloatingContainerRect,
18
+ getFloatingArrowOffset,
18
19
  getFloatingPosition,
20
+ getRangeAnchorRect,
21
+ getVisibleClipRect,
19
22
  } from "./floating-position.js";
20
23
 
21
24
  const linkToolbarKey = new PluginKey("linkToolbar");
@@ -195,17 +198,17 @@ export const LinkToolbar = Extension.create({
195
198
  el.style.display = "flex";
196
199
 
197
200
  const dialog = view.dom.closest("dialog");
198
- const start = view.coordsAtPos(from);
199
- const end = view.coordsAtPos(to);
201
+ const containerRect = getFixedFloatingContainerRect(dialog);
202
+ const anchorRect = getRangeAnchorRect(
203
+ view,
204
+ from,
205
+ to,
206
+ getVisibleClipRect(view.dom, containerRect),
207
+ );
200
208
  const rect = el.getBoundingClientRect();
201
209
  const layout = getFloatingPosition({
202
- anchorRect: {
203
- left: start.left,
204
- right: end.right,
205
- top: Math.min(start.top, end.top),
206
- bottom: Math.max(start.bottom, end.bottom),
207
- },
208
- containerRect: getFixedFloatingContainerRect(dialog),
210
+ anchorRect,
211
+ containerRect,
209
212
  floatingWidth: rect.width,
210
213
  floatingHeight: rect.height,
211
214
  preferredPlacement: "top",
@@ -215,6 +218,16 @@ export const LinkToolbar = Extension.create({
215
218
 
216
219
  el.style.left = `${layout.left}px`;
217
220
  el.style.top = `${layout.top}px`;
221
+ el.dataset.placement = layout.placement;
222
+ el.style.setProperty(
223
+ "--floating-arrow-x",
224
+ `${getFloatingArrowOffset({
225
+ anchorRect,
226
+ containerRect,
227
+ floatingLeft: layout.left,
228
+ floatingWidth: rect.width,
229
+ })}px`,
230
+ );
218
231
  }
219
232
 
220
233
  interface ShowInputOptions {