@jant/core 0.3.35 → 0.3.37

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 (307) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -0,0 +1,264 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { extractBodyText } from "../summary.js";
3
+
4
+ describe("extractBodyText", () => {
5
+ it("extracts text from paragraphs", () => {
6
+ const doc = JSON.stringify({
7
+ type: "doc",
8
+ content: [
9
+ {
10
+ type: "paragraph",
11
+ content: [{ type: "text", text: "Hello world" }],
12
+ },
13
+ {
14
+ type: "paragraph",
15
+ content: [{ type: "text", text: "Second paragraph" }],
16
+ },
17
+ ],
18
+ });
19
+
20
+ expect(extractBodyText(doc)).toBe("Hello world Second paragraph");
21
+ });
22
+
23
+ it("extracts text from headings", () => {
24
+ const doc = JSON.stringify({
25
+ type: "doc",
26
+ content: [
27
+ {
28
+ type: "heading",
29
+ attrs: { level: 1 },
30
+ content: [{ type: "text", text: "My Title" }],
31
+ },
32
+ {
33
+ type: "paragraph",
34
+ content: [{ type: "text", text: "Body text" }],
35
+ },
36
+ ],
37
+ });
38
+
39
+ expect(extractBodyText(doc)).toBe("My Title Body text");
40
+ });
41
+
42
+ it("extracts text from bullet lists", () => {
43
+ const doc = JSON.stringify({
44
+ type: "doc",
45
+ content: [
46
+ {
47
+ type: "bulletList",
48
+ content: [
49
+ {
50
+ type: "listItem",
51
+ content: [
52
+ {
53
+ type: "paragraph",
54
+ content: [{ type: "text", text: "Item one" }],
55
+ },
56
+ ],
57
+ },
58
+ {
59
+ type: "listItem",
60
+ content: [
61
+ {
62
+ type: "paragraph",
63
+ content: [{ type: "text", text: "Item two" }],
64
+ },
65
+ ],
66
+ },
67
+ ],
68
+ },
69
+ ],
70
+ });
71
+
72
+ expect(extractBodyText(doc)).toContain("Item one");
73
+ expect(extractBodyText(doc)).toContain("Item two");
74
+ });
75
+
76
+ it("extracts text from ordered lists", () => {
77
+ const doc = JSON.stringify({
78
+ type: "doc",
79
+ content: [
80
+ {
81
+ type: "orderedList",
82
+ content: [
83
+ {
84
+ type: "listItem",
85
+ content: [
86
+ {
87
+ type: "paragraph",
88
+ content: [{ type: "text", text: "First" }],
89
+ },
90
+ ],
91
+ },
92
+ ],
93
+ },
94
+ ],
95
+ });
96
+
97
+ expect(extractBodyText(doc)).toContain("First");
98
+ });
99
+
100
+ it("extracts text from blockquotes", () => {
101
+ const doc = JSON.stringify({
102
+ type: "doc",
103
+ content: [
104
+ {
105
+ type: "blockquote",
106
+ content: [
107
+ {
108
+ type: "paragraph",
109
+ content: [{ type: "text", text: "Quoted text" }],
110
+ },
111
+ ],
112
+ },
113
+ ],
114
+ });
115
+
116
+ expect(extractBodyText(doc)).toContain("Quoted text");
117
+ });
118
+
119
+ it("extracts text from code blocks", () => {
120
+ const doc = JSON.stringify({
121
+ type: "doc",
122
+ content: [
123
+ {
124
+ type: "codeBlock",
125
+ content: [{ type: "text", text: "const x = 1;" }],
126
+ },
127
+ ],
128
+ });
129
+
130
+ expect(extractBodyText(doc)).toBe("const x = 1;");
131
+ });
132
+
133
+ it("extracts text from tables", () => {
134
+ const doc = JSON.stringify({
135
+ type: "doc",
136
+ content: [
137
+ {
138
+ type: "table",
139
+ content: [
140
+ {
141
+ type: "tableRow",
142
+ content: [
143
+ {
144
+ type: "tableHeader",
145
+ content: [
146
+ {
147
+ type: "paragraph",
148
+ content: [{ type: "text", text: "Header" }],
149
+ },
150
+ ],
151
+ },
152
+ ],
153
+ },
154
+ {
155
+ type: "tableRow",
156
+ content: [
157
+ {
158
+ type: "tableCell",
159
+ content: [
160
+ {
161
+ type: "paragraph",
162
+ content: [{ type: "text", text: "Cell data" }],
163
+ },
164
+ ],
165
+ },
166
+ ],
167
+ },
168
+ ],
169
+ },
170
+ ],
171
+ });
172
+
173
+ expect(extractBodyText(doc)).toContain("Header");
174
+ expect(extractBodyText(doc)).toContain("Cell data");
175
+ });
176
+
177
+ it("skips image nodes", () => {
178
+ const doc = JSON.stringify({
179
+ type: "doc",
180
+ content: [
181
+ {
182
+ type: "paragraph",
183
+ content: [{ type: "text", text: "Before image" }],
184
+ },
185
+ {
186
+ type: "image",
187
+ attrs: { src: "https://example.com/img.png", alt: "Alt text" },
188
+ },
189
+ {
190
+ type: "paragraph",
191
+ content: [{ type: "text", text: "After image" }],
192
+ },
193
+ ],
194
+ });
195
+
196
+ const result = extractBodyText(doc);
197
+ expect(result).toContain("Before image");
198
+ expect(result).toContain("After image");
199
+ expect(result).not.toContain("Alt text");
200
+ expect(result).not.toContain("img.png");
201
+ });
202
+
203
+ it("skips moreBreak nodes", () => {
204
+ const doc = JSON.stringify({
205
+ type: "doc",
206
+ content: [
207
+ {
208
+ type: "paragraph",
209
+ content: [{ type: "text", text: "Before break" }],
210
+ },
211
+ { type: "moreBreak" },
212
+ {
213
+ type: "paragraph",
214
+ content: [{ type: "text", text: "After break" }],
215
+ },
216
+ ],
217
+ });
218
+
219
+ const result = extractBodyText(doc);
220
+ expect(result).toContain("Before break");
221
+ expect(result).toContain("After break");
222
+ });
223
+
224
+ it("skips horizontalRule nodes", () => {
225
+ const doc = JSON.stringify({
226
+ type: "doc",
227
+ content: [
228
+ {
229
+ type: "paragraph",
230
+ content: [{ type: "text", text: "Above rule" }],
231
+ },
232
+ { type: "horizontalRule" },
233
+ {
234
+ type: "paragraph",
235
+ content: [{ type: "text", text: "Below rule" }],
236
+ },
237
+ ],
238
+ });
239
+
240
+ const result = extractBodyText(doc);
241
+ expect(result).toContain("Above rule");
242
+ expect(result).toContain("Below rule");
243
+ });
244
+
245
+ it("returns null for invalid JSON", () => {
246
+ expect(extractBodyText("not json")).toBeNull();
247
+ expect(extractBodyText("{invalid")).toBeNull();
248
+ });
249
+
250
+ it("returns null for empty doc", () => {
251
+ const doc = JSON.stringify({ type: "doc", content: [] });
252
+ expect(extractBodyText(doc)).toBeNull();
253
+ });
254
+
255
+ it("returns null for non-doc type", () => {
256
+ const doc = JSON.stringify({ type: "paragraph", content: [] });
257
+ expect(extractBodyText(doc)).toBeNull();
258
+ });
259
+
260
+ it("returns null for doc without content", () => {
261
+ const doc = JSON.stringify({ type: "doc" });
262
+ expect(extractBodyText(doc)).toBeNull();
263
+ });
264
+ });
@@ -23,7 +23,7 @@ describe("buildThemeStyle", () => {
23
23
  expect(css).toContain("--font-body:");
24
24
  expect(css).toContain("--font-heading:");
25
25
  expect(css).toContain("Charter");
26
- expect(css).toContain(":root.dark");
26
+ expect(css).toContain("prefers-color-scheme: dark");
27
27
  });
