@jant/core 0.3.36 → 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 (271) 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/url-FWFqPJPb.js +1 -0
  6. package/dist/client/client.css +1 -1
  7. package/dist/client/client.js +4012 -3276
  8. package/dist/index.js +10285 -5809
  9. package/package.json +11 -3
  10. package/src/__tests__/helpers/app.ts +9 -9
  11. package/src/__tests__/helpers/db.ts +91 -93
  12. package/src/app.tsx +157 -27
  13. package/src/auth.ts +20 -2
  14. package/src/client/archive-nav.js +187 -0
  15. package/src/client/audio-player.ts +478 -0
  16. package/src/client/audio-processor.ts +84 -0
  17. package/src/client/avatar-upload.ts +3 -2
  18. package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
  19. package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
  20. package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
  21. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  22. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  23. package/src/client/components/collection-sidebar-types.ts +7 -9
  24. package/src/client/components/compose-types.ts +101 -4
  25. package/src/client/components/jant-collection-form.ts +43 -7
  26. package/src/client/components/jant-collection-sidebar.ts +88 -84
  27. package/src/client/components/jant-compose-dialog.ts +1655 -219
  28. package/src/client/components/jant-compose-editor.ts +732 -168
  29. package/src/client/components/jant-compose-fullscreen.ts +23 -78
  30. package/src/client/components/jant-media-lightbox.ts +2 -0
  31. package/src/client/components/jant-nav-manager.ts +24 -284
  32. package/src/client/components/jant-post-form.ts +89 -9
  33. package/src/client/components/jant-post-menu.ts +1019 -0
  34. package/src/client/components/jant-settings-avatar.ts +3 -3
  35. package/src/client/components/jant-settings-general.ts +38 -4
  36. package/src/client/components/jant-text-preview.ts +232 -0
  37. package/src/client/components/nav-manager-types.ts +4 -19
  38. package/src/client/components/post-form-template.ts +107 -12
  39. package/src/client/components/post-form-types.ts +11 -4
  40. package/src/client/compose-bridge.ts +410 -109
  41. package/src/client/image-processor.ts +26 -8
  42. package/src/client/media-metadata.ts +247 -0
  43. package/src/client/multipart-upload.ts +160 -0
  44. package/src/client/post-form-bridge.ts +52 -1
  45. package/src/client/settings-bridge.ts +0 -12
  46. package/src/client/thread-context.ts +140 -0
  47. package/src/client/tiptap/create-editor.ts +46 -0
  48. package/src/client/tiptap/extensions.ts +5 -0
  49. package/src/client/tiptap/image-node.ts +2 -8
  50. package/src/client/tiptap/paste-image.ts +2 -13
  51. package/src/client/tiptap/slash-commands.ts +173 -63
  52. package/src/client/toast.ts +101 -3
  53. package/src/client/types/sortablejs.d.ts +15 -0
  54. package/src/client/upload-with-metadata.ts +54 -0
  55. package/src/client/video-processor.ts +207 -0
  56. package/src/client.ts +5 -2
  57. package/src/db/__tests__/migrations.test.ts +118 -0
  58. package/src/db/index.ts +52 -0
  59. package/src/db/migrations/0000_baseline.sql +269 -0
  60. package/src/db/migrations/0001_fts_setup.sql +31 -0
  61. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  62. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  63. package/src/db/migrations/meta/_journal.json +4 -39
  64. package/src/db/schema.ts +409 -145
  65. package/src/i18n/__tests__/detect.test.ts +115 -0
  66. package/src/i18n/context.tsx +2 -2
  67. package/src/i18n/detect.ts +85 -1
  68. package/src/i18n/i18n.ts +1 -1
  69. package/src/i18n/index.ts +2 -1
  70. package/src/i18n/locales/en.po +487 -1217
  71. package/src/i18n/locales/en.ts +1 -1
  72. package/src/i18n/locales/zh-Hans.po +613 -996
  73. package/src/i18n/locales/zh-Hans.ts +1 -1
  74. package/src/i18n/locales/zh-Hant.po +624 -1007
  75. package/src/i18n/locales/zh-Hant.ts +1 -1
  76. package/src/i18n/middleware.ts +6 -0
  77. package/src/index.ts +5 -7
  78. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  79. package/src/lib/__tests__/constants.test.ts +0 -1
  80. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  81. package/src/lib/__tests__/nanoid.test.ts +26 -0
  82. package/src/lib/__tests__/schemas.test.ts +181 -63
  83. package/src/lib/__tests__/slug.test.ts +126 -0
  84. package/src/lib/__tests__/sse.test.ts +6 -6
  85. package/src/lib/__tests__/summary.test.ts +264 -0
  86. package/src/lib/__tests__/theme.test.ts +1 -1
  87. package/src/lib/__tests__/timeline.test.ts +33 -30
  88. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  89. package/src/lib/__tests__/view.test.ts +141 -66
  90. package/src/lib/blurhash-placeholder.ts +102 -0
  91. package/src/lib/constants.ts +3 -1
  92. package/src/lib/emoji-catalog.ts +885 -68
  93. package/src/lib/errors.ts +11 -8
  94. package/src/lib/feed.ts +78 -32
  95. package/src/lib/html.ts +2 -1
  96. package/src/lib/icon-catalog.ts +5033 -1
  97. package/src/lib/icons.ts +3 -2
  98. package/src/lib/index.ts +0 -1
  99. package/src/lib/markdown-to-tiptap.ts +286 -0
  100. package/src/lib/media-helpers.ts +12 -3
  101. package/src/lib/nanoid.ts +29 -0
  102. package/src/lib/navigation.ts +1 -1
  103. package/src/lib/render.tsx +20 -2
  104. package/src/lib/resolve-config.ts +6 -2
  105. package/src/lib/schemas.ts +224 -55
  106. package/src/lib/search-snippet.ts +34 -0
  107. package/src/lib/slug.ts +96 -0
  108. package/src/lib/sse.ts +6 -6
  109. package/src/lib/storage.ts +115 -7
  110. package/src/lib/summary.ts +66 -0
  111. package/src/lib/theme.ts +11 -8
  112. package/src/lib/timeline.ts +74 -34
  113. package/src/lib/tiptap-render.ts +5 -10
  114. package/src/lib/tiptap-to-markdown.ts +305 -0
  115. package/src/lib/upload.ts +190 -29
  116. package/src/lib/url.ts +31 -0
  117. package/src/lib/view.ts +204 -37
  118. package/src/middleware/__tests__/auth.test.ts +191 -11
  119. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  120. package/src/middleware/auth.ts +63 -9
  121. package/src/middleware/onboarding.ts +1 -1
  122. package/src/middleware/secure-headers.ts +40 -0
  123. package/src/preset.css +45 -2
  124. package/src/routes/__tests__/compose.test.ts +17 -24
  125. package/src/routes/api/__tests__/collections.test.ts +109 -61
  126. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  127. package/src/routes/api/__tests__/posts.test.ts +132 -68
  128. package/src/routes/api/__tests__/search.test.ts +15 -2
  129. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  130. package/src/routes/api/collections.ts +51 -42
  131. package/src/routes/api/custom-urls.ts +80 -0
  132. package/src/routes/api/export.ts +31 -0
  133. package/src/routes/api/nav-items.ts +23 -19
  134. package/src/routes/api/posts.ts +43 -39
  135. package/src/routes/api/search.ts +3 -4
  136. package/src/routes/api/upload-multipart.ts +245 -0
  137. package/src/routes/api/upload.ts +85 -19
  138. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  139. package/src/routes/auth/setup.tsx +26 -33
  140. package/src/routes/auth/signin.tsx +3 -7
  141. package/src/routes/compose.tsx +10 -55
  142. package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
  143. package/src/routes/dash/custom-urls.tsx +414 -0
  144. package/src/routes/dash/settings.tsx +304 -232
  145. package/src/routes/feed/__tests__/rss.test.ts +27 -28
  146. package/src/routes/feed/rss.ts +6 -4
  147. package/src/routes/feed/sitemap.ts +2 -12
  148. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  149. package/src/routes/pages/__tests__/featured.test.ts +41 -22
  150. package/src/routes/pages/archive.tsx +175 -39
  151. package/src/routes/pages/collection.tsx +22 -10
  152. package/src/routes/pages/collections.tsx +3 -3
  153. package/src/routes/pages/featured.tsx +28 -4
  154. package/src/routes/pages/home.tsx +16 -15
  155. package/src/routes/pages/latest.tsx +1 -11
  156. package/src/routes/pages/new.tsx +39 -0
  157. package/src/routes/pages/page.tsx +95 -49
  158. package/src/routes/pages/search.tsx +1 -1
  159. package/src/services/__tests__/api-token.test.ts +135 -0
  160. package/src/services/__tests__/collection.test.ts +275 -227
  161. package/src/services/__tests__/custom-url.test.ts +213 -0
  162. package/src/services/__tests__/media.test.ts +162 -22
  163. package/src/services/__tests__/navigation.test.ts +109 -68
  164. package/src/services/__tests__/post-timeline.test.ts +205 -32
  165. package/src/services/__tests__/post.test.ts +713 -234
  166. package/src/services/__tests__/search.test.ts +67 -10
  167. package/src/services/api-token.ts +166 -0
  168. package/src/services/auth.ts +17 -2
  169. package/src/services/collection.ts +397 -131
  170. package/src/services/custom-url.ts +188 -0
  171. package/src/services/export.ts +802 -0
  172. package/src/services/index.ts +26 -19
  173. package/src/services/media.ts +100 -22
  174. package/src/services/navigation.ts +158 -47
  175. package/src/services/path.ts +339 -0
  176. package/src/services/post.ts +687 -154
  177. package/src/services/search.ts +160 -75
  178. package/src/styles/components.css +58 -7
  179. package/src/styles/tokens.css +84 -6
  180. package/src/styles/ui.css +2964 -457
  181. package/src/types/bindings.ts +4 -1
  182. package/src/types/config.ts +12 -4
  183. package/src/types/constants.ts +15 -3
  184. package/src/types/entities.ts +74 -35
  185. package/src/types/operations.ts +11 -24
  186. package/src/types/props.ts +51 -16
  187. package/src/types/views.ts +45 -22
  188. package/src/ui/color-themes.ts +133 -23
  189. package/src/ui/compose/ComposeDialog.tsx +239 -17
  190. package/src/ui/compose/ComposePrompt.tsx +1 -1
  191. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  192. package/src/ui/dash/ListItemRow.tsx +1 -1
  193. package/src/ui/dash/StatusBadge.tsx +3 -1
  194. package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
  195. package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
  196. package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
  197. package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
  198. package/src/ui/dash/index.ts +0 -3
  199. package/src/ui/dash/settings/AccountContent.tsx +3 -57
  200. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  201. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  202. package/src/ui/dash/settings/AvatarContent.tsx +8 -0
  203. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  204. package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
  205. package/src/ui/feed/LinkCard.tsx +89 -40
  206. package/src/ui/feed/NoteCard.tsx +39 -25
  207. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  208. package/src/ui/feed/QuoteCard.tsx +38 -23
  209. package/src/ui/feed/ThreadPreview.tsx +90 -26
  210. package/src/ui/feed/TimelineFeed.tsx +3 -2
  211. package/src/ui/feed/TimelineItem.tsx +15 -6
  212. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  213. package/src/ui/feed/thread-preview-state.ts +61 -0
  214. package/src/ui/font-themes.ts +2 -2
  215. package/src/ui/layouts/BaseLayout.tsx +2 -2
  216. package/src/ui/layouts/SiteLayout.tsx +105 -92
  217. package/src/ui/pages/ArchivePage.tsx +923 -98
  218. package/src/ui/pages/ComposePage.tsx +54 -0
  219. package/src/ui/pages/PostPage.tsx +30 -45
  220. package/src/ui/pages/SearchPage.tsx +181 -37
  221. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  222. package/src/ui/shared/CollectionsSidebar.tsx +47 -37
  223. package/src/ui/shared/MediaGallery.tsx +445 -149
  224. package/src/ui/shared/PostFooter.tsx +204 -0
  225. package/src/ui/shared/StarRating.tsx +27 -0
  226. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  227. package/src/ui/shared/index.ts +0 -1
  228. package/dist/client/assets/url-8Dj-5CLW.js +0 -1
  229. package/src/client/media-upload.ts +0 -161
  230. package/src/client/page-slug-bridge.ts +0 -42
  231. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  232. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  233. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  234. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  235. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  236. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  237. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  238. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  239. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  240. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  241. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  242. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  243. package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
  244. package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
  245. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  246. package/src/lib/__tests__/sqid.test.ts +0 -65
  247. package/src/lib/sqid.ts +0 -79
  248. package/src/routes/api/__tests__/pages.test.ts +0 -218
  249. package/src/routes/api/pages.ts +0 -73
  250. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  251. package/src/routes/dash/index.tsx +0 -109
  252. package/src/routes/dash/media.tsx +0 -135
  253. package/src/routes/dash/pages.tsx +0 -245
  254. package/src/routes/dash/posts.tsx +0 -338
  255. package/src/routes/dash/redirects.tsx +0 -263
  256. package/src/routes/pages/post.tsx +0 -59
  257. package/src/services/__tests__/page.test.ts +0 -298
  258. package/src/services/__tests__/path-registry.test.ts +0 -165
  259. package/src/services/__tests__/redirect.test.ts +0 -159
  260. package/src/services/page.ts +0 -216
  261. package/src/services/path-registry.ts +0 -160
  262. package/src/services/redirect.ts +0 -97
  263. package/src/ui/dash/PageForm.tsx +0 -187
  264. package/src/ui/dash/PostList.tsx +0 -95
  265. package/src/ui/dash/media/MediaListContent.tsx +0 -206
  266. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  267. package/src/ui/dash/pages/PagesContent.tsx +0 -75
  268. package/src/ui/dash/posts/PostForm.tsx +0 -260
  269. package/src/ui/layouts/DashLayout.tsx +0 -247
  270. package/src/ui/pages/SinglePage.tsx +0 -23
  271. package/src/ui/shared/ThreadView.tsx +0 -136
