@jant/core 0.6.14 → 0.6.15

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 (311) hide show
  1. package/bin/commands/import-site.js +81 -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-BxCOR3Uc.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
  18. package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
  47. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  48. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  49. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  50. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  51. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  52. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  53. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  54. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  55. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  56. package/src/client/components/collection-manager-types.ts +9 -3
  57. package/src/client/components/compose-types.ts +8 -0
  58. package/src/client/components/jant-collection-directory.ts +165 -30
  59. package/src/client/components/jant-command-palette.ts +2 -1
  60. package/src/client/components/jant-compose-dialog.ts +85 -21
  61. package/src/client/components/jant-compose-editor.ts +13 -7
  62. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  63. package/src/client/components/jant-config-editor.ts +722 -0
  64. package/src/client/components/jant-media-lightbox.ts +100 -18
  65. package/src/client/components/jant-nav-manager.ts +1283 -54
  66. package/src/client/components/jant-post-form.ts +3 -3
  67. package/src/client/components/jant-post-menu.ts +86 -74
  68. package/src/client/components/nav-manager-types.ts +51 -2
  69. package/src/client/components/post-form-types.ts +1 -1
  70. package/src/client/compose-bridge.ts +4 -1
  71. package/src/client/feed-video-player.ts +62 -2
  72. package/src/client/footnote-rail.ts +478 -0
  73. package/src/client/hydrate-partial.ts +2 -0
  74. package/src/client/media-lightbox-events.ts +63 -0
  75. package/src/client/site-header-fragment.ts +38 -0
  76. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  77. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  78. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  79. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  80. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  81. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  82. package/src/client/tiptap/bubble-menu.ts +88 -10
  83. package/src/client/tiptap/continuous-lists.ts +102 -0
  84. package/src/client/tiptap/create-editor.ts +3 -0
  85. package/src/client/tiptap/extensions.ts +14 -5
  86. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  87. package/src/client/tiptap/slash-commands.ts +8 -1
  88. package/src/client/tiptap/structural-keymap.ts +211 -0
  89. package/src/client/tiptap/tab-indent.ts +8 -22
  90. package/src/client/tiptap/table-control-labels.ts +30 -0
  91. package/src/client/tiptap/table-controls.ts +537 -0
  92. package/src/client/tiptap/table-size-picker.ts +254 -0
  93. package/src/client/toast.ts +8 -6
  94. package/src/client-auth.ts +1 -0
  95. package/src/client.ts +1 -0
  96. package/src/db/__tests__/d1-query.test.ts +43 -1
  97. package/src/db/__tests__/migration-runner.test.ts +63 -0
  98. package/src/db/__tests__/migrations.test.ts +469 -0
  99. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  100. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  101. package/src/db/migrations/0028_superb_post.sql +2 -0
  102. package/src/db/migrations/0029_boring_magma.sql +1 -0
  103. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  104. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  105. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  106. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  107. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  108. package/src/db/migrations/meta/_journal.json +28 -0
  109. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  110. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  111. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  112. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  113. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  114. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  115. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  116. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  117. package/src/db/migrations/pg/meta/_journal.json +28 -0
  118. package/src/db/pg/schema.ts +28 -20
  119. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  120. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  121. package/src/db/schema.ts +28 -20
  122. package/src/i18n/__tests__/detect.test.ts +8 -2
  123. package/src/i18n/detect.ts +14 -5
  124. package/src/i18n/locales/public/en.po +133 -30
  125. package/src/i18n/locales/public/en.ts +1 -1
  126. package/src/i18n/locales/public/zh-Hans.po +128 -25
  127. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  128. package/src/i18n/locales/public/zh-Hant.po +128 -25
  129. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  130. package/src/i18n/locales/settings/en.po +513 -10
  131. package/src/i18n/locales/settings/en.ts +1 -1
  132. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  133. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  134. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  135. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  136. package/src/lib/__tests__/api-settings.test.ts +299 -0
  137. package/src/lib/__tests__/constants.test.ts +5 -0
  138. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  139. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  140. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  141. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  142. package/src/lib/__tests__/markdown.test.ts +8 -6
  143. package/src/lib/__tests__/navigation.test.ts +59 -2
  144. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  145. package/src/lib/__tests__/post-display.test.ts +64 -0
  146. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  147. package/src/lib/__tests__/summary.test.ts +82 -0
  148. package/src/lib/__tests__/timeline.test.ts +109 -70
  149. package/src/lib/__tests__/timezones.test.ts +31 -3
  150. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  151. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  152. package/src/lib/__tests__/view.test.ts +53 -0
  153. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  154. package/src/lib/api-settings.ts +239 -12
  155. package/src/lib/collection-sort.ts +7 -5
  156. package/src/lib/constants.ts +2 -0
  157. package/src/lib/feed-path.ts +25 -0
  158. package/src/lib/feed-policy.ts +64 -0
  159. package/src/lib/footnotes.ts +264 -0
  160. package/src/lib/github-sync-site-config.ts +2 -0
  161. package/src/lib/hugo-markdown.ts +10 -3
  162. package/src/lib/jant-branding.ts +1 -0
  163. package/src/lib/markdown-manager.ts +188 -5
  164. package/src/lib/markdown.ts +10 -3
  165. package/src/lib/navigation.ts +16 -3
  166. package/src/lib/post-body-html.ts +131 -0
  167. package/src/lib/post-display.ts +7 -1
  168. package/src/lib/render.tsx +9 -2
  169. package/src/lib/resolve-config.ts +100 -16
  170. package/src/lib/schemas.ts +133 -1
  171. package/src/lib/settings-paths.ts +5 -0
  172. package/src/lib/site-skill.ts +78 -0
  173. package/src/lib/summary.ts +16 -5
  174. package/src/lib/timeline.ts +98 -122
  175. package/src/lib/timezones.ts +54 -14
  176. package/src/lib/tiptap-render.ts +355 -81
  177. package/src/lib/view.ts +30 -20
  178. package/src/lib/worker-response-cache.ts +11 -18
  179. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  180. package/src/middleware/config.ts +2 -2
  181. package/src/middleware/public-content-access.ts +87 -0
  182. package/src/node/__tests__/cli-export.test.ts +14 -0
  183. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  184. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  185. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  186. package/src/node/index.ts +2 -1
  187. package/src/preset.css +16 -2
  188. package/src/routes/__tests__/compose.test.ts +51 -0
  189. package/src/routes/__tests__/site-skill.test.ts +112 -0
  190. package/src/routes/api/__tests__/collections.test.ts +91 -19
  191. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  192. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  193. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  194. package/src/routes/api/__tests__/settings.test.ts +217 -0
  195. package/src/routes/api/collections.ts +22 -26
  196. package/src/routes/api/export.ts +2 -0
  197. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  198. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  199. package/src/routes/api/internal/post-body-html.ts +43 -0
  200. package/src/routes/api/internal/sites.ts +24 -0
  201. package/src/routes/api/nav-items.ts +24 -1
  202. package/src/routes/api/posts.ts +2 -2
  203. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  204. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  205. package/src/routes/api/public/archive.ts +4 -0
  206. package/src/routes/api/public/posts.ts +41 -24
  207. package/src/routes/api/search.ts +3 -0
  208. package/src/routes/api/settings.ts +33 -0
  209. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  210. package/src/routes/compose.tsx +2 -1
  211. package/src/routes/dash/settings.tsx +45 -2
  212. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  213. package/src/routes/feed/feed.ts +23 -14
  214. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  215. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  216. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  217. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  218. package/src/routes/pages/__tests__/search.test.ts +69 -0
  219. package/src/routes/pages/archive.tsx +27 -7
  220. package/src/routes/pages/collection.tsx +32 -19
  221. package/src/routes/pages/collections.tsx +6 -1
  222. package/src/routes/pages/page.tsx +83 -11
  223. package/src/routes/site-skill.ts +18 -0
  224. package/src/services/__tests__/auth.test.ts +7 -5
  225. package/src/services/__tests__/collection.test.ts +159 -44
  226. package/src/services/__tests__/navigation.test.ts +229 -21
  227. package/src/services/__tests__/post-timeline.test.ts +109 -30
  228. package/src/services/__tests__/post.test.ts +567 -5
  229. package/src/services/__tests__/search.test.ts +48 -1
  230. package/src/services/__tests__/settings.test.ts +8 -0
  231. package/src/services/__tests__/site-admin.test.ts +121 -1
  232. package/src/services/auth.ts +2 -2
  233. package/src/services/bootstrap.ts +3 -2
  234. package/src/services/collection.ts +226 -167
  235. package/src/services/export-theme/assets/client-site.css +1 -1
  236. package/src/services/export-theme/assets/client-site.js +46 -25
  237. package/src/services/export-theme/layouts/_default/list.html +18 -4
  238. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  239. package/src/services/export-theme/layouts/featured/list.html +5 -4
  240. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  241. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  242. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  243. package/src/services/export-theme/styles/main.css +30 -2
  244. package/src/services/export.ts +91 -80
  245. package/src/services/mcp.ts +37 -27
  246. package/src/services/media.ts +1 -1
  247. package/src/services/navigation.ts +185 -46
  248. package/src/services/post.ts +874 -271
  249. package/src/services/search.ts +8 -2
  250. package/src/services/settings.ts +2 -10
  251. package/src/services/site-admin.ts +63 -43
  252. package/src/style-cjk-jp.css +1 -1
  253. package/src/style-cjk-kr.css +1 -1
  254. package/src/style-cjk-tc.css +1 -1
  255. package/src/style-cjk.css +1 -1
  256. package/src/styles/components.css +540 -0
  257. package/src/styles/site-media.css +111 -19
  258. package/src/styles/tokens.css +14 -9
  259. package/src/styles/ui.css +829 -29
  260. package/src/types/bindings.ts +3 -0
  261. package/src/types/config.ts +243 -12
  262. package/src/types/constants.ts +22 -1
  263. package/src/types/entities.ts +9 -8
  264. package/src/types/operations.ts +18 -10
  265. package/src/types/props.ts +8 -2
  266. package/src/types/raw-assets.d.ts +5 -0
  267. package/src/types/views.ts +16 -4
  268. package/src/ui/__tests__/font-themes.test.ts +42 -30
  269. package/src/ui/compose/ComposeDialog.tsx +92 -2
  270. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  271. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  272. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  273. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  274. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  275. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  276. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  277. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  278. package/src/ui/feed/LinkCard.tsx +2 -12
  279. package/src/ui/feed/NoteCard.tsx +10 -20
  280. package/src/ui/feed/QuoteCard.tsx +2 -12
  281. package/src/ui/feed/ThreadPreview.tsx +26 -34
  282. package/src/ui/feed/TimelineFeed.tsx +2 -2
  283. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  284. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  285. package/src/ui/feed/thread-preview-state.ts +5 -7
  286. package/src/ui/font-themes.ts +74 -26
  287. package/src/ui/layouts/BaseLayout.tsx +22 -8
  288. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  289. package/src/ui/pages/CollectionPage.tsx +101 -22
  290. package/src/ui/pages/CollectionsPage.tsx +3 -1
  291. package/src/ui/pages/PostPage.tsx +39 -12
  292. package/src/ui/pages/SearchPage.tsx +6 -14
  293. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  294. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  295. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  296. package/src/ui/shared/CollectionsManager.tsx +17 -7
  297. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  298. package/src/ui/shared/HomePageBranding.tsx +2 -2
  299. package/src/ui/shared/MediaGallery.tsx +1 -0
  300. package/src/ui/shared/PostFooter.tsx +3 -1
  301. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  302. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  303. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  304. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  305. package/src/ui/shared/collection-management-labels.ts +33 -2
  306. package/src/ui/shared/navigation-labels.ts +2 -4
  307. package/src/ui/shared/post-article-attributes.ts +46 -0
  308. package/dist/app-DZDlmh7C.js +0 -6
  309. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  310. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  311. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -68,7 +68,8 @@ describe("getThreadPreviewState", () => {
68
68
 
69
69
  expect(
70
70
  getThreadPreviewState({
71
- secondReply: latestReply,
71
+ leadingReplies: [latestReply],
72
+ trailingReplies: [],
72
73
  latestReply,
73
74
  totalReplyCount: 1,
74
75
  }),
@@ -77,29 +78,39 @@ describe("getThreadPreviewState", () => {
77
78
  });
78
79
  });
79
80
 
80
- it("has no hidden posts for a 4-post thread when all four slots are visible", () => {
81
- const secondReply = createPostView({
81
+ it("has no hidden posts for a 6-post thread when all slots are visible", () => {
82
+ const firstReply = createPostView({
82
83
  id: "post-2",
83
84
  permalink: "/post-2",
84
85
  slug: "post-2",
85
86
  });
86
- const penultimateReply = createPostView({
87
+ const secondReply = createPostView({
87
88
  id: "post-3",
88
89
  permalink: "/post-3",
89
90
  slug: "post-3",
90
91
  });
91
- const latestReply = createPostView({
92
+ const antepenultimateReply = createPostView({
92
93
  id: "post-4",
93
94
  permalink: "/post-4",
94
95
  slug: "post-4",
95
96
  });
97
+ const penultimateReply = createPostView({
98
+ id: "post-5",
99
+ permalink: "/post-5",
100
+ slug: "post-5",
101
+ });
102
+ const latestReply = createPostView({
103
+ id: "post-6",
104
+ permalink: "/post-6",
105
+ slug: "post-6",
106
+ });
96
107
 
97
108
  expect(
98
109
  getThreadPreviewState({
99
- secondReply,
100
- penultimateReply,
110
+ leadingReplies: [firstReply, secondReply],
111
+ trailingReplies: [antepenultimateReply, penultimateReply],
101
112
  latestReply,
102
- totalReplyCount: 3,
113
+ totalReplyCount: 5,
103
114
  }),
104
115
  ).toEqual({
105
116
  hiddenCount: 0,
@@ -107,31 +118,41 @@ describe("getThreadPreviewState", () => {
107
118
  });
108
119
 
109
120
  it("counts hidden posts for longer threads after deduping visible slots", () => {
110
- const secondReply = createPostView({
121
+ const firstReply = createPostView({
111
122
  id: "post-2",
112
123
  permalink: "/post-2",
113
124
  slug: "post-2",
114
125
  });
126
+ const secondReply = createPostView({
127
+ id: "post-3",
128
+ permalink: "/post-3",
129
+ slug: "post-3",
130
+ });
131
+ const antepenultimateReply = createPostView({
132
+ id: "post-7",
133
+ permalink: "/post-7",
134
+ slug: "post-7",
135
+ });
115
136
  const penultimateReply = createPostView({
116
- id: "post-4",
117
- permalink: "/post-4",
118
- slug: "post-4",
137
+ id: "post-8",
138
+ permalink: "/post-8",
139
+ slug: "post-8",
119
140
  });
120
141
  const latestReply = createPostView({
121
- id: "post-5",
122
- permalink: "/post-5",
123
- slug: "post-5",
142
+ id: "post-9",
143
+ permalink: "/post-9",
144
+ slug: "post-9",
124
145
  });
125
146
 
126
147
  expect(
127
148
  getThreadPreviewState({
128
- secondReply,
129
- penultimateReply,
149
+ leadingReplies: [firstReply, secondReply],
150
+ trailingReplies: [antepenultimateReply, penultimateReply],
130
151
  latestReply,
131
- totalReplyCount: 4,
152
+ totalReplyCount: 8,
132
153
  }),
133
154
  ).toEqual({
134
- hiddenCount: 1,
155
+ hiddenCount: 3,
135
156
  });
136
157
  });
137
158
 
@@ -191,7 +212,8 @@ describe("getThreadPreviewState", () => {
191
212
  const html = renderWithI18n(() =>
192
213
  ThreadPreview({
193
214
  rootPost,
194
- secondReply,
215
+ leadingReplies: [secondReply],
216
+ trailingReplies: [],
195
217
  latestReply,
196
218
  totalReplyCount: 3,
197
219
  }),
@@ -214,18 +236,22 @@ describe("getThreadPreviewState", () => {
214
236
  summaryHtml: "<p>Root summary</p>",
215
237
  summaryHasMore: true,
216
238
  }),
217
- secondReply: createPostView({
218
- id: "post-2",
219
- permalink: "/post-2",
220
- slug: "post-2",
221
- bodyHtml: "<p>Second</p>",
222
- }),
223
- penultimateReply: createPostView({
224
- id: "post-4",
225
- permalink: "/post-4",
226
- slug: "post-4",
227
- bodyHtml: "<p>Penultimate</p>",
228
- }),
239
+ leadingReplies: [
240
+ createPostView({
241
+ id: "post-2",
242
+ permalink: "/post-2",
243
+ slug: "post-2",
244
+ bodyHtml: "<p>Second</p>",
245
+ }),
246
+ ],
247
+ trailingReplies: [
248
+ createPostView({
249
+ id: "post-4",
250
+ permalink: "/post-4",
251
+ slug: "post-4",
252
+ bodyHtml: "<p>Penultimate</p>",
253
+ }),
254
+ ],
229
255
  latestReply: createPostView({
230
256
  id: "post-5",
231
257
  permalink: "/post-5",
@@ -246,6 +272,37 @@ describe("getThreadPreviewState", () => {
246
272
  expect(html).toMatch(/data-label-less="[^"]+"/);
247
273
  });
248
274
 
275
+ it("renders at most six ordered posts with a gap between leading and trailing replies", () => {
276
+ const reply = (id: number) =>
277
+ createPostView({
278
+ id: `post-${id}`,
279
+ permalink: `/post-${id}`,
280
+ slug: `post-${id}`,
281
+ bodyHtml: `<p>Post ${id}</p>`,
282
+ });
283
+
284
+ const html = renderWithI18n(() =>
285
+ ThreadPreview({
286
+ rootPost: createPostView({ bodyHtml: "<p>Post 1</p>" }),
287
+ leadingReplies: [reply(2), reply(3)],
288
+ trailingReplies: [reply(6), reply(7)],
289
+ latestReply: reply(8),
290
+ totalReplyCount: 7,
291
+ }),
292
+ );
293
+
294
+ const visiblePosts = [1, 2, 3, 6, 7, 8];
295
+ for (const id of visiblePosts) {
296
+ expect(html).toContain(`<p>Post ${id}</p>`);
297
+ }
298
+ expect(html).toContain("2 more posts");
299
+ expect(visiblePosts.map((id) => html.indexOf(`<p>Post ${id}</p>`))).toEqual(
300
+ [...visiblePosts]
301
+ .map((id) => html.indexOf(`<p>Post ${id}</p>`))
302
+ .sort((left, right) => left - right),
303
+ );
304
+ });
305
+
249
306
  it("hides the show-more toggle on first paint for a short lone root that fits the cap", () => {
250
307
  // 2-post thread: the shell holds only the root. A short root genuinely
251
308
  // fits the height cap, so the toggle is rendered hidden to avoid flashing
@@ -257,6 +314,8 @@ describe("getThreadPreviewState", () => {
257
314
  bodyHtml: "<p>Root</p>",
258
315
  summary: "Root",
259
316
  }),
317
+ leadingReplies: [],
318
+ trailingReplies: [],
260
319
  latestReply: createPostView({
261
320
  id: "post-2",
262
321
  permalink: "/post-2",
@@ -285,6 +344,8 @@ describe("getThreadPreviewState", () => {
285
344
  bodyHtml: "<p>Root</p>",
286
345
  summary: "word ".repeat(60),
287
346
  }),
347
+ leadingReplies: [],
348
+ trailingReplies: [],
288
349
  latestReply: createPostView({
289
350
  id: "post-2",
290
351
  permalink: "/post-2",
@@ -308,12 +369,15 @@ describe("getThreadPreviewState", () => {
308
369
  const html = renderWithI18n(() =>
309
370
  ThreadPreview({
310
371
  rootPost: createPostView({ bodyHtml: "<p>Root</p>" }),
311
- secondReply: createPostView({
312
- id: "post-2",
313
- permalink: "/post-2",
314
- slug: "post-2",
315
- bodyHtml: "<p>Second</p>",
316
- }),
372
+ leadingReplies: [
373
+ createPostView({
374
+ id: "post-2",
375
+ permalink: "/post-2",
376
+ slug: "post-2",
377
+ bodyHtml: "<p>Second</p>",
378
+ }),
379
+ ],
380
+ trailingReplies: [],
317
381
  latestReply: createPostView({
318
382
  id: "post-3",
319
383
  permalink: "/post-3",
@@ -340,18 +404,22 @@ describe("getThreadPreviewState", () => {
340
404
  const html = renderWithI18n(() =>
341
405
  ThreadPreview({
342
406
  rootPost: createPostView({ bodyHtml: "<p>Root</p>" }),
343
- secondReply: createPostView({
344
- id: "post-2",
345
- permalink: "/post-2",
346
- slug: "post-2",
347
- bodyHtml: "<p>Second</p>",
348
- }),
349
- penultimateReply: createPostView({
350
- id: "post-4",
351
- permalink: "/post-4",
352
- slug: "post-4",
353
- bodyHtml: "<p>Penultimate</p>",
354
- }),
407
+ leadingReplies: [
408
+ createPostView({
409
+ id: "post-2",
410
+ permalink: "/post-2",
411
+ slug: "post-2",
412
+ bodyHtml: "<p>Second</p>",
413
+ }),
414
+ ],
415
+ trailingReplies: [
416
+ createPostView({
417
+ id: "post-4",
418
+ permalink: "/post-4",
419
+ slug: "post-4",
420
+ bodyHtml: "<p>Penultimate</p>",
421
+ }),
422
+ ],
355
423
  latestReply: createPostView({
356
424
  id: "post-5",
357
425
  permalink: "/post-5",
@@ -372,12 +440,15 @@ describe("getThreadPreviewState", () => {
372
440
  const html = renderWithI18n(() =>
373
441
  ThreadPreview({
374
442
  rootPost: createPostView({ bodyHtml: "<p>Root</p>" }),
375
- penultimateReply: createPostView({
376
- id: "post-4",
377
- permalink: "/post-4",
378
- slug: "post-4",
379
- bodyHtml: "<p>Penultimate</p>",
380
- }),
443
+ leadingReplies: [],
444
+ trailingReplies: [
445
+ createPostView({
446
+ id: "post-4",
447
+ permalink: "/post-4",
448
+ slug: "post-4",
449
+ bodyHtml: "<p>Penultimate</p>",
450
+ }),
451
+ ],
381
452
  latestReply: createPostView({
382
453
  id: "post-5",
383
454
  permalink: "/post-5",
@@ -402,6 +473,7 @@ describe("getThreadPreviewState", () => {
402
473
  CuratedThreadPreview({
403
474
  curatedThread: {
404
475
  rootPost: post,
476
+ showContextRatings: false,
405
477
  segments: [{ post, hiddenBeforeCount: 0, highlighted: true }],
406
478
  },
407
479
  }),
@@ -419,6 +491,7 @@ describe("getThreadPreviewState", () => {
419
491
  });
420
492
  const curatedThread: NonNullable<TimelineItemView["curatedThread"]> = {
421
493
  rootPost: articlePost,
494
+ showContextRatings: false,
422
495
  segments: [
423
496
  {
424
497
  post: articlePost,
@@ -438,6 +511,74 @@ describe("getThreadPreviewState", () => {
438
511
  expect(html).not.toContain("<p>Body</p>");
439
512
  expect(html).not.toContain('id="continue"');
440
513
  });
514
+
515
+ it("shows ratings on every post in a complete Collection Thread", () => {
516
+ const root = createPostView({
517
+ id: "post-root",
518
+ slug: "post-root",
519
+ permalink: "/post-root",
520
+ rating: 3,
521
+ });
522
+ const reply = createPostView({
523
+ id: "post-reply",
524
+ slug: "post-reply",
525
+ permalink: "/post-reply",
526
+ replyToId: root.id,
527
+ threadRootId: root.id,
528
+ rating: 5,
529
+ isLastInThread: true,
530
+ });
531
+
532
+ const html = renderWithI18n(() =>
533
+ CuratedThreadPreview({
534
+ curatedThread: {
535
+ rootPost: root,
536
+ showContextRatings: true,
537
+ segments: [
538
+ { post: root, hiddenBeforeCount: 0, highlighted: false },
539
+ { post: reply, hiddenBeforeCount: 0, highlighted: true },
540
+ ],
541
+ },
542
+ }),
543
+ );
544
+
545
+ expect(html.match(/class="post-rating"/g)).toHaveLength(2);
546
+ expect(html.match(/thread-item-curated/g)).toHaveLength(1);
547
+ expect(html).toContain("thread-item-context");
548
+ });
549
+
550
+ it("keeps non-selected Featured context ratings hidden", () => {
551
+ const featured = createPostView({
552
+ id: "post-featured",
553
+ slug: "post-featured",
554
+ permalink: "/post-featured",
555
+ rating: 4,
556
+ });
557
+ const context = createPostView({
558
+ id: "post-context",
559
+ slug: "post-context",
560
+ permalink: "/post-context",
561
+ rating: 5,
562
+ isLastInThread: true,
563
+ });
564
+
565
+ const html = renderWithI18n(() =>
566
+ CuratedThreadPreview({
567
+ curatedThread: {
568
+ rootPost: featured,
569
+ showContextRatings: false,
570
+ segments: [
571
+ { post: featured, hiddenBeforeCount: 0, highlighted: true },
572
+ { post: context, hiddenBeforeCount: 0, highlighted: false },
573
+ ],
574
+ },
575
+ }),
576
+ );
577
+
578
+ expect(html.match(/class="post-rating"/g)).toHaveLength(1);
579
+ expect(html).toContain("thread-item-curated");
580
+ expect(html).toContain("thread-item-context");
581
+ });
441
582
  });
442
583
 
443
584
  describe("threadContextAssumesOverflow", () => {
@@ -166,11 +166,15 @@ describe("timeline cards", () => {
166
166
  expect(css).toContain("margin-bottom: 0;");
167
167
  });
168
168
 
169
- it("keeps prose list rhythm compact in live and exported reading", () => {
169
+ it("aligns list rhythm across published and ordinary compose prose", () => {
170
170
  const presetCss = readFileSync(
171
171
  new URL("../../../preset.css", import.meta.url),
172
172
  "utf8",
173
173
  );
174
+ const uiCss = readFileSync(
175
+ new URL("../../../styles/ui.css", import.meta.url),
176
+ "utf8",
177
+ );
174
178
  const exportCss = readFileSync(
175
179
  new URL(
176
180
  "../../../services/export-theme/styles/main.css",
@@ -186,10 +190,68 @@ describe("timeline cards", () => {
186
190
  /ul,\s*[\s\S]*ol\s*\{[\s\S]*padding-left:\s*1\.4em;/,
187
191
  );
188
192
  expect(presetCss).toMatch(
189
- /:where\(li,\s*dt\)\s*\{[\s\S]*margin-top:\s*0\.65em;[\s\S]*margin-bottom:\s*0\.65em;/,
193
+ /:where\(li,\s*dt\)\s*\{[\s\S]*margin-top:\s*0\.5em;[\s\S]*margin-bottom:\s*0\.5em;/,
194
+ );
195
+ expect(exportCss).toMatch(
196
+ /li\s*\{[\s\S]*margin-top:\s*0\.5em;[\s\S]*margin-bottom:\s*0\.5em;/,
197
+ );
198
+ expect(uiCss).toMatch(
199
+ /\.compose-tiptap-body\s+\.tiptap\s+ul\s*\{[^}]*padding-left:\s*1\.4em;[^}]*margin:\s*1\.25em 0;/,
200
+ );
201
+ expect(uiCss).toMatch(
202
+ /\.compose-tiptap-body\s+\.tiptap\s+ol\s*\{[^}]*padding-left:\s*2\.25em;[^}]*margin:\s*1\.25em 0;/,
203
+ );
204
+ expect(uiCss).toMatch(
205
+ /\.compose-tiptap-body\s+\.tiptap\s+:is\(ul, ol\)\s+:is\(ul, ol\)\s*\{[^}]*padding-left:\s*1\.5em;[^}]*margin:\s*0\.4em 0;/,
206
+ );
207
+ expect(uiCss).toMatch(
208
+ /\.compose-tiptap-body\s+\.tiptap\s+ul ul\s*\{[^}]*list-style-type:\s*circle;/,
209
+ );
210
+ expect(uiCss).toMatch(
211
+ /\.compose-tiptap-body\s+\.tiptap\s+ul ul ul\s*\{[^}]*list-style-type:\s*square;/,
212
+ );
213
+ expect(uiCss).toMatch(
214
+ /\.compose-tiptap-body\s+\.tiptap\s+ol ol\s*\{[^}]*list-style-type:\s*lower-alpha;/,
215
+ );
216
+ expect(uiCss).toMatch(
217
+ /\.compose-tiptap-body\s+\.tiptap\s+ol ol ol\s*\{[^}]*list-style-type:\s*lower-roman;/,
218
+ );
219
+ expect(uiCss).toMatch(
220
+ /\.compose-tiptap-body\s+\.tiptap\s+li\s*\{[^}]*margin:\s*0\.5em 0;/,
221
+ );
222
+ expect(uiCss).toContain(".compose-tiptap-body .tiptap li > p:first-child");
223
+ expect(uiCss).toContain(
224
+ ".compose-tiptap-body .tiptap li > p:has(+ :is(ul, ol))",
225
+ );
226
+ expect(uiCss).toMatch(
227
+ /p:has\(\+ \.ProseMirror-gapcursor \+ :is\(ul, ol\)\)\s*\{[^}]*margin-bottom:\s*0;/,
228
+ );
229
+ expect(uiCss).toMatch(
230
+ /\.compose-reply-compose-layout\s+\.compose-tiptap-body\s+\.tiptap\s+li\s*\{[^}]*margin:\s*0\.15em 0;/,
231
+ );
232
+ expect(uiCss).toMatch(
233
+ /\.compose-reply-compose-layout\s+\.compose-tiptap-body\s+\.tiptap\s+ol\s*\{[^}]*padding-left:\s*2\.25em;[^}]*margin:\s*0\.25em 0;/,
234
+ );
235
+ expect(uiCss).toMatch(
236
+ /\.compose-reply-compose-layout\s+\.compose-tiptap-body\s+\.tiptap\s+:is\(ul, ol\)\s+:is\(ul, ol\)\s*\{[^}]*padding-left:\s*1\.5em;[^}]*margin:\s*0\.2em 0;/,
237
+ );
238
+ expect(presetCss).toMatch(
239
+ /:where\(ol ol\)\s*\{[^}]*list-style-type:\s*lower-alpha;[^}]*margin-top:\s*0\.4em;[^}]*margin-bottom:\s*0\.4em;/,
240
+ );
241
+ expect(presetCss).toMatch(
242
+ /:where\(ol ol ol\)\s*\{[^}]*list-style-type:\s*lower-roman;/,
243
+ );
244
+ expect(exportCss).toMatch(
245
+ /ol ol\s*\{[^}]*list-style-type:\s*lower-alpha;[^}]*margin-top:\s*0\.4em;[^}]*margin-bottom:\s*0\.4em;/,
246
+ );
247
+ expect(exportCss).toMatch(
248
+ /ol ol ol\s*\{[^}]*list-style-type:\s*lower-roman;/,
249
+ );
250
+ expect(presetCss).toMatch(
251
+ /:where\(li > p:has\(\+ ol\)\)\s*\{[^}]*margin-bottom:\s*0;/,
190
252
  );
191
253
  expect(exportCss).toMatch(
192
- /li\s*\{[\s\S]*margin-top:\s*0\.65em;[\s\S]*margin-bottom:\s*0\.65em;/,
254
+ /li > p:has\(\+ ol\)\s*\{[^}]*margin-bottom:\s*0;/,
193
255
  );
194
256
  });
195
257
 
@@ -307,6 +369,12 @@ describe("timeline cards", () => {
307
369
  expect(css).toMatch(
308
370
  /\.compose-tiptap-body\s+\.tiptap\s+blockquote:focus-within\s*\{/,
309
371
  );
372
+ expect(css).toMatch(
373
+ /\.compose-tiptap-body\s+\.tiptap\s+blockquote\s*\{[^}]*margin:\s*1\.4rem 0;[^}]*padding:\s*1\.4rem 1rem 0\.75rem;/,
374
+ );
375
+ expect(css).toMatch(
376
+ /\.compose-reply-compose-layout\s+\.compose-tiptap-body\s+\.tiptap\s+blockquote\s*\{[^}]*margin:\s*0\.72em 0;/,
377
+ );
310
378
  });
311
379
 
312
380
  it("keeps quote compose aligned with quote-card typography", () => {
@@ -432,6 +500,34 @@ describe("timeline cards", () => {
432
500
  );
433
501
  });
434
502
 
503
+ it("keeps detail dates quiet while preserving standalone footer spacing", () => {
504
+ const css = readFileSync(
505
+ new URL("../../../styles/ui.css", import.meta.url),
506
+ "utf8",
507
+ );
508
+
509
+ expect(css).toMatch(
510
+ /\.post-header-meta-link\s*\{[^}]*font-size:\s*var\(--type-ui-hint\);/,
511
+ );
512
+ expect(css).toMatch(
513
+ /\.post-footer-detail\s*\{[^}]*margin-top:\s*24px;[^}]*font-size:\s*var\(--type-ui-hint\);[^}]*color:\s*var\(--site-text-secondary\);/,
514
+ );
515
+ expect(css).toMatch(
516
+ /\.thread-group-detail \.post-footer-detail\s*\{[^}]*margin-top:\s*var\(--content-gap\);/,
517
+ );
518
+ });
519
+
520
+ it("adds breathing room when a quote source leads directly into its footer", () => {
521
+ const css = readFileSync(
522
+ new URL("../../../styles/ui.css", import.meta.url),
523
+ "utf8",
524
+ );
525
+
526
+ expect(css).toMatch(
527
+ /\.feed-quote-attribution:has\(\+ \.post-menu-footer\)\s*\{[^}]*margin-bottom:\s*0\.45rem;/,
528
+ );
529
+ });
530
+
435
531
  it("renders titled note feed summaries as secondary prose without shrinking detail reading", () => {
436
532
  const post = createPostView({
437
533
  format: "note",
@@ -445,6 +541,63 @@ describe("timeline cards", () => {
445
541
  expect(detailHtml).toContain('class="e-content prose post-detail-body"');
446
542
  });
447
543
 
544
+ it("progressively enhances canonical endnotes on wide detail and timeline posts", () => {
545
+ const css = readFileSync(
546
+ new URL("../../../styles/ui.css", import.meta.url),
547
+ "utf8",
548
+ );
549
+ const tokens = readFileSync(
550
+ new URL("../../../styles/tokens.css", import.meta.url),
551
+ "utf8",
552
+ );
553
+
554
+ expect(css).toMatch(
555
+ /\[data-post-view\],\s*\[data-timeline-item\]\s*\{[^}]*container:\s*post-view\s*\/\s*inline-size;/,
556
+ );
557
+ expect(css).toContain(
558
+ "[data-timeline-item] article[data-post] > [data-post-body].prose",
559
+ );
560
+ expect(css).toContain("@container post-view (min-width: 56rem)");
561
+ expect(css).toContain("--jant-footnote-rail-enabled: 1");
562
+ expect(css).toContain(".footnote-rail-measuring");
563
+ expect(css).toContain(".footnote-rail-ready");
564
+ expect(css).toContain("--footnote-rail-y");
565
+ expect(css).toContain("--footnote-rail-number");
566
+ expect(css).toContain("--footnote-rail-block-size");
567
+ expect(css).toMatch(
568
+ /> \.footnote-endnotes\s*\{[^}]*position:\s*absolute;[^}]*inset-inline-start:\s*calc\(100% \+ var\(--layout-sidenote-gap\)\);[^}]*width:\s*var\(--layout-sidenote-width\);/,
569
+ );
570
+ expect(css).toMatch(
571
+ /> \.footnote-list\s*> \.footnote\s*\{[^}]*display:\s*grid;[^}]*column-gap:\s*1ch;[^}]*align-items:\s*baseline;[^}]*list-style:\s*none;/,
572
+ );
573
+ expect(css).toMatch(
574
+ /> \.footnote::before\s*\{[^}]*content:\s*var\(--footnote-rail-number\);[^}]*font-size:\s*inherit;[^}]*line-height:\s*inherit;[^}]*color:\s*var\(--site-footnote-marker\);/,
575
+ );
576
+ expect(css).toMatch(
577
+ /\.footnote-rail-ready[\s\S]*> \.footnote-list[\s\S]*> \.footnote\s*\{[^}]*position:\s*absolute;[^}]*inset-block-start:\s*var\(--footnote-rail-y\);/,
578
+ );
579
+ expect(css).toMatch(
580
+ /\.footnote-rail-ready[\s\S]*\.footnote:target\s*\{[^}]*background:\s*transparent;/,
581
+ );
582
+ expect(css).toMatch(
583
+ /\.footnote-rail-ready[\s\S]*\.footnote-backlinks\s*\{[^}]*position:\s*absolute;[^}]*clip-path:\s*inset\(50%\);/,
584
+ );
585
+ expect(css).toMatch(
586
+ /\.footnote-rail-ready[\s\S]*\.footnote-backlinks:focus-within\s*\{[^}]*position:\s*static;[^}]*clip-path:\s*none;/,
587
+ );
588
+ expect(css).toMatch(
589
+ /\.footnote-ref\s*\{[^}]*--site-prose-link-color:\s*var\(--site-footnote-marker\);[^}]*--site-prose-link-underline:\s*transparent;[^}]*margin-inline:\s*0\.25rem;[^}]*vertical-align:\s*super;[^}]*font-size:\s*var\(--type-footnote-ref\);[^}]*color:\s*var\(--site-footnote-marker\);/,
590
+ );
591
+ expect(tokens).toContain(
592
+ "--type-footnote-ref: calc(var(--type-body-size) * 0.75)",
593
+ );
594
+ expect(css).toContain("@media print");
595
+ expect(css).not.toContain("@supports (grid-template-rows: subgrid)");
596
+ expect(css).not.toContain(".footnote-document");
597
+ expect(css).not.toContain(".footnote-main");
598
+ expect(css).not.toContain(".footnote-body");
599
+ });
600
+
448
601
  it("can render full article bodies inside feed contexts without permalink anchors", () => {
449
602
  const post = createPostView({
450
603
  format: "note",
@@ -505,6 +658,28 @@ describe("timeline cards", () => {
505
658
  expect(html).toContain("<p>Rest</p>");
506
659
  });
507
660
 
661
+ it("renders read more before text attachments on truncated notes", () => {
662
+ const post = createPostView({
663
+ format: "note",
664
+ title: undefined,
665
+ bodyHtml: "<p>Intro</p><span data-note-break></span><p>Rest</p>",
666
+ summaryHasMore: true,
667
+ media: [
668
+ createMediaView({
669
+ url: "/attachments/notes.txt",
670
+ thumbnailUrl: undefined,
671
+ mimeType: "text/plain",
672
+ }),
673
+ ],
674
+ });
675
+
676
+ const html = renderWithI18n(NoteCard({ post, mode: "feed" }));
677
+
678
+ expect(html.indexOf("data-note-expand")).toBeLessThan(
679
+ html.indexOf("data-post-media"),
680
+ );
681
+ });
682
+
508
683
  it("renders untitled notes in full without a control when not truncated", () => {
509
684
  const post = createPostView({
510
685
  format: "note",
@@ -1,20 +1,18 @@
1
1
  import type { PostView } from "../../types.js";
2
2
 
3
3
  export function getThreadPreviewState({
4
- secondReply,
5
- penultimateReply,
4
+ leadingReplies,
5
+ trailingReplies,
6
6
  latestReply,
7
7
  totalReplyCount,
8
8
  }: {
9
- secondReply?: PostView;
10
- penultimateReply?: PostView;
9
+ leadingReplies: PostView[];
10
+ trailingReplies: PostView[];
11
11
  latestReply: PostView;
12
12
  totalReplyCount: number;
13
13
  }) {
14
14
  const visibleReplyIds = new Set(
15
- [secondReply, penultimateReply, latestReply]
16
- .filter((post): post is PostView => post !== undefined)
17
- .map((post) => post.id),
15
+ [...leadingReplies, ...trailingReplies, latestReply].map((post) => post.id),
18
16
  );
19
17
  const hiddenCount = Math.max(0, totalReplyCount - visibleReplyIds.size);
20
18