28
28
 
29
29
  it("font override merges with color theme", () => {
@@ -11,7 +11,6 @@ import { describe, it, expect, beforeEach } from "vitest";
11
11
  import { createTestDatabase } from "../../__tests__/helpers/db.js";
12
12
  import { createPostService } from "../../services/post.js";
13
13
  import { createMediaService } from "../../services/media.js";
14
- import { createPathRegistryService } from "../../services/path-registry.js";
15
14
  import { buildMediaMap } from "../media-helpers.js";
16
15
  import type { Database } from "../../db/index.js";
17
16
  import type { PostWithMedia } from "../../types.js";
@@ -24,14 +23,14 @@ describe("Timeline data assembly", () => {
24
23
  beforeEach(() => {
25
24
  const testDb = createTestDatabase();
26
25
  db = testDb.db as unknown as Database;
27
- postService = createPostService(db, createPathRegistryService(db));
26
+ postService = createPostService(db, { slugIdLength: 5 });
28
27
  mediaService = createMediaService(db);
29
28
  });
30
29
 
31
30
  it("assembles timeline items with media attachments", async () => {
32
31
  const post = await postService.create({
33
32
  format: "note",
34
- body: "Hello",
33
+ bodyMarkdown: "Hello",
35
34
  });
36
35
 
37
36
  const posts = await postService.list({
@@ -57,19 +56,19 @@ describe("Timeline data assembly", () => {
57
56
  expect(items[0]?.post.mediaAttachments).toEqual([]);
58
57
  });
59
58
 
60
- it("identifies thread roots and builds thread previews", async () => {
59
+ it("identifies thread roots and builds thread context", async () => {
61
60
  const root = await postService.create({
62
61
  format: "note",
63
- body: "Thread root",
62
+ bodyMarkdown: "Thread root",
64
63
  });
65
64
  await postService.create({
66
65
  format: "note",
67
- body: "Reply 1",
66
+ bodyMarkdown: "Reply 1",
68
67
  replyToId: root.id,
69
68
  });
70
69
  await postService.create({
71
70
  format: "note",
72
- body: "Reply 2",
71
+ bodyMarkdown: "Reply 2",
73
72
  replyToId: root.id,
74
73
  });
75
74
 
@@ -90,10 +89,12 @@ describe("Timeline data assembly", () => {
90
89
  expect(threadRootIds).toEqual([root.id]);
91
90
  expect(replyCounts.get(root.id)).toBe(2);
92
91
 
93
- const threadPreviews = await postService.getThreadPreviews(threadRootIds);
94
- const replies = threadPreviews.get(root.id);
95
- expect(replies).toHaveLength(2);
96
- expect(replies?.[0]?.body).toBe("Reply 1");
92
+ const threadContexts =
93
+ await postService.getThreadTimelineContext(threadRootIds);
94
+ const ctx = threadContexts.get(root.id);
95
+ expect(ctx).toBeDefined();
96
+ expect(ctx?.latestReply.bodyText).toBe("Reply 2");
97
+ expect(ctx?.totalReplyCount).toBe(2);
97
98
 
98
99
  // Assemble items
99
100
  const rawMediaMap = await mediaService.getByPostIds(postIds);
@@ -105,18 +106,20 @@ describe("Timeline data assembly", () => {
105
106
  mediaAttachments: mediaMap.get(post.id) ?? [],
106
107
  };
107
108
 
108
- const replyCount = replyCounts.get(post.id) ?? 0;
109
- const previewReplies = threadPreviews.get(post.id);
109
+ const threadCtx = threadContexts.get(post.id);
110
110
 
111
- if (replyCount > 0 && previewReplies) {
111
+ if (threadCtx) {
112
112
  return {
113
113
  post: postWithMedia,
114
114
  threadPreview: {
115
- replies: previewReplies.map((r) => ({
116
- ...r,
115
+ latestReply: {
116
+ ...threadCtx.latestReply,
117
117
  mediaAttachments: [],
118
- })),
119
- totalReplyCount: replyCount,
118
+ },
119
+ parentReply: threadCtx.parentReply
120
+ ? { ...threadCtx.parentReply, mediaAttachments: [] }
121
+ : undefined,
122
+ totalReplyCount: threadCtx.totalReplyCount,
120
123
  },
121
124
  };
122
125
  }
@@ -126,18 +129,18 @@ describe("Timeline data assembly", () => {
126
129
 
127
130
  expect(items).toHaveLength(1);
128
131
  expect(items[0]?.threadPreview).toBeDefined();
129
- expect(items[0]?.threadPreview?.replies).toHaveLength(2);
132
+ expect(items[0]?.threadPreview?.latestReply.bodyText).toBe("Reply 2");
130
133
  expect(items[0]?.threadPreview?.totalReplyCount).toBe(2);
131
134
  });
132
135
 
133
136
  it("excludes replies from top-level list", async () => {
134
137
  const root = await postService.create({
135
138
  format: "note",
136
- body: "Root",
139
+ bodyMarkdown: "Root",
137
140
  });
138
141
  await postService.create({
139
142
  format: "note",
140
- body: "Reply",
143
+ bodyMarkdown: "Reply",
141
144
  replyToId: root.id,
142
145
  });
143
146
 
@@ -148,7 +151,7 @@ describe("Timeline data assembly", () => {
148
151
  });
149
152
 
150
153
  expect(posts).toHaveLength(1);
151
- expect(posts[0]?.body).toBe("Root");
154
+ expect(posts[0]?.bodyText).toBe("Root");
152
155
  });
153
156
 
154
157
  it("supports cursor pagination for load more", async () => {
@@ -157,7 +160,7 @@ describe("Timeline data assembly", () => {
157
160
  posts.push(
158
161
  await postService.create({
159
162
  format: "note",
160
- body: `Post ${i}`,
163
+ bodyMarkdown: `Post ${i}`,
161
164
  publishedAt: 1000 + i,
162
165
  }),
163
166
  );
@@ -188,7 +191,7 @@ describe("Timeline data assembly", () => {
188
191
  for (let i = 0; i < 5; i++) {
189
192
  await postService.create({
190
193
  format: "note",
191
- body: `Post ${i}`,
194
+ bodyMarkdown: `Post ${i}`,
192
195
  publishedAt: 1000 + i,
193
196
  });
194
197
  }
@@ -203,8 +206,8 @@ describe("Timeline data assembly", () => {
203
206
  offset: 0,
204
207
  });
205
208
  expect(page1).toHaveLength(2);
206
- expect(page1[0]?.body).toBe("Post 4");
207
- expect(page1[1]?.body).toBe("Post 3");
209
+ expect(page1[0]?.bodyText).toBe("Post 4");
210
+ expect(page1[1]?.bodyText).toBe("Post 3");
208
211
 
209
212
  // Page 2
210
213
  const page2 = await postService.list({
@@ -214,8 +217,8 @@ describe("Timeline data assembly", () => {
214
217
  offset: 2,
215
218
  });
216
219
  expect(page2).toHaveLength(2);
217
- expect(page2[0]?.body).toBe("Post 2");
218
- expect(page2[1]?.body).toBe("Post 1");
220
+ expect(page2[0]?.bodyText).toBe("Post 2");
221
+ expect(page2[1]?.bodyText).toBe("Post 1");
219
222
 
220
223
  // Page 3 (partial)
221
224
  const page3 = await postService.list({
@@ -225,14 +228,14 @@ describe("Timeline data assembly", () => {
225
228
  offset: 4,
226
229
  });
227
230
  expect(page3).toHaveLength(1);
228
- expect(page3[0]?.body).toBe("Post 0");
231
+ expect(page3[0]?.bodyText).toBe("Post 0");
229
232
  });
230
233
 
231
234
  it("computes total pages from count", async () => {
232
235
  for (let i = 0; i < 5; i++) {
233
236
  await postService.create({
234
237
  format: "note",
235
- body: `Post ${i}`,
238
+ bodyMarkdown: `Post ${i}`,
236
239
  });
237
240
  }
238
241