@jant/core 0.3.36 → 0.3.38

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
@@ -1,6 +1,6 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
- import { describe, it, expect, beforeEach } from "vitest";
3
+ import { describe, it, expect, beforeEach, vi } from "vitest";
4
4
  import type {
5
5
  ComposeLabels,
6
6
  ComposeCollection,
@@ -48,6 +48,7 @@ const labels: ComposeLabels = {
48
48
  collection: "Collection",
49
49
  searchCollections: "Search...",
50
50
  noCollections: "No collections found.",
51
+ emptyCollections: "Create a collection to get started.",
51
52
  post: "Post",
52
53
  addAlt: "+ ALT",
53
54
  addAltTitle: "Add alt text",
@@ -56,12 +57,59 @@ const labels: ComposeLabels = {
56
57
  addMore: "Add",
57
58
  uploading: "Uploading...",
58
59
  published: "Published!",
59
- retryAll: "Click to retry all",
60
+ view: "View",
61
+ retryAll: "Tap to retry",
62
+ editPost: "Edit post",
63
+ update: "Done",
64
+ confirmCloseTitle: "Save to drafts?",
65
+ confirmCloseSubtitle: "Save to drafts to edit and post at a later time.",
66
+ confirmCloseSave: "Save",
67
+ confirmCloseCancel: "Cancel",
68
+ confirmCloseDiscard: "Don't save",
69
+ confirmEditTitle: "You have unsaved changes",
70
+ confirmEditSubtitle: "Do you want to publish your changes or discard them?",
71
+ confirmEditPublish: "Publish",
72
+ confirmEditDiscard: "Discard",
73
+ drafts: "Drafts",
74
+ draftsEmpty: "No drafts yet. Save a draft to find it here.",
75
+ deleteDraft: "Delete Draft",
76
+ draftDeleted: "Draft deleted.",
77
+ publishFailedDraft: "Couldn't publish. Saved as draft.",
78
+ uploadFailedDraft: "Some uploads failed. Saved as draft.",
79
+ addCollection: "Add Collection",
80
+ collectionCountLabel: "%name% + %count% more",
81
+ draftRestored: "Draft restored.",
82
+ reply: "Reply",
83
+ publishFeatured: "Post as Featured",
84
+ publishUnlisted: "Post Unlisted",
85
+ publishPrivate: "Post as Private",
86
+ showMore: "Show more",
87
+ showLess: "Show less",
88
+ collectionFormLabels: {
89
+ titleLabel: "Title",
90
+ titlePlaceholder: "My Collection",
91
+ slugLabel: "Slug",
92
+ slugHelp: "URL-safe identifier",
93
+ descriptionLabel: "Description (optional)",
94
+ descriptionPlaceholder: "What's this collection about?",
95
+ removeIcon: "Remove",
96
+ iconsTab: "Icons",
97
+ emojisTab: "Emojis",
98
+ searchIconsPlaceholder: "Search icons...",
99
+ searchEmojisPlaceholder: "Search emojis...",
100
+ sortOrderLabel: "Sort Order",
101
+ sortNewest: "Newest first",
102
+ sortOldest: "Oldest first",
103
+ sortRatingDesc: "Highest rated",
104
+ sortRatingAsc: "Lowest rated",
105
+ submitLabel: "Save",
106
+ cancelLabel: "Cancel",
107
+ },
60
108
  };
61
109
 
62
110
  const collections: ComposeCollection[] = [
63
- { id: 1, title: "Books", iconHtml: "" },
64
- { id: 2, title: "Movies", iconHtml: "<span>🎬</span>" },
111
+ { id: "col-1", title: "Books", iconHtml: "" },
112
+ { id: "col-2", title: "Movies", iconHtml: "<span>🎬</span>" },
65
113
  ];
66
114
 
67
115
  async function createElement(
@@ -96,9 +144,9 @@ describe("JantComposeDialog", () => {
96
144
  expect(segmentedItems[1].textContent?.trim()).toBe("Link");
97
145
  expect(segmentedItems[2].textContent?.trim()).toBe("Quote");
98
146
 
99
- // Post button present
147
+ // Post button present (split button with visibility dropdown)
100
148
  const postBtn = requireElement(
101
- el.querySelector<HTMLButtonElement>(".compose-post-btn"),
149
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
102
150
  "expected post button",
103
151
  );
104
152
  expect(postBtn.textContent?.trim()).toBe("Post");
@@ -131,7 +179,7 @@ describe("JantComposeDialog", () => {
131
179
  );
132
180
  });
133
181
 
134
- it("submit dispatches jant:compose-submit with correct payload", async () => {
182
+ it("submit dispatches jant:compose-submit-deferred with correct payload", async () => {
135
183
  const el = await createElement();
136
184
  const editor = requireElement(
137
185
  el.querySelector<JantComposeEditor>("jant-compose-editor"),
@@ -145,26 +193,34 @@ describe("JantComposeDialog", () => {
145
193
  };
146
194
  await editor.updateComplete;
147
195
 
148
- let receivedDetail: ComposeSubmitDetail | null = null;
149
- el.addEventListener("jant:compose-submit", (event) => {
150
- const customEvent = event as CustomEvent<ComposeSubmitDetail>;
196
+ let receivedDetail:
197
+ | (ComposeSubmitDetail & { pendingAttachments: unknown[] })
198
+ | null = null;
199
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
200
+ const customEvent = event as CustomEvent<
201
+ ComposeSubmitDetail & { pendingAttachments: unknown[] }
202
+ >;
151
203
  receivedDetail = customEvent.detail;
152
204
  });
153
205
 
154
206
  // Click post button
155
207
  requireElement(
156
- el.querySelector<HTMLButtonElement>(".compose-post-btn"),
208
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
157
209
  "expected post button",
158
210
  ).click();
159
211
 
160
212
  expect(receivedDetail).not.toBeNull();
161
- const detail = receivedDetail as unknown as ComposeSubmitDetail;
213
+ const detail = receivedDetail as unknown as ComposeSubmitDetail & {
214
+ pendingAttachments: unknown[];
215
+ };
162
216
  expect(detail.format).toBe("note");
163
217
  expect(detail.body).toContain("Hello world");
164
218
  expect(detail.status).toBe("published");
219
+ expect(detail.visibility).toBe("public");
165
220
  expect(detail.collectionIds).toEqual([]);
166
221
  expect(detail.mediaIds).toEqual([]);
167
222
  expect(detail.mediaAlts).toEqual({});
223
+ expect(detail.pendingAttachments).toEqual([]);
168
224
  });
169
225
 
170
226
  it("collection selector toggles IDs", async () => {
@@ -186,30 +242,32 @@ describe("JantComposeDialog", () => {
186
242
  // Select first collection
187
243
  options[0].click();
188
244
  await el.updateComplete;
189
- expect(el._collectionIds).toEqual([1]);
245
+ expect(el._collectionIds).toEqual(["col-1"]);
190
246
 
191
247
  // Select second collection
192
248
  options[1].click();
193
249
  await el.updateComplete;
194
- expect(el._collectionIds).toEqual([1, 2]);
250
+ expect(el._collectionIds).toEqual(["col-1", "col-2"]);
195
251
 
196
252
  // Deselect first
197
253
  options[0].click();
198
254
  await el.updateComplete;
199
- expect(el._collectionIds).toEqual([2]);
255
+ expect(el._collectionIds).toEqual(["col-2"]);
200
256
  });
201
257
 
202
258
  it("reset restores initial state", async () => {
203
259
  const el = await createElement();
204
260
  el._format = "link";
205
- el._collectionIds = [1, 2];
261
+ el._collectionIds = ["col-1", "col-2"];
206
262
  el._loading = true;
263
+ el._draftSourceId = "abc123";
207
264
 
208
265
  el.reset();
209
266
 
210
267
  expect(el._format).toBe("note");
211
268
  expect(el._collectionIds).toEqual([]);
212
269
  expect(el._loading).toBe(false);
270
+ expect(el._draftSourceId).toBeNull();
213
271
  });
214
272
 
215
273
  it("loading state disables submit button", async () => {
@@ -218,23 +276,22 @@ describe("JantComposeDialog", () => {
218
276
  await el.updateComplete;
219
277
 
220
278
  const postBtn = requireElement(
221
- el.querySelector<HTMLButtonElement>(".compose-post-btn"),
279
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
222
280
  "expected post button",
223
281
  );
224
282
  expect(postBtn.disabled).toBe(true);
225
283
  });
226
284
 
227
- it("renders without collections", async () => {
285
+ it("renders collection selector even without collections", async () => {
228
286
  const el = await createElement([]);
229
287
 
230
- // No collection trigger
231
- expect(el.querySelector(".compose-collection-trigger")).toBeNull();
232
- // Spacer div present instead
288
+ // Collection trigger is still shown so users can create new collections
289
+ expect(el.querySelector(".compose-collection-trigger")).not.toBeNull();
233
290
  const actionRow = el.querySelector(".compose-action-row");
234
291
  expect(actionRow).not.toBeNull();
235
292
  });
236
293
 
237
- it("draft button dispatches submit with draft status", async () => {
294
+ it("draft button with content shows confirm panel", async () => {
238
295
  const el = await createElement();
239
296
  const editor = requireElement(
240
297
  el.querySelector<JantComposeEditor>("jant-compose-editor"),
@@ -251,22 +308,47 @@ describe("JantComposeDialog", () => {
251
308
  };
252
309
  await editor.updateComplete;
253
310
 
254
- let receivedDetail: ComposeSubmitDetail | null = null;
255
- el.addEventListener("jant:compose-submit", (event) => {
256
- const customEvent = event as CustomEvent<ComposeSubmitDetail>;
257
- receivedDetail = customEvent.detail;
258
- });
311
+ // Click the draft header button — should show confirm panel
312
+ const draftBtn = requireElement(
313
+ el.querySelector<HTMLButtonElement>(".compose-dialog-header-btn"),
314
+ "expected draft button",
315
+ );
316
+ draftBtn.click();
317
+ await el.updateComplete;
259
318
 
260
- // Click the draft header button
319
+ expect(el._confirmPanelOpen).toBe(true);
320
+ expect(el.querySelector(".compose-confirm-panel")).not.toBeNull();
321
+ });
322
+
323
+ it("draft button without content opens drafts panel", async () => {
324
+ const el = await createElement();
325
+
326
+ // Mock fetch for drafts list
327
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
328
+ new Response(JSON.stringify({ posts: [] }), {
329
+ status: 200,
330
+ headers: { "Content-Type": "application/json" },
331
+ }),
332
+ );
333
+
334
+ // Click the draft header button — should open drafts panel
261
335
  const draftBtn = requireElement(
262
336
  el.querySelector<HTMLButtonElement>(".compose-dialog-header-btn"),
263
337
  "expected draft button",
264
338
  );
265
339
  draftBtn.click();
340
+ await el.updateComplete;
266
341
 
267
- expect(receivedDetail).not.toBeNull();
268
- const detail = receivedDetail as unknown as ComposeSubmitDetail;
269
- expect(detail.status).toBe("draft");
342
+ expect(el._draftsPanelOpen).toBe(true);
343
+
344
+ // Wait for fetch to resolve
345
+ await new Promise((r) => setTimeout(r, 0));
346
+ await el.updateComplete;
347
+
348
+ expect(el._draftsLoading).toBe(false);
349
+ expect(el.querySelector(".compose-drafts-panel")).not.toBeNull();
350
+
351
+ fetchSpy.mockRestore();
270
352
  });
271
353
 
272
354
  it("does not dispatch submit when loading", async () => {
@@ -275,12 +357,12 @@ describe("JantComposeDialog", () => {
275
357
  await el.updateComplete;
276
358
 
277
359
  let dispatched = false;
278
- el.addEventListener("jant:compose-submit", () => {
360
+ el.addEventListener("jant:compose-submit-deferred", () => {
279
361
  dispatched = true;
280
362
  });
281
363
 
282
364
  const postBtn = requireElement(
283
- el.querySelector<HTMLButtonElement>(".compose-post-btn"),
365
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
284
366
  "expected post button",
285
367
  );
286
368
  postBtn.click();
@@ -293,7 +375,7 @@ describe("JantComposeDialog", () => {
293
375
  el._loading = true;
294
376
  await el.updateComplete;
295
377
 
296
- const spinner = el.querySelector(".compose-post-btn .animate-spin");
378
+ const spinner = el.querySelector(".compose-publish-main .animate-spin");
297
379
  expect(spinner).not.toBeNull();
298
380
  });
299
381
 
@@ -322,11 +404,15 @@ describe("JantComposeDialog", () => {
322
404
  file,
323
405
  previewUrl,
324
406
  status: "done",
407
+ progress: null,
325
408
  mediaId: "media-1",
326
409
  alt: "",
327
410
  error: null,
411
+ summary: null,
412
+ chars: null,
328
413
  },
329
414
  ];
415
+ editor._attachmentOrder = ["test-id-1"];
330
416
  await editor.updateComplete;
331
417
 
332
418
  // Thumbnail strip should be visible
@@ -361,11 +447,15 @@ describe("JantComposeDialog", () => {
361
447
  file,
362
448
  previewUrl,
363
449
  status: "done",
450
+ progress: null,
364
451
  mediaId: "media-1",
365
452
  alt: "",
366
453
  error: null,
454
+ summary: null,
455
+ chars: null,
367
456
  },
368
457
  ];
458
+ editor._attachmentOrder = ["test-id-1"];
369
459
  await editor.updateComplete;
370
460
 
371
461
  // Click remove button
@@ -398,11 +488,15 @@ describe("JantComposeDialog", () => {
398
488
  file,
399
489
  previewUrl,
400
490
  status: "done",
491
+ progress: null,
401
492
  mediaId: "media-1",
402
493
  alt: "",
403
494
  error: null,
495
+ summary: null,
496
+ chars: null,
404
497
  },
405
498
  ];
499
+ editor._attachmentOrder = ["test-id-1"];
406
500
  await editor.updateComplete;
407
501
 
408
502
  // Click ALT button
@@ -448,9 +542,12 @@ describe("JantComposeDialog", () => {
448
542
  file,
449
543
  previewUrl,
450
544
  status: "done",
545
+ progress: null,
451
546
  mediaId: "media-1",
452
547
  alt: "A test image",
453
548
  error: null,
549
+ summary: null,
550
+ chars: null,
454
551
  },
455
552
  ];
456
553
  editor._bodyJson = {
@@ -464,21 +561,28 @@ describe("JantComposeDialog", () => {
464
561
  };
465
562
  await editor.updateComplete;
466
563
 
467
- let receivedDetail: ComposeSubmitDetail | null = null;
468
- el.addEventListener("jant:compose-submit", (event) => {
469
- const customEvent = event as CustomEvent<ComposeSubmitDetail>;
564
+ let receivedDetail:
565
+ | (ComposeSubmitDetail & { pendingAttachments: unknown[] })
566
+ | null = null;
567
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
568
+ const customEvent = event as CustomEvent<
569
+ ComposeSubmitDetail & { pendingAttachments: unknown[] }
570
+ >;
470
571
  receivedDetail = customEvent.detail;
471
572
  });
472
573
 
473
574
  requireElement(
474
- el.querySelector<HTMLButtonElement>(".compose-post-btn"),
575
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
475
576
  "expected post button",
476
577
  ).click();
477
578
 
478
579
  expect(receivedDetail).not.toBeNull();
479
- const detail = receivedDetail as unknown as ComposeSubmitDetail;
580
+ const detail = receivedDetail as unknown as ComposeSubmitDetail & {
581
+ pendingAttachments: unknown[];
582
+ };
480
583
  expect(detail.mediaIds).toEqual(["media-1"]);
481
584
  expect(detail.mediaAlts).toEqual({ "media-1": "A test image" });
585
+ expect(detail.pendingAttachments).toEqual([]);
482
586
 
483
587
  URL.revokeObjectURL(previewUrl);
484
588
  });
@@ -500,9 +604,12 @@ describe("JantComposeDialog", () => {
500
604
  file,
501
605
  previewUrl,
502
606
  status: "uploading",
607
+ progress: null,
503
608
  mediaId: null,
504
609
  alt: "Alt for pending",
505
610
  error: null,
611
+ summary: null,
612
+ chars: null,
506
613
  },
507
614
  ];
508
615
  editor._bodyJson = {
@@ -521,24 +628,513 @@ describe("JantComposeDialog", () => {
521
628
  deferredEvent = event as CustomEvent;
522
629
  });
523
630
 
524
- // Prevent dialog.close() from throwing (no parent dialog in test)
525
- let submitEvent: CustomEvent | null = null;
526
- el.addEventListener("jant:compose-submit", (event) => {
527
- submitEvent = event as CustomEvent;
528
- });
529
-
530
631
  requireElement(
531
- el.querySelector<HTMLButtonElement>(".compose-post-btn"),
632
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
532
633
  "expected post button",
533
634
  ).click();
534
635
 
535
- // Should have dispatched deferred, not regular submit
536
636
  expect(deferredEvent).not.toBeNull();
537
- expect(submitEvent).toBeNull();
538
637
  expect(
539
638
  (deferredEvent as unknown as CustomEvent).detail.pendingAttachments,
540
- ).toBeDefined();
639
+ ).toHaveLength(1);
541
640
 
542
641
  URL.revokeObjectURL(previewUrl);
543
642
  });
643
+
644
+ // ── Close confirmation ─────────────────────────────────────────────
645
+
646
+ it("requestClose on empty form closes immediately without confirmation", async () => {
647
+ const el = await createElement();
648
+
649
+ // Ensure no confirmation panel appears
650
+ el.requestClose();
651
+ await el.updateComplete;
652
+
653
+ expect(el._confirmPanelOpen).toBe(false);
654
+ expect(el.querySelector(".compose-confirm-panel")).toBeNull();
655
+ });
656
+
657
+ it("beforeunload does not warn when dialog was only opened", async () => {
658
+ const el = await createElement();
659
+ vi.spyOn(el, "closest").mockReturnValue({
660
+ open: true,
661
+ addEventListener: vi.fn(),
662
+ removeEventListener: vi.fn(),
663
+ } as unknown as HTMLDialogElement);
664
+
665
+ const event = new Event("beforeunload", {
666
+ cancelable: true,
667
+ }) as globalThis.BeforeUnloadEvent;
668
+
669
+ window.dispatchEvent(event);
670
+
671
+ expect(event.defaultPrevented).toBe(false);
672
+ expect(
673
+ (
674
+ el as unknown as { _hasUnsavedChanges: () => boolean }
675
+ )._hasUnsavedChanges(),
676
+ ).toBe(false);
677
+ });
678
+
679
+ it("beforeunload warns after compose content changes", async () => {
680
+ const el = await createElement();
681
+ vi.spyOn(el, "closest").mockReturnValue({
682
+ open: true,
683
+ addEventListener: vi.fn(),
684
+ removeEventListener: vi.fn(),
685
+ } as unknown as HTMLDialogElement);
686
+ const editor = requireElement(
687
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
688
+ "expected compose editor",
689
+ );
690
+
691
+ editor._bodyJson = {
692
+ type: "doc",
693
+ content: [
694
+ { type: "paragraph", content: [{ type: "text", text: "Unsaved" }] },
695
+ ],
696
+ };
697
+ await editor.updateComplete;
698
+
699
+ const event = new Event("beforeunload", {
700
+ cancelable: true,
701
+ }) as globalThis.BeforeUnloadEvent;
702
+
703
+ window.dispatchEvent(event);
704
+
705
+ expect(event.defaultPrevented).toBe(true);
706
+ });
707
+
708
+ it("requestClose with content shows confirmation panel", async () => {
709
+ const el = await createElement();
710
+ const editor = requireElement(
711
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
712
+ "expected compose editor",
713
+ );
714
+ editor._bodyJson = {
715
+ type: "doc",
716
+ content: [
717
+ { type: "paragraph", content: [{ type: "text", text: "Some text" }] },
718
+ ],
719
+ };
720
+ await editor.updateComplete;
721
+
722
+ el.requestClose();
723
+ await el.updateComplete;
724
+
725
+ expect(el._confirmPanelOpen).toBe(true);
726
+ expect(el.querySelector(".compose-confirm-panel")).not.toBeNull();
727
+ expect(
728
+ el.querySelector(".compose-confirm-title")?.textContent?.trim(),
729
+ ).toBe("Save to drafts?");
730
+ });
731
+
732
+ it("confirm save draft dispatches submit-deferred with draft status", async () => {
733
+ const el = await createElement();
734
+ const editor = requireElement(
735
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
736
+ "expected compose editor",
737
+ );
738
+ editor._bodyJson = {
739
+ type: "doc",
740
+ content: [
741
+ { type: "paragraph", content: [{ type: "text", text: "Draft me" }] },
742
+ ],
743
+ };
744
+ await editor.updateComplete;
745
+
746
+ el.requestClose();
747
+ await el.updateComplete;
748
+
749
+ let receivedDetail: ComposeSubmitDetail | null = null;
750
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
751
+ receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
752
+ });
753
+
754
+ const saveBtn = requireElement(
755
+ el.querySelector<HTMLButtonElement>(".compose-confirm-save"),
756
+ "expected save draft button",
757
+ );
758
+ saveBtn.click();
759
+ await el.updateComplete;
760
+
761
+ expect(receivedDetail).not.toBeNull();
762
+ expect((receivedDetail as unknown as ComposeSubmitDetail).status).toBe(
763
+ "draft",
764
+ );
765
+ expect(el._confirmPanelOpen).toBe(false);
766
+ });
767
+
768
+ it("confirm cancel returns to editor without closing", async () => {
769
+ const el = await createElement();
770
+ const editor = requireElement(
771
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
772
+ "expected compose editor",
773
+ );
774
+ editor._bodyJson = {
775
+ type: "doc",
776
+ content: [
777
+ {
778
+ type: "paragraph",
779
+ content: [{ type: "text", text: "Keep editing" }],
780
+ },
781
+ ],
782
+ };
783
+ await editor.updateComplete;
784
+
785
+ el.requestClose();
786
+ await el.updateComplete;
787
+
788
+ expect(el._confirmPanelOpen).toBe(true);
789
+
790
+ const cancelBtn = requireElement(
791
+ el.querySelector<HTMLButtonElement>(".compose-confirm-cancel"),
792
+ "expected cancel button",
793
+ );
794
+ const focusSpy = vi.spyOn(editor, "focusInput");
795
+ cancelBtn.click();
796
+ await el.updateComplete;
797
+
798
+ expect(el._confirmPanelOpen).toBe(false);
799
+ expect(focusSpy).toHaveBeenCalled();
800
+ // Editor content should be preserved
801
+ expect(editor._bodyJson).toEqual({
802
+ type: "doc",
803
+ content: [
804
+ {
805
+ type: "paragraph",
806
+ content: [{ type: "text", text: "Keep editing" }],
807
+ },
808
+ ],
809
+ });
810
+ });
811
+
812
+ it("requestClose on confirm panel dismisses it (Escape = Cancel)", async () => {
813
+ const el = await createElement();
814
+ const editor = requireElement(
815
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
816
+ "expected compose editor",
817
+ );
818
+ editor._bodyJson = {
819
+ type: "doc",
820
+ content: [
821
+ { type: "paragraph", content: [{ type: "text", text: "Esc test" }] },
822
+ ],
823
+ };
824
+ await editor.updateComplete;
825
+
826
+ el.requestClose();
827
+ await el.updateComplete;
828
+ expect(el._confirmPanelOpen).toBe(true);
829
+
830
+ // Second requestClose (same path as Escape via dialog oncancel)
831
+ el.requestClose();
832
+ await el.updateComplete;
833
+
834
+ expect(el._confirmPanelOpen).toBe(false);
835
+ // Content should be preserved (not discarded)
836
+ expect(editor._bodyJson).toEqual({
837
+ type: "doc",
838
+ content: [
839
+ { type: "paragraph", content: [{ type: "text", text: "Esc test" }] },
840
+ ],
841
+ });
842
+ });
843
+
844
+ it("confirm discard closes and resets", async () => {
845
+ const el = await createElement();
846
+ const editor = requireElement(
847
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
848
+ "expected compose editor",
849
+ );
850
+ editor._bodyJson = {
851
+ type: "doc",
852
+ content: [
853
+ {
854
+ type: "paragraph",
855
+ content: [{ type: "text", text: "Will discard" }],
856
+ },
857
+ ],
858
+ };
859
+ await editor.updateComplete;
860
+
861
+ el.requestClose();
862
+ await el.updateComplete;
863
+
864
+ const discardBtn = requireElement(
865
+ el.querySelector<HTMLButtonElement>(".compose-confirm-discard"),
866
+ "expected discard button",
867
+ );
868
+ discardBtn.click();
869
+ await el.updateComplete;
870
+
871
+ expect(el._confirmPanelOpen).toBe(false);
872
+ expect(el._format).toBe("note");
873
+ expect(el._collectionIds).toEqual([]);
874
+ });
875
+
876
+ it("loaded draft shows format switcher and Post button, not edit mode", async () => {
877
+ const el = await createElement();
878
+
879
+ // Simulate what _loadDraft sets (without fetching)
880
+ el._draftSourceId = "draft123";
881
+ el._format = "note";
882
+ await el.updateComplete;
883
+
884
+ // Format switcher should be visible (not "Edit post" title)
885
+ expect(el.querySelector(".compose-segmented")).not.toBeNull();
886
+ expect(el.querySelector(".compose-dialog-title")).toBeNull();
887
+
888
+ // Button should say "Post", not "Done"
889
+ const postBtn = requireElement(
890
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
891
+ "expected post button",
892
+ );
893
+ expect(postBtn.textContent?.trim()).toBe("Post");
894
+ });
895
+
896
+ it("discard on loaded draft sends DELETE request", async () => {
897
+ const el = await createElement();
898
+ const editor = requireElement(
899
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
900
+ "expected compose editor",
901
+ );
902
+
903
+ // Simulate loaded draft with content
904
+ el._draftSourceId = "draft456";
905
+ editor._bodyJson = {
906
+ type: "doc",
907
+ content: [
908
+ {
909
+ type: "paragraph",
910
+ content: [{ type: "text", text: "Draft content" }],
911
+ },
912
+ ],
913
+ };
914
+ await editor.updateComplete;
915
+
916
+ const fetchSpy = vi
917
+ .spyOn(globalThis, "fetch")
918
+ .mockResolvedValue(new Response(null, { status: 200 }));
919
+
920
+ el.requestClose();
921
+ await el.updateComplete;
922
+
923
+ // Click "Don't save" (discard)
924
+ const discardBtn = requireElement(
925
+ el.querySelector<HTMLButtonElement>(".compose-confirm-discard"),
926
+ "expected discard button",
927
+ );
928
+ discardBtn.click();
929
+ await el.updateComplete;
930
+
931
+ expect(fetchSpy).toHaveBeenCalledWith("/api/posts/draft456", {
932
+ method: "DELETE",
933
+ });
934
+
935
+ fetchSpy.mockRestore();
936
+ });
937
+
938
+ it("submit from loaded draft includes draftSourceId as editPostId", async () => {
939
+ const el = await createElement();
940
+ const editor = requireElement(
941
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
942
+ "expected compose editor",
943
+ );
944
+
945
+ el._draftSourceId = "draft789";
946
+ editor._bodyJson = {
947
+ type: "doc",
948
+ content: [
949
+ {
950
+ type: "paragraph",
951
+ content: [{ type: "text", text: "Publish this draft" }],
952
+ },
953
+ ],
954
+ };
955
+ await editor.updateComplete;
956
+
957
+ let receivedDetail: ComposeSubmitDetail | null = null;
958
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
959
+ receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
960
+ });
961
+
962
+ requireElement(
963
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
964
+ "expected post button",
965
+ ).click();
966
+
967
+ expect(receivedDetail).not.toBeNull();
968
+ expect((receivedDetail as unknown as ComposeSubmitDetail).editPostId).toBe(
969
+ "draft789",
970
+ );
971
+ expect((receivedDetail as unknown as ComposeSubmitDetail).status).toBe(
972
+ "published",
973
+ );
974
+ });
975
+
976
+ it("draft button confirm save dispatches draft then opens drafts panel", async () => {
977
+ const el = await createElement();
978
+ const editor = requireElement(
979
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
980
+ "expected compose editor",
981
+ );
982
+ editor._bodyJson = {
983
+ type: "doc",
984
+ content: [
985
+ {
986
+ type: "paragraph",
987
+ content: [{ type: "text", text: "Save then browse" }],
988
+ },
989
+ ],
990
+ };
991
+ await editor.updateComplete;
992
+
993
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
994
+ new Response(JSON.stringify({ posts: [] }), {
995
+ status: 200,
996
+ headers: { "Content-Type": "application/json" },
997
+ }),
998
+ );
999
+
1000
+ let receivedDetail: ComposeSubmitDetail | null = null;
1001
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
1002
+ receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
1003
+ });
1004
+
1005
+ // Click draft button → confirm panel
1006
+ const draftBtn = requireElement(
1007
+ el.querySelector<HTMLButtonElement>(".compose-dialog-header-btn"),
1008
+ "expected draft button",
1009
+ );
1010
+ draftBtn.click();
1011
+ await el.updateComplete;
1012
+ expect(el._confirmPanelOpen).toBe(true);
1013
+
1014
+ // Click "Save"
1015
+ requireElement(
1016
+ el.querySelector<HTMLButtonElement>(".compose-confirm-save"),
1017
+ "expected save button",
1018
+ ).click();
1019
+ await el.updateComplete;
1020
+ await new Promise((r) => setTimeout(r, 0));
1021
+ await el.updateComplete;
1022
+
1023
+ // Draft submitted
1024
+ expect(receivedDetail).not.toBeNull();
1025
+ expect((receivedDetail as unknown as ComposeSubmitDetail).status).toBe(
1026
+ "draft",
1027
+ );
1028
+ // Drafts panel opened instead of dialog closing
1029
+ expect(el._draftsPanelOpen).toBe(true);
1030
+ expect(el._confirmPanelOpen).toBe(false);
1031
+
1032
+ fetchSpy.mockRestore();
1033
+ });
1034
+
1035
+ it("draft button confirm discard opens drafts panel without saving", async () => {
1036
+ const el = await createElement();
1037
+ const editor = requireElement(
1038
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
1039
+ "expected compose editor",
1040
+ );
1041
+ editor._bodyJson = {
1042
+ type: "doc",
1043
+ content: [
1044
+ {
1045
+ type: "paragraph",
1046
+ content: [{ type: "text", text: "Discard then browse" }],
1047
+ },
1048
+ ],
1049
+ };
1050
+ await editor.updateComplete;
1051
+
1052
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
1053
+ new Response(JSON.stringify({ posts: [] }), {
1054
+ status: 200,
1055
+ headers: { "Content-Type": "application/json" },
1056
+ }),
1057
+ );
1058
+
1059
+ let submitFired = false;
1060
+ el.addEventListener("jant:compose-submit-deferred", () => {
1061
+ submitFired = true;
1062
+ });
1063
+
1064
+ // Click draft button → confirm panel
1065
+ const draftBtn = requireElement(
1066
+ el.querySelector<HTMLButtonElement>(".compose-dialog-header-btn"),
1067
+ "expected draft button",
1068
+ );
1069
+ draftBtn.click();
1070
+ await el.updateComplete;
1071
+
1072
+ // Click "Don't save"
1073
+ requireElement(
1074
+ el.querySelector<HTMLButtonElement>(".compose-confirm-discard"),
1075
+ "expected discard button",
1076
+ ).click();
1077
+ await el.updateComplete;
1078
+ await new Promise((r) => setTimeout(r, 0));
1079
+ await el.updateComplete;
1080
+
1081
+ // No submit dispatched
1082
+ expect(submitFired).toBe(false);
1083
+ // Drafts panel opened
1084
+ expect(el._draftsPanelOpen).toBe(true);
1085
+ expect(el._confirmPanelOpen).toBe(false);
1086
+
1087
+ fetchSpy.mockRestore();
1088
+ });
1089
+
1090
+ it("attachments detected as content for confirmation", async () => {
1091
+ const el = await createElement();
1092
+ const editor = requireElement(
1093
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
1094
+ "expected compose editor",
1095
+ );
1096
+
1097
+ const blob = new Blob(["fake-image"], { type: "image/png" });
1098
+ const file = new File([blob], "test.png", { type: "image/png" });
1099
+ const previewUrl = URL.createObjectURL(blob);
1100
+
1101
+ editor._attachments = [
1102
+ {
1103
+ clientId: "test-id-1",
1104
+ file,
1105
+ previewUrl,
1106
+ status: "done",
1107
+ progress: null,
1108
+ mediaId: "media-1",
1109
+ alt: "",
1110
+ error: null,
1111
+ summary: null,
1112
+ chars: null,
1113
+ },
1114
+ ];
1115
+ await editor.updateComplete;
1116
+
1117
+ el.requestClose();
1118
+ await el.updateComplete;
1119
+
1120
+ expect(el._confirmPanelOpen).toBe(true);
1121
+ expect(el.querySelector(".compose-confirm-panel")).not.toBeNull();
1122
+
1123
+ URL.revokeObjectURL(previewUrl);
1124
+ });
1125
+
1126
+ it("rating detected as content for confirmation", async () => {
1127
+ const el = await createElement();
1128
+ const editor = requireElement(
1129
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
1130
+ "expected compose editor",
1131
+ );
1132
+ editor._rating = 3;
1133
+ await editor.updateComplete;
1134
+
1135
+ el.requestClose();
1136
+ await el.updateComplete;
1137
+
1138
+ expect(el._confirmPanelOpen).toBe(true);
1139
+ });
544
1140
  });