@@ -54,6 +54,7 @@ const labels: ComposeLabels = {
54
54
  collection: "Collection",
55
55
  searchCollections: "Search...",
56
56
  noCollections: "No collections found.",
57
+ emptyCollections: "Create a collection to get started.",
57
58
  post: "Post",
58
59
  addAlt: "+ ALT",
59
60
  addAltTitle: "Add alt text",
@@ -62,7 +63,54 @@ const labels: ComposeLabels = {
62
63
  addMore: "Add",
63
64
  uploading: "Uploading...",
64
65
  published: "Published!",
65
- retryAll: "Click to retry all",
66
+ view: "View",
67
+ retryAll: "Tap to retry",
68
+ editPost: "Edit post",
69
+ update: "Done",
70
+ confirmCloseTitle: "Save to drafts?",
71
+ confirmCloseSubtitle: "Save to drafts to edit and post at a later time.",
72
+ confirmCloseSave: "Save",
73
+ confirmCloseCancel: "Cancel",
74
+ confirmCloseDiscard: "Don't save",
75
+ confirmEditTitle: "You have unsaved changes",
76
+ confirmEditSubtitle: "Do you want to publish your changes or discard them?",
77
+ confirmEditPublish: "Publish",
78
+ confirmEditDiscard: "Discard",
79
+ drafts: "Drafts",
80
+ draftsEmpty: "No drafts yet. Save a draft to find it here.",
81
+ deleteDraft: "Delete Draft",
82
+ draftDeleted: "Draft deleted.",
83
+ publishFailedDraft: "Couldn't publish. Saved as draft.",
84
+ uploadFailedDraft: "Some uploads failed. Saved as draft.",
85
+ addCollection: "Add Collection",
86
+ collectionCountLabel: "%name% + %count% more",
87
+ draftRestored: "Draft restored.",
88
+ reply: "Reply",
89
+ publishFeatured: "Post as Featured",
90
+ publishUnlisted: "Post Unlisted",
91
+ publishPrivate: "Post as Private",
92
+ showMore: "Show more",
93
+ showLess: "Show less",
94
+ collectionFormLabels: {
95
+ titleLabel: "Title",
96
+ titlePlaceholder: "My Collection",
97
+ slugLabel: "Slug",
98
+ slugHelp: "URL-safe identifier",
99
+ descriptionLabel: "Description (optional)",
100
+ descriptionPlaceholder: "What's this collection about?",
101
+ removeIcon: "Remove",
102
+ iconsTab: "Icons",
103
+ emojisTab: "Emojis",
104
+ searchIconsPlaceholder: "Search icons...",
105
+ searchEmojisPlaceholder: "Search emojis...",
106
+ sortOrderLabel: "Sort Order",
107
+ sortNewest: "Newest first",
108
+ sortOldest: "Oldest first",
109
+ sortRatingDesc: "Highest rated",
110
+ sortRatingAsc: "Lowest rated",
111
+ submitLabel: "Save",
112
+ cancelLabel: "Cancel",
113
+ },
66
114
  };
67
115
 
68
116
  async function createElement(
@@ -160,11 +208,13 @@ describe("JantComposeEditor", () => {
160
208
  expect(el._rating).toBe(0);
161
209
  });
162
210
 
163
- it("dispatches attached panel open event", async () => {
211
+ it("dispatches attached panel open event and creates new item", async () => {
164
212
  const el = await createElement("note");
165
213
 
166
- const events: Event[] = [];
167
- el.addEventListener("jant:attached-panel-open", (e) => events.push(e));
214
+ const events: CustomEvent[] = [];
215
+ el.addEventListener("jant:attached-panel-open", (e) =>
216
+ events.push(e as CustomEvent),
217
+ );
168
218
 
169
219
  // Click attached text tool button
170
220
  const toolBtns =
@@ -178,7 +228,9 @@ describe("JantComposeEditor", () => {
178
228
  await el.updateComplete;
179
229
 
180
230
  expect(events).toHaveLength(1);
181
- expect(el._showAttachedText).toBe(true);
231
+ expect(events[0].detail.index).toBe(0);
232
+ expect(el._attachedTexts).toHaveLength(1);
233
+ expect(el._attachedTexts[0].bodyJson).toBeNull();
182
234
  });
183
235
 
184
236
  it("shows title toggle only in note mode", async () => {
@@ -202,13 +254,29 @@ describe("JantComposeEditor", () => {
202
254
  ],
203
255
  };
204
256
  el._rating = 4;
205
- el._attachedText = "Some attached text";
257
+ el._attachedTexts = [
258
+ {
259
+ clientId: "t1",
260
+ bodyJson: {
261
+ type: "doc",
262
+ content: [
263
+ {
264
+ type: "paragraph",
265
+ content: [{ type: "text", text: "Some attached text" }],
266
+ },
267
+ ],
268
+ },
269
+ summary: "Some attached text",
270
+ bodyHtml: "<p>Some attached text</p>",
271
+ },
272
+ ];
206
273
 
207
274
  const data = el.getData();
208
275
  expect(data.title).toBe("Test Title");
209
276
  expect(data.body).toContain("Test Body");
210
277
  expect(data.rating).toBe(4);
211
- expect(data.attachedText).toBe("Some attached text");
278
+ expect(data.attachedTexts).toHaveLength(1);
279
+ expect(data.attachedTexts[0].bodyJson).not.toBeNull();
212
280
  expect(data.url).toBe("");
213
281
  expect(data.quoteText).toBe("");
214
282
  expect(data.quoteAuthor).toBe("");
@@ -252,8 +320,22 @@ describe("JantComposeEditor", () => {
252
320
  };
253
321
  el._rating = 3;
254
322
  el._showRating = true;
255
- el._attachedText = "text";
256
- el._showAttachedText = true;
323
+ el._attachedTexts = [
324
+ {
325
+ clientId: "t1",
326
+ bodyJson: {
327
+ type: "doc",
328
+ content: [
329
+ {
330
+ type: "paragraph",
331
+ content: [{ type: "text", text: "text" }],
332
+ },
333
+ ],
334
+ },
335
+ summary: "text",
336
+ bodyHtml: "<p>text</p>",
337
+ },
338
+ ];
257
339
 
258
340
  el.reset();
259
341
 
@@ -261,18 +343,33 @@ describe("JantComposeEditor", () => {
261
343
  expect(el._bodyJson).toBeNull();
262
344
  expect(el._rating).toBe(0);
263
345
  expect(el._showRating).toBe(false);
264
- expect(el._attachedText).toBe("");
265
- expect(el._showAttachedText).toBe(false);
346
+ expect(el._attachedTexts).toEqual([]);
266
347
  });
267
348
 
268
- it("shows attached text badge when text is present", async () => {
349
+ it("shows attached text card in attachment strip", async () => {
269
350
  const el = await createElement("note");
270
- el._attachedText = "Some content here";
351
+ el._attachedTexts = [
352
+ {
353
+ clientId: "t1",
354
+ bodyJson: {
355
+ type: "doc",
356
+ content: [
357
+ {
358
+ type: "paragraph",
359
+ content: [{ type: "text", text: "Some content here" }],
360
+ },
361
+ ],
362
+ },
363
+ summary: "Some content here",
364
+ bodyHtml: "<p>Some content here</p>",
365
+ },
366
+ ];
367
+ el._attachmentOrder = ["t1"];
271
368
  await el.updateComplete;
272
369
 
273
- const badge = el.querySelector(".compose-attached-badge");
274
- expect(badge).not.toBeNull();
275
- expect(badge?.textContent).toContain("chars");
370
+ const card = el.querySelector(".compose-attachment-text-card");
371
+ expect(card).not.toBeNull();
372
+ expect(card?.textContent).toContain("Some content here");
276
373
  });
277
374
 
278
375
  it("media button shows inline add label when attachments are present", async () => {
@@ -291,9 +388,12 @@ describe("JantComposeEditor", () => {
291
388
  file,
292
389
  previewUrl: URL.createObjectURL(blob),
293
390
  status: "done",
391
+ progress: null,
294
392
  mediaId: "m1",
295
393
  alt: "",
296
394
  error: null,
395
+ summary: null,
396
+ chars: null,
297
397
  },
298
398
  ];
299
399
  await el.updateComplete;
@@ -309,4 +409,96 @@ describe("JantComposeEditor", () => {
309
409
  expect(label).not.toBeNull();
310
410
  expect(label?.textContent).toBe("Add");
311
411
  });
412
+
413
+ it("moves attachments later with keyboard controls", async () => {
414
+ const el = await createElement("note");
415
+ const blob = new Blob(["fake"], { type: "image/png" });
416
+ const file = new File([blob], "test.png", { type: "image/png" });
417
+ el._attachments = [
418
+ {
419
+ clientId: "a1",
420
+ file,
421
+ previewUrl: URL.createObjectURL(blob),
422
+ status: "done",
423
+ progress: null,
424
+ mediaId: "m1",
425
+ alt: "",
426
+ error: null,
427
+ summary: null,
428
+ chars: null,
429
+ },
430
+ {
431
+ clientId: "a2",
432
+ file,
433
+ previewUrl: URL.createObjectURL(blob),
434
+ status: "done",
435
+ progress: null,
436
+ mediaId: "m2",
437
+ alt: "",
438
+ error: null,
439
+ summary: null,
440
+ chars: null,
441
+ },
442
+ ];
443
+ el._attachmentOrder = ["a1", "a2"];
444
+ await el.updateComplete;
445
+
446
+ const attachment = requireElement(
447
+ el.querySelector<HTMLElement>(
448
+ '[data-attachment-id="a1"] [data-attachment-sortable]',
449
+ ),
450
+ "expected attachment card",
451
+ );
452
+ attachment.dispatchEvent(
453
+ new globalThis.KeyboardEvent("keydown", {
454
+ key: "ArrowRight",
455
+ bubbles: true,
456
+ }),
457
+ );
458
+ await el.updateComplete;
459
+
460
+ expect(el._attachmentOrder).toEqual(["a2", "a1"]);
461
+ });
462
+
463
+ it("preserves mixed attachment order when populate provides one", async () => {
464
+ const el = await createElement("note");
465
+
466
+ el.populate({
467
+ format: "note",
468
+ media: [
469
+ {
470
+ id: "m1",
471
+ previewUrl: "/a.png",
472
+ mimeType: "image/png",
473
+ },
474
+ ],
475
+ textAttachments: [
476
+ {
477
+ clientId: "t1",
478
+ bodyJson: JSON.stringify({
479
+ type: "doc",
480
+ content: [
481
+ {
482
+ type: "paragraph",
483
+ content: [{ type: "text", text: "Text attachment" }],
484
+ },
485
+ ],
486
+ }),
487
+ bodyHtml: "<p>Text attachment</p>",
488
+ summary: "Text attachment",
489
+ },
490
+ ],
491
+ attachmentOrder: ["t1", "m1"],
492
+ });
493
+ await el.updateComplete;
494
+
495
+ const items = [
496
+ ...el.querySelectorAll<HTMLElement>("[data-attachment-id]"),
497
+ ].map((item) => item.dataset.attachmentId);
498
+
499
+ expect(items).toHaveLength(2);
500
+ expect(items[0]).toBe(el._attachmentOrder[0]);
501
+ expect(items[1]).toBe(el._attachmentOrder[1]);
502
+ expect(el._attachmentOrder[0]).toBe("t1");
503
+ });
312
504
  });
@@ -18,6 +18,9 @@ const labels: PostFormLabels = {
18
18
  quoteOption: "Quote",
19
19
  titleLabel: "Title",
20
20
  titlePlaceholder: "Title...",
21
+ slugLabel: "Slug",
22
+ slugPlaceholder: "auto-generated",
23
+ slugHelp: "Auto-generated from title",
21
24
  bodyLabel: "Body",
22
25
  bodyPlaceholder: "Body...",
23
26
  urlLabel: "URL",
@@ -32,8 +35,7 @@ const labels: PostFormLabels = {
32
35
  statusPublished: "Published",
33
36
  statusDraft: "Draft",
34
37
  visibilityLabel: "Visibility",
35
- visibilityListed: "Listed",
36
- visibilityFeatured: "Featured",
38
+ visibilityPublic: "Public",
37
39
  visibilityUnlisted: "Unlisted",
38
40
  pinnedLabel: "Pinned",
39
41
  collectionsLabel: "Collections",
@@ -44,16 +46,18 @@ const labels: PostFormLabels = {
44
46
  mediaDialogLoading: "Loading...",
45
47
  submitSuccessMessage: "Saved!",
46
48
  submitErrorMessage: "Failed.",
49
+ draftFallbackMessage: "Couldn't publish. Saved as draft.",
47
50
  };
48
51
 
49
52
  const initial: PostFormInitial = {
50
53
  format: "note",
51
54
  title: "",
55
+ slug: "",
52
56
  body: "",
53
57
  url: "",
54
58
  quoteText: "",
55
59
  status: "published",
56
- visibility: "listed",
60
+ visibility: "public",
57
61
  pinned: false,
58
62
  rating: 0,
59
63
  collectionIds: [],
@@ -66,7 +70,13 @@ const collections: PostCollectionOption[] = [
66
70
  ];
67
71
 
68
72
  const media: PostMediaItem[] = [
69
- { id: "m1", thumbUrl: "https://cdn.example.com/m1.jpg", alt: "Media 1" },
73
+ {
74
+ id: "m1",
75
+ thumbUrl: "https://cdn.example.com/m1.jpg",
76
+ alt: "Media 1",
77
+ mimeType: "image/jpeg",
78
+ originalName: "photo.jpg",
79
+ },
70
80
  ];
71
81
 
72
82
  async function createElement(
@@ -77,7 +87,7 @@ async function createElement(
77
87
  el.initial = { ...initial };
78
88
  el.collections = [...collections];
79
89
  el.media = [...media];
80
- el.action = "/dash/posts";
90
+ el.action = "/compose";
81
91
  Object.assign(el, overrides);
82
92
  document.body.appendChild(el);
83
93
  await el.updateComplete;
@@ -131,12 +141,12 @@ describe("JantPostForm", () => {
131
141
  };
132
142
  el._body = JSON.stringify(el._bodyJson);
133
143
 
134
- // Set visibility to "featured" via the select dropdown
144
+ // Set visibility to "unlisted" via the select dropdown
135
145
  const visibilitySelect =
136
146
  el.querySelectorAll<HTMLSelectElement>("select.select")[2]; // [0]=format, [1]=status, [2]=visibility
137
147
  expect(visibilitySelect).not.toBeNull();
138
148
  if (!visibilitySelect) throw new Error("Visibility select not found");
139
- visibilitySelect.value = "featured";
149
+ visibilitySelect.value = "unlisted";
140
150
  visibilitySelect.dispatchEvent(new Event("change", { bubbles: true }));
141
151
 
142
152
  const checkboxList =
@@ -162,10 +172,10 @@ describe("JantPostForm", () => {
162
172
 
163
173
  expect(detail).not.toBeNull();
164
174
  const d = detail as unknown as PostSubmitDetail;
165
- expect(d.endpoint).toBe("/dash/posts");
175
+ expect(d.endpoint).toBe("/compose");
166
176
  expect(d.data.title).toBe("Sample Post");
167
177
  expect(d.data.body).toContain("Hello world");
168
- expect(d.data.visibility).toBe("featured");
178
+ expect(d.data.visibility).toBe("unlisted");
169
179
  expect(d.data.collectionIds).toEqual([collections[0].id]);
170
180
  expect(d.data.mediaIds).toEqual(["m1"]);
171
181
  });
@@ -179,7 +179,7 @@ describe("JantSettingsAvatar", () => {
179
179
 
180
180
  expect(detail).not.toBeNull();
181
181
  const d = detail as unknown as SettingsSaveDetail;
182
- expect(d.endpoint).toBe("/dash/settings/avatar/display");
182
+ expect(d.endpoint).toBe("/settings/avatar/display");
183
183
  expect(d.section).toBe("avatar-display");
184
184
  expect(d.data.showHeaderAvatar).toBe("true");
185
185
  });
@@ -201,7 +201,7 @@ describe("JantSettingsAvatar", () => {
201
201
 
202
202
  expect(detail).not.toBeNull();
203
203
  const d = detail as unknown as AvatarRemoveDetail;
204
- expect(d.endpoint).toBe("/dash/settings/avatar/remove");
204
+ expect(d.endpoint).toBe("/settings/avatar/remove");
205
205
  });
206
206
 
207
207
  it("saved() resets dirty state", async () => {
@@ -188,7 +188,7 @@ describe("JantSettingsGeneral", () => {
188
188
 
189
189
  expect(detail).not.toBeNull();
190
190
  const d = detail as unknown as SettingsSaveDetail;
191
- expect(d.endpoint).toBe("/dash/settings/general");
191
+ expect(d.endpoint).toBe("/settings/general");
192
192
  expect(d.section).toBe("general");
193
193
  expect(d.data.siteName).toBe("New Name");
194
194
  });
@@ -255,7 +255,7 @@ describe("JantSettingsGeneral", () => {
255
255
 
256
256
  expect(detail).not.toBeNull();
257
257
  const d = detail as unknown as SettingsSaveDetail;
258
- expect(d.endpoint).toBe("/dash/settings/general");
258
+ expect(d.endpoint).toBe("/settings/general");
259
259
  expect(d.section).toBe("general");
260
260
  expect(d.data.siteFooter).toBe("New footer");
261
261
  });
@@ -286,7 +286,7 @@ describe("JantSettingsGeneral", () => {
286
286
 
287
287
  expect(detail).not.toBeNull();
288
288
  const d = detail as unknown as SettingsSaveDetail;
289
- expect(d.endpoint).toBe("/dash/settings/general/seo");
289
+ expect(d.endpoint).toBe("/settings/general/seo");
290
290
  expect(d.section).toBe("seo");
291
291
  });
292
292
 
@@ -25,21 +25,19 @@ export interface CollectionSidebarLabels {
25
25
  }
26
26
 
27
27
  export interface SidebarCollection {
28
- id: number;
28
+ id: string;
29
29
  slug: string;
30
30
  title: string;
31
31
  description: string | null;
32
32
  icon: string | null;
33
33
  sortOrder: string;
34
- position: number;
35
34
  postCount: number;
36
35
  }
37
36
 
38
- export interface SidebarDivider {
39
- id: number;
40
- position: number;
37
+ export interface ClientSidebarItem {
38
+ id: string;
39
+ type: "collection" | "divider";
40
+ collectionId: string | null;
41
+ position: string;
42
+ collection?: SidebarCollection;
41
43
  }
42
-
43
- export type SidebarItem =
44
- | { kind: "collection"; data: SidebarCollection }
45
- | { kind: "divider"; data: SidebarDivider };
@@ -5,16 +5,74 @@
5
5
  * Lit Web Components, and the compose bridge script.
6
6
  */
7
7
 
8
+ import type { JSONContent } from "@tiptap/core";
9
+ import type { CollectionFormLabels } from "./collection-types.js";
10
+
8
11
  export type ComposeFormat = "note" | "link" | "quote";
9
12
 
10
13
  export interface ComposeAttachment {
11
14
  clientId: string;
12
15
  file: File;
13
16
  previewUrl: string;
14
- status: "pending" | "uploading" | "done" | "error";
17
+ status: "pending" | "processing" | "uploading" | "done" | "error";
18
+ progress: number | null;
15
19
  mediaId: string | null;
16
20
  alt: string;
17
21
  error: string | null;
22
+ /** Text content preview for text files (first ~100 chars) */
23
+ summary: string | null;
24
+ /** Character count of text content */
25
+ chars: number | null;
26
+ }
27
+
28
+ export interface AttachedTextItem {
29
+ clientId: string;
30
+ bodyJson: JSONContent | null;
31
+ /** Pre-rendered HTML from TipTap, used for preview on the public page */
32
+ bodyHtml: string;
33
+ summary: string;
34
+ /** Set for already-persisted text media items (edit mode) */
35
+ mediaId?: string;
36
+ }
37
+
38
+ export interface DraftItem {
39
+ id: string;
40
+ format: ComposeFormat;
41
+ title: string | null;
42
+ bodyText: string | null;
43
+ bodyHtml: string | null;
44
+ url: string | null;
45
+ quoteText: string | null;
46
+ replyToId: string | null;
47
+ updatedAt: number;
48
+ mediaAttachments: {
49
+ id: string;
50
+ previewUrl: string;
51
+ alt: string | null;
52
+ mimeType: string;
53
+ }[];
54
+ }
55
+
56
+ export interface LocalDraft {
57
+ format: ComposeFormat;
58
+ title: string;
59
+ bodyJson: JSONContent | null;
60
+ url: string;
61
+ quoteText: string;
62
+ quoteAuthor: string;
63
+ rating: number;
64
+ showTitle: boolean;
65
+ showRating: boolean;
66
+ collectionIds: string[];
67
+ replyToId: string | null;
68
+ attachedTexts: Array<{
69
+ clientId: string;
70
+ bodyJson: JSONContent | null;
71
+ bodyHtml: string;
72
+ summary: string;
73
+ }>;
74
+ attachmentOrder?: string[];
75
+ savedAt: number;
18
76
  }
19
77
 
20
78
  export interface ComposeLabels {
@@ -44,6 +102,7 @@ export interface ComposeLabels {
44
102
  collection: string;
45
103
  searchCollections: string;
46
104
  noCollections: string;
105
+ emptyCollections: string;
47
106
  post: string;
48
107
  addAlt: string;
49
108
  addAltTitle: string;
@@ -52,9 +111,39 @@ export interface ComposeLabels {
52
111
  addMore: string;
53
112
  uploading: string;
54
113
  published: string;
114
+ view: string;
55
115
  retryAll: string;
116
+ editPost: string;
117
+ update: string;
118
+ confirmCloseTitle: string;
119
+ confirmCloseSubtitle: string;
120
+ confirmCloseSave: string;
121
+ confirmCloseCancel: string;
122
+ confirmCloseDiscard: string;
123
+ confirmEditTitle: string;
124
+ confirmEditSubtitle: string;
125
+ confirmEditPublish: string;
126
+ confirmEditDiscard: string;
127
+ drafts: string;
128
+ draftsEmpty: string;
129
+ deleteDraft: string;
130
+ draftDeleted: string;
131
+ publishFailedDraft: string;
132
+ uploadFailedDraft: string;
133
+ addCollection: string;
134
+ collectionCountLabel: string;
135
+ draftRestored: string;
136
+ reply: string;
137
+ publishFeatured: string;
138
+ publishUnlisted: string;
139
+ publishPrivate: string;
140
+ showMore: string;
141
+ showLess: string;
142
+ collectionFormLabels: CollectionFormLabels;
56
143
  }
57
144
 
145
+ export type ComposeVisibility = "public" | "unlisted" | "private";
146
+
58
147
  export interface ComposeSubmitDetail {
59
148
  format: ComposeFormat;
60
149
  title: string;
@@ -63,15 +152,23 @@ export interface ComposeSubmitDetail {
63
152
  quoteText: string;
64
153
  quoteAuthor: string;
65
154
  status: "published" | "draft";
155
+ visibility: ComposeVisibility;
66
156
  rating: number;
67
- collectionIds: number[];
157
+ collectionIds: string[];
68
158
  mediaIds: string[];
69
159
  mediaAlts: Record<string, string>;
70
- attachedText: string;
160
+ attachedTexts: AttachedTextItem[];
161
+ /** Interleaved order of media clientIds + text clientIds */
162
+ attachmentOrder: string[];
163
+ /** clientId → mediaId for already-uploaded file attachments (captured at submit time) */
164
+ mediaClientMap: Record<string, string>;
165
+ featured?: boolean;
166
+ editPostId?: string;
167
+ replyToId?: string;
71
168
  }
72
169
 
73
170
  export interface ComposeCollection {
74
- id: number;
171
+ id: string;
75
172
  title: string;
76
173
  iconHtml: string;
77
174
  }