@jant/core 0.6.14 → 0.6.16

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 (350) hide show
  1. package/bin/commands/import-site.js +88 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-C75Zc8dD.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
  13. package/dist/client/.vite/manifest.json +8 -8
  14. package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
  16. package/dist/client/_assets/client-D3VZyG4L.css +2 -0
  17. package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
  18. package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
  19. package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
  20. package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
  21. package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
  22. package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
  23. package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
  24. package/dist/index.js +5 -5
  25. package/dist/node.js +7 -7
  26. package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
  27. package/package.json +11 -10
  28. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  29. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  30. package/src/__tests__/export-hugo-build.test.ts +63 -4
  31. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  32. package/src/__tests__/export-service.test.ts +142 -9
  33. package/src/__tests__/helpers/app.ts +3 -0
  34. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  35. package/src/__tests__/import-site-command.test.ts +65 -0
  36. package/src/__tests__/mise-config.test.ts +22 -0
  37. package/src/__tests__/vite-config.test.ts +45 -0
  38. package/src/app.tsx +16 -3
  39. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  40. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  41. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  42. package/src/client/__tests__/compose-bridge.test.ts +316 -2
  43. package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
  44. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  45. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  46. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  47. package/src/client/__tests__/toast.test.ts +82 -1
  48. package/src/client/collection-created-notice.ts +66 -0
  49. package/src/client/collection-form-bridge.ts +17 -0
  50. package/src/client/collection-navigation.ts +44 -0
  51. package/src/client/collection-page-actions.ts +57 -1
  52. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  53. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  54. package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
  55. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
  56. package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
  57. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
  58. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  59. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  60. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  61. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  62. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  63. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  64. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  65. package/src/client/components/collection-manager-types.ts +9 -3
  66. package/src/client/components/compose-format-convert.ts +5 -1
  67. package/src/client/components/compose-types.ts +55 -3
  68. package/src/client/components/jant-collection-directory.ts +165 -30
  69. package/src/client/components/jant-command-palette.ts +14 -7
  70. package/src/client/components/jant-compose-dialog.ts +1542 -694
  71. package/src/client/components/jant-compose-editor.ts +237 -99
  72. package/src/client/components/jant-compose-fullscreen.ts +27 -9
  73. package/src/client/components/jant-config-editor.ts +722 -0
  74. package/src/client/components/jant-media-lightbox.ts +100 -18
  75. package/src/client/components/jant-nav-manager.ts +1283 -54
  76. package/src/client/components/jant-post-form.ts +3 -3
  77. package/src/client/components/jant-post-menu.ts +177 -95
  78. package/src/client/components/nav-manager-types.ts +51 -2
  79. package/src/client/components/post-form-types.ts +1 -1
  80. package/src/client/compose-bridge.ts +147 -123
  81. package/src/client/compose-launch.ts +13 -0
  82. package/src/client/compose-shortcuts.ts +5 -1
  83. package/src/client/feed-video-player.ts +62 -2
  84. package/src/client/footnote-rail.ts +478 -0
  85. package/src/client/hydrate-partial.ts +2 -0
  86. package/src/client/media-lightbox-events.ts +63 -0
  87. package/src/client/post-refresh.ts +135 -0
  88. package/src/client/site-header-fragment.ts +38 -0
  89. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  90. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  91. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  92. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  93. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  94. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  95. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  96. package/src/client/tiptap/bubble-menu.ts +88 -10
  97. package/src/client/tiptap/continuous-lists.ts +102 -0
  98. package/src/client/tiptap/create-editor.ts +3 -0
  99. package/src/client/tiptap/extensions.ts +14 -5
  100. package/src/client/tiptap/link-input-rules.ts +4 -4
  101. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  102. package/src/client/tiptap/slash-commands.ts +8 -1
  103. package/src/client/tiptap/structural-keymap.ts +211 -0
  104. package/src/client/tiptap/tab-indent.ts +8 -22
  105. package/src/client/tiptap/table-control-labels.ts +30 -0
  106. package/src/client/tiptap/table-controls.ts +537 -0
  107. package/src/client/tiptap/table-size-picker.ts +254 -0
  108. package/src/client/toast.ts +133 -39
  109. package/src/client-auth.ts +1 -0
  110. package/src/client.ts +1 -0
  111. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  112. package/src/db/__tests__/d1-query.test.ts +43 -1
  113. package/src/db/__tests__/migration-runner.test.ts +63 -0
  114. package/src/db/__tests__/migrations.test.ts +469 -0
  115. package/src/db/__tests__/thread-activity.test.ts +113 -0
  116. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  117. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  118. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  119. package/src/db/migrations/0028_superb_post.sql +2 -0
  120. package/src/db/migrations/0029_boring_magma.sql +1 -0
  121. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  122. package/src/db/migrations/0031_many_ego.sql +3 -0
  123. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  124. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  125. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  126. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  127. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  128. package/src/db/migrations/meta/_journal.json +35 -0
  129. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  130. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  131. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  132. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  133. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  134. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  135. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  136. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  137. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  138. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  139. package/src/db/migrations/pg/meta/_journal.json +35 -0
  140. package/src/db/pg/schema.ts +46 -20
  141. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  142. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  143. package/src/db/schema.ts +48 -20
  144. package/src/db/thread-activity.ts +101 -0
  145. package/src/i18n/__tests__/detect.test.ts +8 -2
  146. package/src/i18n/detect.ts +14 -5
  147. package/src/i18n/locales/public/en.po +222 -51
  148. package/src/i18n/locales/public/en.ts +1 -1
  149. package/src/i18n/locales/public/zh-Hans.po +216 -45
  150. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  151. package/src/i18n/locales/public/zh-Hant.po +216 -45
  152. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  153. package/src/i18n/locales/settings/en.po +513 -10
  154. package/src/i18n/locales/settings/en.ts +1 -1
  155. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  156. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  157. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  158. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  159. package/src/lib/__tests__/api-settings.test.ts +299 -0
  160. package/src/lib/__tests__/constants.test.ts +5 -0
  161. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  162. package/src/lib/__tests__/feed.test.ts +168 -0
  163. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  164. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  165. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  166. package/src/lib/__tests__/markdown.test.ts +24 -6
  167. package/src/lib/__tests__/navigation.test.ts +59 -2
  168. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  169. package/src/lib/__tests__/post-display.test.ts +64 -0
  170. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  171. package/src/lib/__tests__/slug.test.ts +18 -0
  172. package/src/lib/__tests__/summary.test.ts +82 -0
  173. package/src/lib/__tests__/timeline.test.ts +109 -70
  174. package/src/lib/__tests__/timezones.test.ts +31 -3
  175. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  176. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  177. package/src/lib/__tests__/translit.test.ts +87 -0
  178. package/src/lib/__tests__/url.test.ts +53 -0
  179. package/src/lib/__tests__/view.test.ts +53 -0
  180. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  181. package/src/lib/api-settings.ts +239 -12
  182. package/src/lib/collection-sort.ts +7 -5
  183. package/src/lib/constants.ts +2 -0
  184. package/src/lib/feed-path.ts +25 -0
  185. package/src/lib/feed-policy.ts +64 -0
  186. package/src/lib/feed.ts +113 -11
  187. package/src/lib/footnotes.ts +264 -0
  188. package/src/lib/github-sync-site-config.ts +2 -0
  189. package/src/lib/hugo-markdown.ts +15 -3
  190. package/src/lib/jant-branding.ts +1 -0
  191. package/src/lib/link-preview.ts +25 -0
  192. package/src/lib/markdown-manager.ts +193 -5
  193. package/src/lib/markdown.ts +10 -3
  194. package/src/lib/navigation.ts +16 -3
  195. package/src/lib/post-body-html.ts +131 -0
  196. package/src/lib/post-display.ts +47 -8
  197. package/src/lib/render.tsx +9 -2
  198. package/src/lib/resolve-config.ts +100 -16
  199. package/src/lib/schemas.ts +133 -1
  200. package/src/lib/settings-paths.ts +5 -0
  201. package/src/lib/site-skill.ts +78 -0
  202. package/src/lib/summary.ts +16 -5
  203. package/src/lib/timeline.ts +137 -131
  204. package/src/lib/timezones.ts +54 -14
  205. package/src/lib/tiptap-render.ts +359 -83
  206. package/src/lib/translit.ts +288 -0
  207. package/src/lib/url.ts +123 -12
  208. package/src/lib/view.ts +58 -20
  209. package/src/lib/worker-response-cache.ts +11 -18
  210. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  211. package/src/middleware/config.ts +2 -2
  212. package/src/middleware/public-content-access.ts +87 -0
  213. package/src/node/__tests__/cli-export.test.ts +14 -0
  214. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  215. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  216. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  217. package/src/node/index.ts +2 -1
  218. package/src/preset.css +16 -2
  219. package/src/routes/__tests__/compose.test.ts +51 -0
  220. package/src/routes/__tests__/site-skill.test.ts +112 -0
  221. package/src/routes/api/__tests__/collections.test.ts +91 -19
  222. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  223. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  224. package/src/routes/api/__tests__/palette.test.ts +44 -0
  225. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  226. package/src/routes/api/__tests__/settings.test.ts +217 -0
  227. package/src/routes/api/collections.ts +22 -26
  228. package/src/routes/api/export.ts +2 -0
  229. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  230. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  231. package/src/routes/api/internal/post-body-html.ts +43 -0
  232. package/src/routes/api/internal/sites.ts +24 -0
  233. package/src/routes/api/nav-items.ts +24 -1
  234. package/src/routes/api/posts.ts +14 -10
  235. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  236. package/src/routes/api/public/__tests__/posts.test.ts +246 -0
  237. package/src/routes/api/public/archive.ts +4 -0
  238. package/src/routes/api/public/posts.ts +48 -24
  239. package/src/routes/api/search.ts +3 -0
  240. package/src/routes/api/settings.ts +33 -0
  241. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  242. package/src/routes/compose.tsx +8 -3
  243. package/src/routes/dash/settings.tsx +45 -2
  244. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  245. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  246. package/src/routes/feed/feed.ts +23 -14
  247. package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
  248. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  249. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  250. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  251. package/src/routes/pages/__tests__/preview.test.ts +176 -0
  252. package/src/routes/pages/__tests__/search.test.ts +69 -0
  253. package/src/routes/pages/archive.tsx +90 -22
  254. package/src/routes/pages/collection.tsx +32 -19
  255. package/src/routes/pages/collections.tsx +6 -1
  256. package/src/routes/pages/page.tsx +86 -11
  257. package/src/routes/site-skill.ts +18 -0
  258. package/src/services/__tests__/auth.test.ts +7 -5
  259. package/src/services/__tests__/collection.test.ts +190 -44
  260. package/src/services/__tests__/navigation.test.ts +229 -21
  261. package/src/services/__tests__/post-timeline.test.ts +249 -39
  262. package/src/services/__tests__/post.test.ts +844 -5
  263. package/src/services/__tests__/search.test.ts +48 -1
  264. package/src/services/__tests__/settings.test.ts +8 -0
  265. package/src/services/__tests__/site-admin.test.ts +121 -1
  266. package/src/services/auth.ts +2 -2
  267. package/src/services/bootstrap.ts +3 -2
  268. package/src/services/collection.ts +224 -167
  269. package/src/services/export-theme/assets/client-site.css +1 -1
  270. package/src/services/export-theme/assets/client-site.js +46 -25
  271. package/src/services/export-theme/layouts/_default/list.html +18 -4
  272. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  273. package/src/services/export-theme/layouts/featured/list.html +5 -4
  274. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  275. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  276. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  277. package/src/services/export-theme/styles/main.css +30 -2
  278. package/src/services/export.ts +94 -85
  279. package/src/services/mcp.ts +37 -27
  280. package/src/services/media.ts +1 -1
  281. package/src/services/navigation.ts +185 -46
  282. package/src/services/path.ts +4 -1
  283. package/src/services/post.ts +1142 -342
  284. package/src/services/search.ts +17 -2
  285. package/src/services/settings.ts +2 -10
  286. package/src/services/site-admin.ts +63 -43
  287. package/src/style-cjk-jp.css +1 -1
  288. package/src/style-cjk-kr.css +1 -1
  289. package/src/style-cjk-tc.css +1 -1
  290. package/src/style-cjk.css +1 -1
  291. package/src/styles/components.css +540 -14
  292. package/src/styles/site-media.css +111 -19
  293. package/src/styles/tokens.css +76 -10
  294. package/src/styles/ui.css +2227 -1347
  295. package/src/types/bindings.ts +3 -0
  296. package/src/types/config.ts +243 -12
  297. package/src/types/constants.ts +22 -1
  298. package/src/types/entities.ts +14 -8
  299. package/src/types/operations.ts +18 -10
  300. package/src/types/props.ts +20 -2
  301. package/src/types/raw-assets.d.ts +5 -0
  302. package/src/types/views.ts +22 -4
  303. package/src/ui/__tests__/font-themes.test.ts +42 -30
  304. package/src/ui/compose/ComposeDialog.tsx +109 -19
  305. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  306. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  307. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  308. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  309. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  310. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  311. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  312. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  313. package/src/ui/feed/LinkCard.tsx +2 -12
  314. package/src/ui/feed/LinkPreview.tsx +2 -7
  315. package/src/ui/feed/NoteCard.tsx +10 -20
  316. package/src/ui/feed/PostStatusBadges.tsx +42 -3
  317. package/src/ui/feed/QuoteCard.tsx +2 -12
  318. package/src/ui/feed/ThreadPreview.tsx +26 -34
  319. package/src/ui/feed/TimelineFeed.tsx +2 -2
  320. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  321. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  322. package/src/ui/feed/thread-preview-state.ts +5 -7
  323. package/src/ui/font-themes.ts +74 -26
  324. package/src/ui/layouts/BaseLayout.tsx +22 -8
  325. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  326. package/src/ui/pages/ArchivePage.tsx +131 -24
  327. package/src/ui/pages/CollectionPage.tsx +101 -22
  328. package/src/ui/pages/CollectionsPage.tsx +3 -1
  329. package/src/ui/pages/PostPage.tsx +39 -12
  330. package/src/ui/pages/SearchPage.tsx +6 -14
  331. package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
  332. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  333. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  334. package/src/ui/shared/CollectionsManager.tsx +17 -7
  335. package/src/ui/shared/DraftPreviewBar.tsx +49 -0
  336. package/src/ui/shared/HomePageBranding.tsx +2 -2
  337. package/src/ui/shared/MediaGallery.tsx +1 -0
  338. package/src/ui/shared/PostFooter.tsx +38 -13
  339. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  340. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  341. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  342. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  343. package/src/ui/shared/collection-management-labels.ts +33 -2
  344. package/src/ui/shared/custom-icons.ts +5 -0
  345. package/src/ui/shared/navigation-labels.ts +2 -4
  346. package/src/ui/shared/post-article-attributes.ts +52 -0
  347. package/dist/app-DZDlmh7C.js +0 -6
  348. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  349. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  350. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -0,0 +1,478 @@
