@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
@@ -11,7 +11,7 @@ describe("Compose Routes", () => {
11
11
  const res = await app.request("/compose", {
12
12
  method: "POST",
13
13
  headers: { "Content-Type": "application/json" },
14
- body: JSON.stringify({ format: "note", body: "Hello" }),
14
+ body: JSON.stringify({ format: "note", bodyMarkdown: "Hello" }),
15
15
  });
16
16
 
17
17
  expect(res.status).toBe(302);
@@ -25,23 +25,22 @@ describe("Compose Routes", () => {
25
25
  const res = await app.request("/compose", {
26
26
  method: "POST",
27
27
  headers: { "Content-Type": "application/json" },
28
- body: JSON.stringify({ format: "note", body: "Hello world" }),
28
+ body: JSON.stringify({ format: "note", bodyMarkdown: "Hello world" }),
29
29
  });
30
30
 
31
31
  expect(res.status).toBe(200);
32
32
  expect(res.headers.get("Content-Type")).toBe("text/event-stream");
33
33
 
34
34
  const text = await res.text();
35
- // SSE prepends the card to the timeline
35
+ // SSE closes the compose dialog and resets signals
36
36
  expect(text).toContain("datastar-patch-elements");
37
- expect(text).toContain('data-format="note"');
38
- expect(text).toContain("selector #timeline-items");
37
+ expect(text).toContain("compose-dialog");
39
38
 
40
39
  // Verify post was created
41
40
  const posts = await services.posts.list();
42
41
  expect(posts).toHaveLength(1);
43
42
  expect(posts[0].format).toBe("note");
44
- expect(posts[0].body).toBe("Hello world");
43
+ expect(posts[0].bodyText).toBe("Hello world");
45
44
  expect(posts[0].status).toBe("published");
46
45
  });
47
46
 
@@ -54,7 +53,7 @@ describe("Compose Routes", () => {
54
53
  headers: { "Content-Type": "application/json" },
55
54
  body: JSON.stringify({
56
55
  format: "link",
57
- body: "Check this out",
56
+ bodyMarkdown: "Check this out",
58
57
  url: "https://example.com",
59
58
  }),
60
59
  });
@@ -62,9 +61,6 @@ describe("Compose Routes", () => {
62
61
  expect(res.status).toBe(200);
63
62
  expect(res.headers.get("Content-Type")).toBe("text/event-stream");
64
63
 
65
- const text = await res.text();
66
- expect(text).toContain('data-format="link"');
67
-
68
64
  const posts = await services.posts.list();
69
65
  expect(posts).toHaveLength(1);
70
66
  expect(posts[0].format).toBe("link");
@@ -80,7 +76,7 @@ describe("Compose Routes", () => {
80
76
  headers: { "Content-Type": "application/json" },
81
77
  body: JSON.stringify({
82
78
  format: "quote",
83
- body: "Great insight",
79
+ bodyMarkdown: "Great insight",
84
80
  quoteText: "The original quote",
85
81
  url: "https://example.com/source",
86
82
  }),
@@ -89,9 +85,6 @@ describe("Compose Routes", () => {
89
85
  expect(res.status).toBe(200);
90
86
  expect(res.headers.get("Content-Type")).toBe("text/event-stream");
91
87
 
92
- const text = await res.text();
93
- expect(text).toContain('data-format="quote"');
94
-
95
88
  const posts = await services.posts.list();
96
89
  expect(posts).toHaveLength(1);
97
90
  expect(posts[0].format).toBe("quote");
@@ -107,7 +100,7 @@ describe("Compose Routes", () => {
107
100
  headers: { "Content-Type": "application/json" },
108
101
  body: JSON.stringify({
109
102
  format: "note",
110
- body: "Draft content",
103
+ bodyMarkdown: "Draft content",
111
104
  status: "draft",
112
105
  }),
113
106
  });
@@ -133,7 +126,7 @@ describe("Compose Routes", () => {
133
126
  const res = await app.request("/compose", {
134
127
  method: "POST",
135
128
  headers: { "Content-Type": "application/json" },
136
- body: JSON.stringify({ format: "invalid", body: "Hello" }),
129
+ body: JSON.stringify({ format: "invalid", bodyMarkdown: "Hello" }),
137
130
  });
138
131
 
139
132
  expect(res.status).toBe(200);
@@ -163,7 +156,7 @@ describe("Compose Routes", () => {
163
156
  headers: { "Content-Type": "application/json" },
164
157
  body: JSON.stringify({
165
158
  format: "note",
166
- body: "Post with media",
159
+ bodyMarkdown: "Post with media",
167
160
  mediaIds: [media.id],
168
161
  }),
169
162
  });
@@ -186,7 +179,7 @@ describe("Compose Routes", () => {
186
179
  const res = await app.request("/compose", {
187
180
  method: "POST",
188
181
  headers: { "Content-Type": "application/json" },
189
- body: JSON.stringify({ format: "note", body: "Hello" }),
182
+ body: JSON.stringify({ format: "note", bodyMarkdown: "Hello" }),
190
183
  });
191
184
 
192
185
  const text = await res.text();
@@ -202,7 +195,7 @@ describe("Compose Routes", () => {
202
195
  const res = await app.request("/compose", {
203
196
  method: "POST",
204
197
  headers: { "Content-Type": "application/json" },
205
- body: JSON.stringify({ body: "No format" }),
198
+ body: JSON.stringify({ bodyMarkdown: "No format" }),
206
199
  });
207
200
 
208
201
  expect(res.status).toBe(200);
@@ -222,7 +215,7 @@ describe("Compose Routes", () => {
222
215
  "Content-Type": "application/json",
223
216
  Accept: "application/json",
224
217
  },
225
- body: JSON.stringify({ format: "note", body: "Hello JSON" }),
218
+ body: JSON.stringify({ format: "note", bodyMarkdown: "Hello JSON" }),
226
219
  });
227
220
 
228
221
  expect(res.status).toBe(200);
@@ -230,11 +223,11 @@ describe("Compose Routes", () => {
230
223
 
231
224
  const data = await res.json();
232
225
  expect(data.status).toBe("published");
233
- expect(data.cardHtml).toContain('data-format="note"');
226
+ expect(data.permalink).toBeDefined();
234
227
 
235
228
  const posts = await services.posts.list();
236
229
  expect(posts).toHaveLength(1);
237
- expect(posts[0].body).toBe("Hello JSON");
230
+ expect(posts[0].bodyText).toBe("Hello JSON");
238
231
  });
239
232
 
240
233
  it("returns JSON for draft", async () => {
@@ -249,7 +242,7 @@ describe("Compose Routes", () => {
249
242
  },
250
243
  body: JSON.stringify({
251
244
  format: "note",
252
- body: "Draft JSON",
245
+ bodyMarkdown: "Draft JSON",
253
246
  status: "draft",
254
247
  }),
255
248
  });
@@ -274,7 +267,7 @@ describe("Compose Routes", () => {
274
267
  "Content-Type": "application/json",
275
268
  Accept: "application/json",
276
269
  },
277
- body: JSON.stringify({ format: "invalid", body: "Hello" }),
270
+ body: JSON.stringify({ format: "invalid", bodyMarkdown: "Hello" }),
278
271
  });
279
272
 
280
273
  expect(res.status).toBe(422);
@@ -13,9 +13,10 @@ describe("Collections API Routes", () => {
13
13
 
14
14
  const body = await res.json();
15
15
  expect(body.collections).toEqual([]);
16
+ expect(body.sidebarItems).toEqual([]);
16
17
  });
17
18
 
18
- it("returns collections with post counts", async () => {
19
+ it("returns collections with post counts and sidebar items", async () => {
19
20
  const { app, services } = createTestApp();
20
21
  app.route("/api/collections", collectionsApiRoutes);
21
22
 
@@ -25,7 +26,7 @@ describe("Collections API Routes", () => {
25
26
  });
26
27
  const post = await services.posts.create({
27
28
  format: "note",
28
- body: "tech post",
29
+ bodyMarkdown: "tech post",
29
30
  });
30
31
  await services.collections.addPost(col.id, post.id);
31
32
 
@@ -35,6 +36,10 @@ describe("Collections API Routes", () => {
35
36
  expect(body.collections).toHaveLength(1);
36
37
  expect(body.collections[0].slug).toBe("tech");
37
38
  expect(body.collections[0].postCount).toBe(1);
39
+
40
+ expect(body.sidebarItems).toHaveLength(1);
41
+ expect(body.sidebarItems[0].type).toBe("collection");
42
+ expect(body.sidebarItems[0].collectionId).toBe(col.id);
38
43
  });
39
44
  });
40
45
 
@@ -60,7 +65,7 @@ describe("Collections API Routes", () => {
60
65
  const { app } = createTestApp();
61
66
  app.route("/api/collections", collectionsApiRoutes);
62
67
 
63
- const res = await app.request("/api/collections/abc");
68
+ const res = await app.request("/api/collections/!!invalid!!");
64
69
  expect(res.status).toBe(400);
65
70
  });
66
71
 
@@ -68,7 +73,9 @@ describe("Collections API Routes", () => {
68
73
  const { app } = createTestApp();
69
74
  app.route("/api/collections", collectionsApiRoutes);
70
75
 
71
- const res = await app.request("/api/collections/9999");
76
+ const res = await app.request(
77
+ `/api/collections/${"00000000-0000-0000-0000-000000009999"}`,
78
+ );
72
79
  expect(res.status).toBe(404);
73
80
  });
74
81
  });
@@ -122,46 +129,6 @@ describe("Collections API Routes", () => {
122
129
  });
123
130
  });
124
131
 
125
- describe("PUT /api/collections/reorder", () => {
126
- it("returns 401 when not authenticated", async () => {
127
- const { app } = createTestApp({ authenticated: false });
128
- app.route("/api/collections", collectionsApiRoutes);
129
-
130
- const res = await app.request("/api/collections/reorder", {
131
- method: "PUT",
132
- headers: { "Content-Type": "application/json" },
133
- body: JSON.stringify({ ids: [1, 2] }),
134
- });
135
-
136
- expect(res.status).toBe(401);
137
- });
138
-
139
- it("reorders collections when authenticated", async () => {
140
- const { app, services } = createTestApp({ authenticated: true });
141
- app.route("/api/collections", collectionsApiRoutes);
142
-
143
- const col1 = await services.collections.create({
144
- slug: "first",
145
- title: "First",
146
- });
147
- const col2 = await services.collections.create({
148
- slug: "second",
149
- title: "Second",
150
- });
151
-
152
- const res = await app.request("/api/collections/reorder", {
153
- method: "PUT",
154
- headers: { "Content-Type": "application/json" },
155
- body: JSON.stringify({ ids: [col2.id, col1.id] }),
156
- });
157
-
158
- expect(res.status).toBe(200);
159
- const body = await res.json();
160
- expect(body.collections[0].slug).toBe("second");
161
- expect(body.collections[1].slug).toBe("first");
162
- });
163
- });
164
-
165
132
  describe("PUT /api/collections/:id", () => {
166
133
  it("updates a collection when authenticated", async () => {
167
134
  const { app, services } = createTestApp({ authenticated: true });
@@ -187,11 +154,14 @@ describe("Collections API Routes", () => {
187
154
  const { app } = createTestApp({ authenticated: true });
188
155
  app.route("/api/collections", collectionsApiRoutes);
189
156
 
190
- const res = await app.request("/api/collections/9999", {
191
- method: "PUT",
192
- headers: { "Content-Type": "application/json" },
193
- body: JSON.stringify({ title: "test" }),
194
- });
157
+ const res = await app.request(
158
+ `/api/collections/${"00000000-0000-0000-0000-000000009999"}`,
159
+ {
160
+ method: "PUT",
161
+ headers: { "Content-Type": "application/json" },
162
+ body: JSON.stringify({ title: "test" }),
163
+ },
164
+ );
195
165
 
196
166
  expect(res.status).toBe(404);
197
167
  });
@@ -239,14 +209,87 @@ describe("Collections API Routes", () => {
239
209
  const { app } = createTestApp({ authenticated: true });
240
210
  app.route("/api/collections", collectionsApiRoutes);
241
211
 
242
- const res = await app.request("/api/collections/9999", {
243
- method: "DELETE",
244
- });
212
+ const res = await app.request(
213
+ `/api/collections/${"00000000-0000-0000-0000-000000009999"}`,
214
+ { method: "DELETE" },
215
+ );
245
216
 
246
217
  expect(res.status).toBe(404);
247
218
  });
248
219
  });
249
220
 
221
+ describe("POST /api/collections/sidebar-items", () => {
222
+ it("creates a divider sidebar item", async () => {
223
+ const { app } = createTestApp({ authenticated: true });
224
+ app.route("/api/collections", collectionsApiRoutes);
225
+
226
+ const res = await app.request("/api/collections/sidebar-items", {
227
+ method: "POST",
228
+ headers: { "Content-Type": "application/json" },
229
+ });
230
+
231
+ expect(res.status).toBe(201);
232
+ const body = await res.json();
233
+ expect(body.type).toBe("divider");
234
+ expect(body.collectionId).toBeNull();
235
+ });
236
+ });
237
+
238
+ describe("DELETE /api/collections/sidebar-items/:id", () => {
239
+ it("deletes a sidebar item", async () => {
240
+ const { app, services } = createTestApp({ authenticated: true });
241
+ app.route("/api/collections", collectionsApiRoutes);
242
+
243
+ const item = await services.collections.createSidebarItem("divider");
244
+
245
+ const res = await app.request(
246
+ `/api/collections/sidebar-items/${item.id}`,
247
+ { method: "DELETE" },
248
+ );
249
+
250
+ expect(res.status).toBe(200);
251
+ const body = await res.json();
252
+ expect(body.success).toBe(true);
253
+ });
254
+ });
255
+
256
+ describe("PUT /api/collections/sidebar-items/:id/move", () => {
257
+ it("moves a sidebar item", async () => {
258
+ const { app, services } = createTestApp({ authenticated: true });
259
+ app.route("/api/collections", collectionsApiRoutes);
260
+
261
+ await services.collections.create({ slug: "a", title: "A" });
262
+ await services.collections.create({ slug: "b", title: "B" });
263
+ await services.collections.create({ slug: "c", title: "C" });
264
+
265
+ const items = await services.collections.listSidebarItems();
266
+ expect(items).toHaveLength(3);
267
+ const itemA = items[0];
268
+ const itemB = items[1];
269
+ const itemC = items[2];
270
+
271
+ // Move C between A and B
272
+ const res = await app.request(
273
+ `/api/collections/sidebar-items/${itemC?.id ?? ""}/move`,
274
+ {
275
+ method: "PUT",
276
+ headers: { "Content-Type": "application/json" },
277
+ body: JSON.stringify({
278
+ after: itemA?.id ?? "",
279
+ before: itemB?.id ?? "",
280
+ }),
281
+ },
282
+ );
283
+
284
+ expect(res.status).toBe(200);
285
+
286
+ const reordered = await services.collections.listSidebarItems();
287
+ expect(reordered[0]?.id).toBe(itemA?.id);
288
+ expect(reordered[1]?.id).toBe(itemC?.id);
289
+ expect(reordered[2]?.id).toBe(itemB?.id);
290
+ });
291
+ });
292
+
250
293
  describe("POST /api/collections/:id/posts", () => {
251
294
  it("adds a post to a collection", async () => {
252
295
  const { app, services } = createTestApp({ authenticated: true });
@@ -258,7 +301,7 @@ describe("Collections API Routes", () => {
258
301
  });
259
302
  const post = await services.posts.create({
260
303
  format: "note",
261
- body: "test",
304
+ bodyMarkdown: "test",
262
305
  });
263
306
 
264
307
  const res = await app.request(`/api/collections/${col.id}/posts`, {
@@ -279,14 +322,17 @@ describe("Collections API Routes", () => {
279
322
 
280
323
  const post = await services.posts.create({
281
324
  format: "note",
282
- body: "test",
325
+ bodyMarkdown: "test",
283
326
  });
284
327
 
285
- const res = await app.request("/api/collections/9999/posts", {
286
- method: "POST",
287
- headers: { "Content-Type": "application/json" },
288
- body: JSON.stringify({ postId: post.id }),
289
- });
328
+ const res = await app.request(
329
+ `/api/collections/${"00000000-0000-0000-0000-000000009999"}/posts`,
330
+ {
331
+ method: "POST",
332
+ headers: { "Content-Type": "application/json" },
333
+ body: JSON.stringify({ postId: post.id }),
334
+ },
335
+ );
290
336
 
291
337
  expect(res.status).toBe(404);
292
338
  });
@@ -303,7 +349,9 @@ describe("Collections API Routes", () => {
303
349
  const res = await app.request(`/api/collections/${col.id}/posts`, {
304
350
  method: "POST",
305
351
  headers: { "Content-Type": "application/json" },
306
- body: JSON.stringify({ postId: 1 }),
352
+ body: JSON.stringify({
353
+ postId: "00000000-0000-0000-0000-000000000001",
354
+ }),
307
355
  });
308
356
 
309
357
  expect(res.status).toBe(401);
@@ -321,7 +369,7 @@ describe("Collections API Routes", () => {
321
369
  });
322
370
  const post = await services.posts.create({
323
371
  format: "note",
324
- body: "test",
372
+ bodyMarkdown: "test",
325
373
  });
326
374
 
327
375
  await services.collections.addPost(col.id, post.id);
@@ -92,21 +92,8 @@ describe("Nav Items API Routes", () => {
92
92
  });
93
93
  });
94
94
 
95
- describe("PUT /api/nav-items/reorder", () => {
96
- it("returns 401 when not authenticated", async () => {
97
- const { app } = createTestApp({ authenticated: false });
98
- app.route("/api/nav-items", navItemsApiRoutes);
99
-
100
- const res = await app.request("/api/nav-items/reorder", {
101
- method: "PUT",
102
- headers: { "Content-Type": "application/json" },
103
- body: JSON.stringify({ ids: [1, 2] }),
104
- });
105
-
106
- expect(res.status).toBe(401);
107
- });
108
-
109
- it("reorders nav items when authenticated", async () => {
95
+ describe("PUT /api/nav-items/:id/move", () => {
96
+ it("moves a nav item between two others", async () => {
110
97
  const { app, services } = createTestApp({ authenticated: true });
111
98
  app.route("/api/nav-items", navItemsApiRoutes);
112
99
 
@@ -120,18 +107,44 @@ describe("Nav Items API Routes", () => {
120
107
  label: "Second",
121
108
  url: "/second",
122
109
  });
110
+ const item3 = await services.navItems.create({
111
+ type: "link",
112
+ label: "Third",
113
+ url: "/third",
114
+ });
123
115
 
124
- // Reverse order
125
- const res = await app.request("/api/nav-items/reorder", {
116
+ // Move Third between First and Second
117
+ const res = await app.request(`/api/nav-items/${item3.id}/move`, {
126
118
  method: "PUT",
127
119
  headers: { "Content-Type": "application/json" },
128
- body: JSON.stringify({ ids: [item2.id, item1.id] }),
120
+ body: JSON.stringify({
121
+ after: item1.id,
122
+ before: item2.id,
123
+ }),
129
124
  });
130
125
 
131
126
  expect(res.status).toBe(200);
132
- const body = await res.json();
133
- expect(body.navItems[0].label).toBe("Second");
134
- expect(body.navItems[1].label).toBe("First");
127
+
128
+ const items = await services.navItems.list();
129
+ expect(items[0]?.label).toBe("First");
130
+ expect(items[1]?.label).toBe("Third");
131
+ expect(items[2]?.label).toBe("Second");
132
+ });
133
+
134
+ it("returns 404 for non-existent item", async () => {
135
+ const { app } = createTestApp({ authenticated: true });
136
+ app.route("/api/nav-items", navItemsApiRoutes);
137
+
138
+ const res = await app.request(
139
+ `/api/nav-items/${"00000000-0000-0000-0000-000000009999"}/move`,
140
+ {
141
+ method: "PUT",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: JSON.stringify({ after: null, before: null }),
144
+ },
145
+ );
146
+
147
+ expect(res.status).toBe(404);
135
148
  });
136
149
  });
137
150
 
@@ -161,11 +174,14 @@ describe("Nav Items API Routes", () => {
161
174
  const { app } = createTestApp({ authenticated: true });
162
175
  app.route("/api/nav-items", navItemsApiRoutes);
163
176
 
164
- const res = await app.request("/api/nav-items/9999", {
165
- method: "PUT",
166
- headers: { "Content-Type": "application/json" },
167
- body: JSON.stringify({ label: "test" }),
168
- });
177
+ const res = await app.request(
178
+ `/api/nav-items/${"00000000-0000-0000-0000-000000009999"}`,
179
+ {
180
+ method: "PUT",
181
+ headers: { "Content-Type": "application/json" },
182
+ body: JSON.stringify({ label: "test" }),
183
+ },
184
+ );
169
185
 
170
186
  expect(res.status).toBe(404);
171
187
  });
@@ -212,9 +228,10 @@ describe("Nav Items API Routes", () => {
212
228
  const { app } = createTestApp({ authenticated: true });
213
229
  app.route("/api/nav-items", navItemsApiRoutes);
214
230
 
215
- const res = await app.request("/api/nav-items/9999", {
216
- method: "DELETE",
217
- });
231
+ const res = await app.request(
232
+ `/api/nav-items/${"00000000-0000-0000-0000-000000009999"}`,
233
+ { method: "DELETE" },
234
+ );
218
235
 
219
236
  expect(res.status).toBe(404);
220
237
  });