1
+ /**
2
+ * Progressive Tufte-style footnote rail for wide public post views.
3
+ *
4
+ * Published HTML always remains a natural trailing endnote list. This module
5
+ * only positions that existing list in the margin when the post's detail or
6
+ * timeline container is wide enough and every note can be laid out safely. It
7
+ * never clones or moves footnote content, so narrow, no-JS, print, reader, and
8
+ * failure states retain the canonical endnote document order.
9
+ */
10
+
11
+ const FOOTNOTE_BODY_SELECTOR = [
12
+ '[data-post-view] [data-page="post"] [data-post-body].post-detail-body',
13
+ "[data-timeline-item] article[data-post] > [data-post-body].prose",
14
+ ].join(", ");
15
+ const FOOTNOTE_CONTAINER_SELECTOR = "[data-post-view], [data-timeline-item]";
16
+ const RAIL_ENABLED_PROPERTY = "--jant-footnote-rail-enabled";
17
+ const DEFAULT_NOTE_GAP_PX = 12;
18
+ const MAX_EXTRA_RAIL_HEIGHT_PX = 160;
19
+ const MAX_EXTRA_RAIL_HEIGHT_RATIO = 0.5;
20
+ const LAYOUT_EPSILON_PX = 0.5;
21
+
22
+ interface FootnoteRailItem {
23
+ anchorTop: number;
24
+ blockSize: number;
25
+ }
26
+
27
+ interface FootnoteRailLayout {
28
+ offsets: number[];
29
+ blockSize: number;
30
+ }
31
+
32
+ interface FootnotePair {
33
+ reference: HTMLAnchorElement;
34
+ note: globalThis.HTMLLIElement;
35
+ }
36
+
37
+ interface FootnoteStructure {
38
+ section: HTMLElement;
39
+ list: globalThis.HTMLOListElement;
40
+ pairs: FootnotePair[];
41
+ }
42
+
43
+ interface RailController extends FootnoteStructure {
44
+ body: HTMLElement;
45
+ container: HTMLElement;
46
+ observer: globalThis.ResizeObserver;
47
+ abortController: AbortController;
48
+ rafId: number;
49
+ containerInlineSize: number | null;
50
+ correctedInitialHash: boolean;
51
+ }
52
+
53
+ const controllers = new Map<HTMLElement, RailController>();
54
+ let printActive = false;
55
+
56
+ function finiteOr(value: number, fallback: number): number {
57
+ return Number.isFinite(value) ? value : fallback;
58
+ }
59
+
60
+ /**
61
+ * Place notes at their references while greedily preventing collisions.
62
+ *
63
+ * @param items - Reference offsets and measured note block sizes
64
+ * @param gap - Minimum gap between adjacent notes in pixels
65
+ * @returns Per-note offsets and the total rail block size
66
+ */
67
+ function computeFootnoteRailLayout(
68
+ items: FootnoteRailItem[],
69
+ gap: number,
70
+ ): FootnoteRailLayout {
71
+ const safeGap = Math.max(0, finiteOr(gap, DEFAULT_NOTE_GAP_PX));
72
+ const offsets: number[] = [];
73
+ let previousBottom = 0;
74
+
75
+ for (const [index, item] of items.entries()) {
76
+ const anchorTop = Math.max(0, finiteOr(item.anchorTop, 0));
77
+ const blockSize = Math.max(0, finiteOr(item.blockSize, 0));
78
+ const offset =
79
+ index === 0 ? anchorTop : Math.max(anchorTop, previousBottom + safeGap);
80
+
81
+ offsets.push(offset);
82
+ previousBottom = offset + blockSize;
83
+ }
84
+
85
+ return { offsets, blockSize: previousBottom };
86
+ }
87
+
88
+ function decodeFragment(href: string): string | null {
89
+ if (!href.startsWith("#") || href.length === 1) return null;
90
+
91
+ try {
92
+ return decodeURIComponent(href.slice(1));
93
+ } catch {
94
+ return href.slice(1);
95
+ }
96
+ }
97
+
98
+ function hasRole(element: globalThis.Element, role: string): boolean {
99
+ return (element.getAttribute("role") ?? "").split(/\s+/).includes(role);
100
+ }
101
+
102
+ function findFootnoteStructure(body: HTMLElement): FootnoteStructure | null {
103
+ const sections = Array.from(body.children).filter(
104
+ (child): child is HTMLElement =>
105
+ child instanceof HTMLElement &&
106
+ child.classList.contains("footnote-endnotes") &&
107
+ hasRole(child, "doc-endnotes"),
108
+ );
109
+ if (sections.length !== 1) return null;
110
+
111
+ const section = sections[0];
112
+ if (!section) return null;
113
+
114
+ const lists = Array.from(section.children).filter(
115
+ (child): child is globalThis.HTMLOListElement =>
116
+ child instanceof globalThis.HTMLOListElement &&
117
+ child.classList.contains("footnote-list"),
118
+ );
119
+ if (lists.length !== 1) return null;
120
+
121
+ const list = lists[0];
122
+ if (!list) return null;
123
+
124
+ const notes = Array.from(list.children).filter(
125
+ (child): child is globalThis.HTMLLIElement =>
126
+ child instanceof globalThis.HTMLLIElement &&
127
+ child.classList.contains("footnote"),
128
+ );
129
+ if (notes.length === 0 || notes.length !== list.children.length) return null;
130
+
131
+ const notesById = new Map<string, globalThis.HTMLLIElement>();
132
+ for (const note of notes) {
133
+ if (!note.id || notesById.has(note.id)) return null;
134
+ if (document.getElementById(note.id) !== note) return null;
135
+ notesById.set(note.id, note);
136
+ }
137
+
138
+ const firstReferencesByTarget = new Map<string, HTMLAnchorElement>();
139
+ for (const reference of body.querySelectorAll<HTMLAnchorElement>(
140
+ 'a[role~="doc-noteref"][href^="#"]',
141
+ )) {
142
+ const targetId = decodeFragment(reference.getAttribute("href") ?? "");
143
+ if (
144
+ targetId &&
145
+ notesById.has(targetId) &&
146
+ !firstReferencesByTarget.has(targetId)
147
+ ) {
148
+ firstReferencesByTarget.set(targetId, reference);
149
+ }
150
+ }
151
+
152
+ const pairs: FootnotePair[] = [];
153
+ for (const note of notes) {
154
+ const reference = firstReferencesByTarget.get(note.id);
155
+ if (!reference) return null;
156
+ pairs.push({ reference, note });
157
+ }
158
+
159
+ return { section, list, pairs };
160
+ }
161
+
162
+ function getObservedInlineSize(entry: globalThis.ResizeObserverEntry): number {
163
+ const box = Array.isArray(entry.contentBoxSize)
164
+ ? entry.contentBoxSize[0]
165
+ : entry.contentBoxSize;
166
+ return finiteOr(box?.inlineSize ?? entry.contentRect.width, 0);
167
+ }
168
+
169
+ function formatPixels(value: number): string {
170
+ return `${Math.round(value * 1000) / 1000}px`;
171
+ }
172
+
173
+ function clearRuntimeLayout(controller: RailController): void {
174
+ controller.body.classList.remove(
175
+ "footnote-rail-measuring",
176
+ "footnote-rail-ready",
177
+ );
178
+ controller.body.style.removeProperty("min-block-size");
179
+ controller.list.style.removeProperty("--footnote-rail-block-size");
180
+
181
+ for (const { note } of controller.pairs) {
182
+ note.style.removeProperty("--footnote-rail-y");
183
+ note.style.removeProperty("--footnote-rail-number");
184
+ }
185
+ }
186
+
187
+ function setRuntimeNumbers(controller: RailController): void {
188
+ let number = controller.list.start;
189
+
190
+ for (const { note } of controller.pairs) {
191
+ if (note.hasAttribute("value")) number = note.value;
192
+ note.style.setProperty("--footnote-rail-number", `"${number}"`);
193
+ number += 1;
194
+ }
195
+ }
196
+
197
+ function getRailGap(list: globalThis.HTMLOListElement): number {
198
+ const gap = Number.parseFloat(getComputedStyle(list).rowGap);
199
+ return Number.isFinite(gap) ? gap : DEFAULT_NOTE_GAP_PX;
200
+ }
201
+
202
+ function railCanGrowTo(contentSize: number, railSize: number): boolean {
203
+ const extra = Math.max(0, railSize - contentSize);
204
+ const allowedExtra = Math.max(
205
+ MAX_EXTRA_RAIL_HEIGHT_PX,
206
+ contentSize * MAX_EXTRA_RAIL_HEIGHT_RATIO,
207
+ );
208
+ return extra <= allowedExtra + LAYOUT_EPSILON_PX;
209
+ }
210
+
211
+ function correctInitialHash(controller: RailController): void {
212
+ if (controller.correctedInitialHash) return;
213
+ controller.correctedInitialHash = true;
214
+
215
+ const targetId = decodeFragment(globalThis.location.hash);
216
+ if (!targetId) return;
217
+
218
+ const target = controller.pairs.find(
219
+ ({ note }) => note.id === targetId,
220
+ )?.note;
221
+ if (!target) return;
222
+
223
+ requestAnimationFrame(() => {
224
+ if (!controller.body.classList.contains("footnote-rail-ready")) return;
225
+ target.scrollIntoView({ behavior: "auto", block: "nearest" });
226
+ });
227
+ }
228
+
229
+ function isRailEnabled(body: HTMLElement): boolean {
230
+ return (
231
+ !printActive &&
232
+ getComputedStyle(body).getPropertyValue(RAIL_ENABLED_PROPERTY).trim() ===
233
+ "1"
234
+ );
235
+ }
236
+
237
+ function layoutController(controller: RailController): void {
238
+ if (!controller.body.isConnected) {
239
+ destroyController(controller);
240
+ return;
241
+ }
242
+
243
+ if (!isRailEnabled(controller.body)) {
244
+ clearRuntimeLayout(controller);
245
+ return;
246
+ }
247
+
248
+ clearRuntimeLayout(controller);
249
+ controller.body.classList.add("footnote-rail-measuring");
250
+ setRuntimeNumbers(controller);
251
+
252
+ const bodyRect = controller.body.getBoundingClientRect();
253
+ const contentBlockSize = Math.max(0, bodyRect.height);
254
+ const items = controller.pairs.map(({ reference, note }) => ({
255
+ anchorTop: reference.getBoundingClientRect().top - bodyRect.top,
256
+ blockSize: note.getBoundingClientRect().height,
257
+ }));
258
+
259
+ if (items.some(({ blockSize }) => blockSize <= LAYOUT_EPSILON_PX)) {
260
+ clearRuntimeLayout(controller);
261
+ return;
262
+ }
263
+
264
+ const layout = computeFootnoteRailLayout(items, getRailGap(controller.list));
265
+ if (!railCanGrowTo(contentBlockSize, layout.blockSize)) {
266
+ clearRuntimeLayout(controller);
267
+ return;
268
+ }
269
+
270
+ for (const [index, { note }] of controller.pairs.entries()) {
271
+ note.style.setProperty(
272
+ "--footnote-rail-y",
273
+ formatPixels(layout.offsets[index] ?? 0),
274
+ );
275
+ }
276
+ controller.list.style.setProperty(
277
+ "--footnote-rail-block-size",
278
+ formatPixels(layout.blockSize),
279
+ );
280
+ controller.body.style.setProperty(
281
+ "min-block-size",
282
+ formatPixels(Math.max(contentBlockSize, layout.blockSize)),
283
+ );
284
+ controller.body.classList.remove("footnote-rail-measuring");
285
+ controller.body.classList.add("footnote-rail-ready");
286
+ correctInitialHash(controller);
287
+ }
288
+
289
+ function scheduleController(controller: RailController): void {
290
+ if (controller.rafId !== 0) return;
291
+
292
+ controller.rafId = requestAnimationFrame(() => {
293
+ controller.rafId = 0;
294
+ layoutController(controller);
295
+ });
296
+ }
297
+
298
+ function destroyController(controller: RailController): void {
299
+ if (controller.rafId !== 0) cancelAnimationFrame(controller.rafId);
300
+ controller.observer.disconnect();
301
+ controller.abortController.abort();
302
+ clearRuntimeLayout(controller);
303
+ controllers.delete(controller.body);
304
+ }
305
+
306
+ function structuresMatch(
307
+ controller: RailController,
308
+ structure: FootnoteStructure,
309
+ ): boolean {
310
+ return (
311
+ controller.section === structure.section &&
312
+ controller.list === structure.list &&
313
+ controller.pairs.length === structure.pairs.length &&
314
+ controller.pairs.every(
315
+ (pair, index) =>
316
+ pair.note === structure.pairs[index]?.note &&
317
+ pair.reference === structure.pairs[index]?.reference,
318
+ )
319
+ );
320
+ }
321
+
322
+ function createController(
323
+ body: HTMLElement,
324
+ structure: FootnoteStructure,
325
+ ): RailController | null {
326
+ if (!("ResizeObserver" in globalThis)) return null;
327
+
328
+ const container = body.closest<HTMLElement>(FOOTNOTE_CONTAINER_SELECTOR);
329
+ if (!container) return null;
330
+
331
+ const abortController = new AbortController();
332
+ const observer = new globalThis.ResizeObserver((entries) => {
333
+ const controller = controllers.get(body);
334
+ if (!controller) return;
335
+ let needsLayout = false;
336
+
337
+ for (const entry of entries) {
338
+ if (entry.target === container) {
339
+ const inlineSize = getObservedInlineSize(entry);
340
+ if (
341
+ controller.containerInlineSize === null ||
342
+ Math.abs(controller.containerInlineSize - inlineSize) >
343
+ LAYOUT_EPSILON_PX
344
+ ) {
345
+ controller.containerInlineSize = inlineSize;
346
+ needsLayout = true;
347
+ }
348
+ } else {
349
+ needsLayout = true;
350
+ }
351
+ }
352
+
353
+ if (needsLayout) scheduleController(controller);
354
+ });
355
+
356
+ const controller: RailController = {
357
+ body,
358
+ container,
359
+ ...structure,
360
+ observer,
361
+ abortController,
362
+ rafId: 0,
363
+ containerInlineSize: null,
364
+ correctedInitialHash: false,
365
+ };
366
+
367
+ observer.observe(container);
368
+ for (const child of body.children) {
369
+ if (child !== structure.section) observer.observe(child);
370
+ }
371
+
372
+ for (const image of body.querySelectorAll("img")) {
373
+ if (image.complete) continue;
374
+ image.addEventListener("load", () => scheduleController(controller), {
375
+ once: true,
376
+ signal: abortController.signal,
377
+ });
378
+ image.addEventListener("error", () => scheduleController(controller), {
379
+ once: true,
380
+ signal: abortController.signal,
381
+ });
382
+ }
383
+
384
+ controllers.set(body, controller);
385
+ scheduleController(controller);
386
+ return controller;
387
+ }
388
+
389
+ function collectFootnoteBodies(
390
+ root: globalThis.Document | globalThis.Element,
391
+ ): HTMLElement[] {
392
+ const bodies: HTMLElement[] = [];
393
+ if (
394
+ root instanceof globalThis.Element &&
395
+ root.matches(FOOTNOTE_BODY_SELECTOR)
396
+ ) {
397
+ bodies.push(root as HTMLElement);
398
+ }
399
+ bodies.push(...root.querySelectorAll<HTMLElement>(FOOTNOTE_BODY_SELECTOR));
400
+ return bodies;
401
+ }
402
+
403
+ /**
404
+ * Initialize or refresh progressive footnote rails inside a document fragment.
405
+ *
406
+ * @param root - Document or swapped fragment to scan
407
+ * @returns Nothing
408
+ */
409
+ export function initFootnoteRails(
410
+ root: globalThis.Document | globalThis.Element = document,
411
+ ): void {
412
+ for (const controller of controllers.values()) {
413
+ if (!controller.body.isConnected) destroyController(controller);
414
+ }
415
+
416
+ for (const body of collectFootnoteBodies(root)) {
417
+ const structure = findFootnoteStructure(body);
418
+ const existing = controllers.get(body);
419
+
420
+ if (!structure) {
421
+ if (existing) destroyController(existing);
422
+ continue;
423
+ }
424
+
425
+ if (existing && structuresMatch(existing, structure)) {
426
+ scheduleController(existing);
427
+ continue;
428
+ }
429
+
430
+ if (existing) destroyController(existing);
431
+ createController(body, structure);
432
+ }
433
+ }
434
+
435
+ function scheduleAllControllers(): void {
436
+ for (const controller of controllers.values()) scheduleController(controller);
437
+ }
438
+
439
+ globalThis.addEventListener("beforeprint", () => {
440
+ printActive = true;
441
+ for (const controller of controllers.values()) clearRuntimeLayout(controller);
442
+ });
443
+
444
+ globalThis.addEventListener("afterprint", () => {
445
+ printActive = false;
446
+ scheduleAllControllers();
447
+ });
448
+
449
+ if (document.fonts) {
450
+ void document.fonts.ready.then(scheduleAllControllers);
451
+ document.fonts.addEventListener("loadingdone", scheduleAllControllers);
452
+ }
453
+
454
+ if (document.readyState === "loading") {
455
+ document.addEventListener(
456
+ "DOMContentLoaded",
457
+ () => initFootnoteRails(document),
458
+ { once: true },
459
+ );
460
+ } else {
461
+ initFootnoteRails(document);
462
+ }
463
+
464
+ export const __testOnly = {
465
+ computeFootnoteRailLayout,
466
+ decodeFragment,
467
+ findFootnoteStructure,
468
+ railCanGrowTo,
469
+ resetControllers(): void {
470
+ for (const controller of [...controllers.values()]) {
471
+ destroyController(controller);
472
+ }
473
+ printActive = false;
474
+ },
475
+ getControllerCount(): number {
476
+ return controllers.size;
477
+ },
478
+ };
@@ -15,6 +15,7 @@
15
15
  import { setupThreadContexts } from "./thread-context.js";
16
16
  import { initFeedVideoPlayer } from "./feed-video-player.js";
17
17
  import { initPrecomputedWaveforms } from "./audio-player.js";
18
+ import { initFootnoteRails } from "./footnote-rail.js";
18
19
 
19
20
  export function hydratePartial(
20
21
  root: globalThis.Document | globalThis.Element,
@@ -22,4 +23,5 @@ export function hydratePartial(
22
23
  setupThreadContexts(root);
23
24
  initFeedVideoPlayer(root);
24
25
  initPrecomputedWaveforms(root);
26
+ initFootnoteRails(root);
25
27
  }
@@ -1 +1,64 @@
1
1
  export const MEDIA_LIGHTBOX_TOGGLE_EVENT = "jant:media-lightbox-toggle";
2
+
3
+ export const MEDIA_VIDEO_PLAYBACK_INTENT_EVENT =
4
+ "jant:media-video-playback-intent";
5
+
6
+ export interface MediaVideoPlaybackIntentDetail {
7
+ mediaId: string;
8
+ paused: boolean;
9
+ }
10
+
11
+ const manuallyPausedMediaIds = new Set<string>();
12
+
13
+ /**
14
+ * Check whether the user explicitly paused a media item on this page.
15
+ *
16
+ * @param mediaId - Stable media ID to inspect
17
+ * @returns `true` when the media should remain paused
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * isMediaVideoPlaybackPaused("med_123");
22
+ * ```
23
+ */
24
+ export function isMediaVideoPlaybackPaused(
25
+ mediaId: string | undefined,
26
+ ): boolean {
27
+ const normalizedMediaId = mediaId?.trim();
28
+ return !!normalizedMediaId && manuallyPausedMediaIds.has(normalizedMediaId);
29
+ }
30
+
31
+ /**
32
+ * Store and publish the user's explicit playback intent for a media item.
33
+ *
34
+ * @param mediaId - Stable media ID to update
35
+ * @param paused - Whether the media should remain paused
36
+ * @returns Nothing
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * setMediaVideoPlaybackPaused("med_123", true);
41
+ * ```
42
+ */
43
+ export function setMediaVideoPlaybackPaused(
44
+ mediaId: string | undefined,
45
+ paused: boolean,
46
+ ): void {
47
+ const normalizedMediaId = mediaId?.trim();
48
+ if (!normalizedMediaId) return;
49
+
50
+ if (paused) {
51
+ manuallyPausedMediaIds.add(normalizedMediaId);
52
+ } else {
53
+ manuallyPausedMediaIds.delete(normalizedMediaId);
54
+ }
55
+
56
+ document.dispatchEvent(
57
+ new CustomEvent<MediaVideoPlaybackIntentDetail>(
58
+ MEDIA_VIDEO_PLAYBACK_INTENT_EVENT,
59
+ {
60
+ detail: { mediaId: normalizedMediaId, paused },
61
+ },
62
+ ),
63
+ );
64
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * In-place refresh of a rendered Post.
3
+ *
4
+ * Reply submission, draft publishing, and anything else that changes a Post
5
+ * server-side needs the same thing afterwards: re-render exactly the surface
6
+ * the Post is shown on — a thread page, a feed item, or a standalone card —
7
+ * without a full page load.
8
+ */
9
+
10
+ import { hydratePartial } from "./hydrate-partial.js";
11
+ import { getReplyRefreshTarget } from "./compose-launch.js";
12
+
13
+ async function fetchPartialHtml(path: string): Promise<string | null> {
14
+ const res = await fetch(path, {
15
+ headers: { Accept: "text/html" },
16
+ });
17
+ if (!res.ok) return null;
18
+ return res.text();
19
+ }
20
+
21
+ export async function refreshTimelineThreadView(
22
+ threadRootId: string,
23
+ ): Promise<boolean> {
24
+ try {
25
+ const timelineItem = document.querySelector<HTMLElement>(
26
+ `[data-timeline-item][data-thread-root-id="${threadRootId}"]`,
27
+ );
28
+ const content = timelineItem?.querySelector<HTMLElement>(
29
+ "[data-timeline-item-content]",
30
+ );
31
+ if (!content) return false;
32
+
33
+ const html = await fetchPartialHtml(
34
+ `/_/timeline-item/${encodeURIComponent(threadRootId)}`,
35
+ );
36
+ if (!html) return false;
37
+
38
+ content.innerHTML = html;
39
+ // Swapped-in markup carries interactions whose per-element setup only runs
40
+ // on DOMContentLoaded (thread "Show more" toggle, feed video autoplay, audio
41
+ // waveform); re-initialize them or they stay inert until a full reload.
42
+ hydratePartial(content);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ export async function refreshPostCardView(postId: string): Promise<boolean> {
50
+ try {
51
+ const timelineItem = document
52
+ .querySelector<HTMLElement>(`article[data-post-id="${postId}"]`)
53
+ ?.closest<HTMLElement>("[data-timeline-item]");
54
+ const html = await fetchPartialHtml(
55
+ `/_/post-card/${encodeURIComponent(postId)}`,
56
+ );
57
+ if (!html) return false;
58
+
59
+ if (timelineItem) {
60
+ const content = timelineItem.querySelector<HTMLElement>(
61
+ "[data-timeline-item-content]",
62
+ );
63
+ if (!content) return false;
64
+ content.innerHTML = html;
65
+ hydratePartial(content);
66
+ return true;
67
+ }
68
+
69
+ const article = document.querySelector<HTMLElement>(
70
+ `article[data-post-id="${postId}"]`,
71
+ );
72
+ if (!article) return false;
73
+
74
+ article.outerHTML = html;
75
+ // outerHTML detaches `article`; re-query the replacement to hydrate it.
76
+ const nextArticle = document.querySelector<HTMLElement>(
77
+ `article[data-post-id="${postId}"]`,
78
+ );
79
+ if (nextArticle) hydratePartial(nextArticle);
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ export async function refreshPostPageView(postId: string): Promise<boolean> {
87
+ try {
88
+ const container = document.querySelector<HTMLElement>(
89
+ `[data-post-view][data-post-view-id="${postId}"]`,
90
+ );
91
+ if (!container) return false;
92
+
93
+ const html = await fetchPartialHtml(
94
+ `/_/post-view/${encodeURIComponent(postId)}`,
95
+ );
96
+ if (!html) return false;
97
+
98
+ container.outerHTML = html;
99
+ // outerHTML detaches `container`; re-query the replacement to hydrate it
100
+ // (see refreshTimelineThreadView).
101
+ const next = document.querySelector<HTMLElement>(
102
+ `[data-post-view][data-post-view-id="${postId}"]`,
103
+ );
104
+ if (next) hydratePartial(next);
105
+ return true;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Re-renders whichever surface an article belongs to, reusing the same
113
+ * targeting the reply flow uses.
114
+ *
115
+ * @param article - The rendered `article[data-post]` element to refresh
116
+ * @returns Whether a surface was found and re-rendered
117
+ * @example
118
+ * ```ts
119
+ * if (!(await refreshArticleView(article))) globalThis.location.reload();
120
+ * ```
121
+ */
122
+ export async function refreshArticleView(
123
+ article: HTMLElement,
124
+ ): Promise<boolean> {
125
+ const target = getReplyRefreshTarget(article);
126
+ if (!target) return false;
127
+
128
+ if (target.kind === "timeline-item") {
129
+ return refreshTimelineThreadView(target.id);
130
+ }
131
+ if (target.kind === "post-view") {
132
+ return refreshPostPageView(target.id);
133
+ }
134
+ return refreshPostCardView(target.id);
135
+ }