@jant/core 0.6.14 → 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/bin/commands/import-site.js +81 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-BxCOR3Uc.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
  18. package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
  47. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  48. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  49. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  50. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  51. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  52. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  53. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  54. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  55. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  56. package/src/client/components/collection-manager-types.ts +9 -3
  57. package/src/client/components/compose-types.ts +8 -0
  58. package/src/client/components/jant-collection-directory.ts +165 -30
  59. package/src/client/components/jant-command-palette.ts +2 -1
  60. package/src/client/components/jant-compose-dialog.ts +85 -21
  61. package/src/client/components/jant-compose-editor.ts +13 -7
  62. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  63. package/src/client/components/jant-config-editor.ts +722 -0
  64. package/src/client/components/jant-media-lightbox.ts +100 -18
  65. package/src/client/components/jant-nav-manager.ts +1283 -54
  66. package/src/client/components/jant-post-form.ts +3 -3
  67. package/src/client/components/jant-post-menu.ts +86 -74
  68. package/src/client/components/nav-manager-types.ts +51 -2
  69. package/src/client/components/post-form-types.ts +1 -1
  70. package/src/client/compose-bridge.ts +4 -1
  71. package/src/client/feed-video-player.ts +62 -2
  72. package/src/client/footnote-rail.ts +478 -0
  73. package/src/client/hydrate-partial.ts +2 -0
  74. package/src/client/media-lightbox-events.ts +63 -0
  75. package/src/client/site-header-fragment.ts +38 -0
  76. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  77. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  78. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  79. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  80. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  81. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  82. package/src/client/tiptap/bubble-menu.ts +88 -10
  83. package/src/client/tiptap/continuous-lists.ts +102 -0
  84. package/src/client/tiptap/create-editor.ts +3 -0
  85. package/src/client/tiptap/extensions.ts +14 -5
  86. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  87. package/src/client/tiptap/slash-commands.ts +8 -1
  88. package/src/client/tiptap/structural-keymap.ts +211 -0
  89. package/src/client/tiptap/tab-indent.ts +8 -22
  90. package/src/client/tiptap/table-control-labels.ts +30 -0
  91. package/src/client/tiptap/table-controls.ts +537 -0
  92. package/src/client/tiptap/table-size-picker.ts +254 -0
  93. package/src/client/toast.ts +8 -6
  94. package/src/client-auth.ts +1 -0
  95. package/src/client.ts +1 -0
  96. package/src/db/__tests__/d1-query.test.ts +43 -1
  97. package/src/db/__tests__/migration-runner.test.ts +63 -0
  98. package/src/db/__tests__/migrations.test.ts +469 -0
  99. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  100. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  101. package/src/db/migrations/0028_superb_post.sql +2 -0
  102. package/src/db/migrations/0029_boring_magma.sql +1 -0
  103. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  104. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  105. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  106. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  107. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  108. package/src/db/migrations/meta/_journal.json +28 -0
  109. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  110. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  111. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  112. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  113. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  114. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  115. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  116. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  117. package/src/db/migrations/pg/meta/_journal.json +28 -0
  118. package/src/db/pg/schema.ts +28 -20
  119. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  120. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  121. package/src/db/schema.ts +28 -20
  122. package/src/i18n/__tests__/detect.test.ts +8 -2
  123. package/src/i18n/detect.ts +14 -5
  124. package/src/i18n/locales/public/en.po +133 -30
  125. package/src/i18n/locales/public/en.ts +1 -1
  126. package/src/i18n/locales/public/zh-Hans.po +128 -25
  127. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  128. package/src/i18n/locales/public/zh-Hant.po +128 -25
  129. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  130. package/src/i18n/locales/settings/en.po +513 -10
  131. package/src/i18n/locales/settings/en.ts +1 -1
  132. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  133. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  134. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  135. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  136. package/src/lib/__tests__/api-settings.test.ts +299 -0
  137. package/src/lib/__tests__/constants.test.ts +5 -0
  138. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  139. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  140. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  141. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  142. package/src/lib/__tests__/markdown.test.ts +8 -6
  143. package/src/lib/__tests__/navigation.test.ts +59 -2
  144. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  145. package/src/lib/__tests__/post-display.test.ts +64 -0
  146. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  147. package/src/lib/__tests__/summary.test.ts +82 -0
  148. package/src/lib/__tests__/timeline.test.ts +109 -70
  149. package/src/lib/__tests__/timezones.test.ts +31 -3
  150. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  151. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  152. package/src/lib/__tests__/view.test.ts +53 -0
  153. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  154. package/src/lib/api-settings.ts +239 -12
  155. package/src/lib/collection-sort.ts +7 -5
  156. package/src/lib/constants.ts +2 -0
  157. package/src/lib/feed-path.ts +25 -0
  158. package/src/lib/feed-policy.ts +64 -0
  159. package/src/lib/footnotes.ts +264 -0
  160. package/src/lib/github-sync-site-config.ts +2 -0
  161. package/src/lib/hugo-markdown.ts +10 -3
  162. package/src/lib/jant-branding.ts +1 -0
  163. package/src/lib/markdown-manager.ts +188 -5
  164. package/src/lib/markdown.ts +10 -3
  165. package/src/lib/navigation.ts +16 -3
  166. package/src/lib/post-body-html.ts +131 -0
  167. package/src/lib/post-display.ts +7 -1
  168. package/src/lib/render.tsx +9 -2
  169. package/src/lib/resolve-config.ts +100 -16
  170. package/src/lib/schemas.ts +133 -1
  171. package/src/lib/settings-paths.ts +5 -0
  172. package/src/lib/site-skill.ts +78 -0
  173. package/src/lib/summary.ts +16 -5
  174. package/src/lib/timeline.ts +98 -122
  175. package/src/lib/timezones.ts +54 -14
  176. package/src/lib/tiptap-render.ts +355 -81
  177. package/src/lib/view.ts +30 -20
  178. package/src/lib/worker-response-cache.ts +11 -18
  179. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  180. package/src/middleware/config.ts +2 -2
  181. package/src/middleware/public-content-access.ts +87 -0
  182. package/src/node/__tests__/cli-export.test.ts +14 -0
  183. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  184. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  185. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  186. package/src/node/index.ts +2 -1
  187. package/src/preset.css +16 -2
  188. package/src/routes/__tests__/compose.test.ts +51 -0
  189. package/src/routes/__tests__/site-skill.test.ts +112 -0
  190. package/src/routes/api/__tests__/collections.test.ts +91 -19
  191. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  192. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  193. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  194. package/src/routes/api/__tests__/settings.test.ts +217 -0
  195. package/src/routes/api/collections.ts +22 -26
  196. package/src/routes/api/export.ts +2 -0
  197. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  198. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  199. package/src/routes/api/internal/post-body-html.ts +43 -0
  200. package/src/routes/api/internal/sites.ts +24 -0
  201. package/src/routes/api/nav-items.ts +24 -1
  202. package/src/routes/api/posts.ts +2 -2
  203. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  204. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  205. package/src/routes/api/public/archive.ts +4 -0
  206. package/src/routes/api/public/posts.ts +41 -24
  207. package/src/routes/api/search.ts +3 -0
  208. package/src/routes/api/settings.ts +33 -0
  209. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  210. package/src/routes/compose.tsx +2 -1
  211. package/src/routes/dash/settings.tsx +45 -2
  212. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  213. package/src/routes/feed/feed.ts +23 -14
  214. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  215. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  216. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  217. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  218. package/src/routes/pages/__tests__/search.test.ts +69 -0
  219. package/src/routes/pages/archive.tsx +27 -7
  220. package/src/routes/pages/collection.tsx +32 -19
  221. package/src/routes/pages/collections.tsx +6 -1
  222. package/src/routes/pages/page.tsx +83 -11
  223. package/src/routes/site-skill.ts +18 -0
  224. package/src/services/__tests__/auth.test.ts +7 -5
  225. package/src/services/__tests__/collection.test.ts +159 -44
  226. package/src/services/__tests__/navigation.test.ts +229 -21
  227. package/src/services/__tests__/post-timeline.test.ts +109 -30
  228. package/src/services/__tests__/post.test.ts +567 -5
  229. package/src/services/__tests__/search.test.ts +48 -1
  230. package/src/services/__tests__/settings.test.ts +8 -0
  231. package/src/services/__tests__/site-admin.test.ts +121 -1
  232. package/src/services/auth.ts +2 -2
  233. package/src/services/bootstrap.ts +3 -2
  234. package/src/services/collection.ts +226 -167
  235. package/src/services/export-theme/assets/client-site.css +1 -1
  236. package/src/services/export-theme/assets/client-site.js +46 -25
  237. package/src/services/export-theme/layouts/_default/list.html +18 -4
  238. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  239. package/src/services/export-theme/layouts/featured/list.html +5 -4
  240. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  241. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  242. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  243. package/src/services/export-theme/styles/main.css +30 -2
  244. package/src/services/export.ts +91 -80
  245. package/src/services/mcp.ts +37 -27
  246. package/src/services/media.ts +1 -1
  247. package/src/services/navigation.ts +185 -46
  248. package/src/services/post.ts +874 -271
  249. package/src/services/search.ts +8 -2
  250. package/src/services/settings.ts +2 -10
  251. package/src/services/site-admin.ts +63 -43
  252. package/src/style-cjk-jp.css +1 -1
  253. package/src/style-cjk-kr.css +1 -1
  254. package/src/style-cjk-tc.css +1 -1
  255. package/src/style-cjk.css +1 -1
  256. package/src/styles/components.css +540 -0
  257. package/src/styles/site-media.css +111 -19
  258. package/src/styles/tokens.css +14 -9
  259. package/src/styles/ui.css +829 -29
  260. package/src/types/bindings.ts +3 -0
  261. package/src/types/config.ts +243 -12
  262. package/src/types/constants.ts +22 -1
  263. package/src/types/entities.ts +9 -8
  264. package/src/types/operations.ts +18 -10
  265. package/src/types/props.ts +8 -2
  266. package/src/types/raw-assets.d.ts +5 -0
  267. package/src/types/views.ts +16 -4
  268. package/src/ui/__tests__/font-themes.test.ts +42 -30
  269. package/src/ui/compose/ComposeDialog.tsx +92 -2
  270. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  271. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  272. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  273. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  274. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  275. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  276. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  277. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  278. package/src/ui/feed/LinkCard.tsx +2 -12
  279. package/src/ui/feed/NoteCard.tsx +10 -20
  280. package/src/ui/feed/QuoteCard.tsx +2 -12
  281. package/src/ui/feed/ThreadPreview.tsx +26 -34
  282. package/src/ui/feed/TimelineFeed.tsx +2 -2
  283. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  284. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  285. package/src/ui/feed/thread-preview-state.ts +5 -7
  286. package/src/ui/font-themes.ts +74 -26
  287. package/src/ui/layouts/BaseLayout.tsx +22 -8
  288. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  289. package/src/ui/pages/CollectionPage.tsx +101 -22
  290. package/src/ui/pages/CollectionsPage.tsx +3 -1
  291. package/src/ui/pages/PostPage.tsx +39 -12
  292. package/src/ui/pages/SearchPage.tsx +6 -14
  293. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  294. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  295. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  296. package/src/ui/shared/CollectionsManager.tsx +17 -7
  297. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  298. package/src/ui/shared/HomePageBranding.tsx +2 -2
  299. package/src/ui/shared/MediaGallery.tsx +1 -0
  300. package/src/ui/shared/PostFooter.tsx +3 -1
  301. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  302. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  303. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  304. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  305. package/src/ui/shared/collection-management-labels.ts +33 -2
  306. package/src/ui/shared/navigation-labels.ts +2 -4
  307. package/src/ui/shared/post-article-attributes.ts +46 -0
  308. package/dist/app-DZDlmh7C.js +0 -6
  309. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  310. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  311. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -7,6 +7,8 @@ import type { Editor } from "@tiptap/core";
7
7
  import { MAX_THREAD_POSTS } from "../../../types.js";
8
8
 
9
9
  import type {
10
+ ComposeFullscreenCloseDetail,
11
+ ComposeFullscreenOpenDetail,
10
12
  ComposeLabels,
11
13
  ComposeCollection,
12
14
  ComposeSubmitDetail,
@@ -181,6 +183,8 @@ const labels: ComposeLabels = {
181
183
  discardChangesConfirm: "Discard changes?",
182
184
  drafts: "Drafts",
183
185
  draftsEmpty: "No drafts yet. Save a draft to find it here.",
186
+ previewDraft: "Preview",
187
+ draftActions: "Draft actions",
184
188
  deleteDraft: "Delete Draft",
185
189
  draftDeleted: "Draft deleted.",
186
190
  publishFailedDraft: "Couldn't publish. Saved as draft.",
@@ -233,6 +237,20 @@ const labels: ComposeLabels = {
233
237
  replyTitle: "Reply",
234
238
  editTitle: "Edit",
235
239
  slashHint: "Type / for commands",
240
+ tableControls: {
241
+ toolbarLabel: "Table controls",
242
+ addRowAbove: "Add row above",
243
+ addRowBelow: "Add row below",
244
+ addColumnBefore: "Add column before",
245
+ addColumnAfter: "Add column after",
246
+ options: "Table options",
247
+ deleteRow: "Delete row",
248
+ deleteColumn: "Delete column",
249
+ toggleHeaderRow: "Toggle header row",
250
+ deleteTable: "Delete table",
251
+ sizePickerLabel: "Choose table size",
252
+ insertTableSize: "Insert %rows% by %cols% table",
253
+ },
236
254
  collectionFormLabels: {
237
255
  titleLabel: "Title",
238
256
  titlePlaceholder: "My Collection",
@@ -280,6 +298,7 @@ describe("JantComposeDialog", () => {
280
298
  vi.restoreAllMocks();
281
299
  document.body.innerHTML = "";
282
300
  globalThis.localStorage.clear();
301
+ delete document.documentElement.dataset.sitePathPrefix;
283
302
  Object.defineProperty(globalThis, "matchMedia", {
284
303
  configurable: true,
285
304
  writable: true,
@@ -647,6 +666,11 @@ describe("JantComposeDialog", () => {
647
666
  expect(
648
667
  el.querySelector(".compose-thread-layout.compose-reply-compose-layout"),
649
668
  ).not.toBeNull();
669
+ expect(el.querySelector(".compose-collection-trigger")).toBeNull();
670
+ expect(
671
+ el.querySelector(".compose-action-row-without-collection"),
672
+ ).not.toBeNull();
673
+ expect(el.querySelector(".compose-publish-main")).not.toBeNull();
650
674
  });
651
675
 
652
676
  it("shows an Edit title with the format selector above the post when editing a reply", async () => {
@@ -668,6 +692,7 @@ describe("JantComposeDialog", () => {
668
692
  title: "Hello",
669
693
  body: null,
670
694
  replyToId: parentId,
695
+ collectionIds: ["col-thread"],
671
696
  };
672
697
  return Promise.resolve(
673
698
  new Response(JSON.stringify(json), {
@@ -687,6 +712,8 @@ describe("JantComposeDialog", () => {
687
712
  el.querySelector(".compose-dialog-header-center .compose-segmented"),
688
713
  ).toBeNull();
689
714
  expect(el.querySelector(".compose-thread-post-header")).not.toBeNull();
715
+ expect(el._collectionIds).toEqual([]);
716
+ expect(el.querySelector(".compose-collection-trigger")).toBeNull();
690
717
  });
691
718
 
692
719
  it("switches format from the inline selector when replying", async () => {
@@ -2281,7 +2308,7 @@ describe("JantComposeDialog", () => {
2281
2308
  rating: 0,
2282
2309
  showTitle: false,
2283
2310
  showRating: false,
2284
- collectionIds: [],
2311
+ collectionIds: ["col-stale"],
2285
2312
  replyToId,
2286
2313
  attachedTexts: [],
2287
2314
  attachmentOrder: [],
@@ -2300,6 +2327,8 @@ describe("JantComposeDialog", () => {
2300
2327
  "expected compose editor",
2301
2328
  );
2302
2329
  expect(el._replyToId).toBe(replyToId);
2330
+ expect(el._collectionIds).toEqual([]);
2331
+ expect(el.querySelector(".compose-collection-trigger")).toBeNull();
2303
2332
  expect(editor.getData().body).toContain("Recovered reply draft");
2304
2333
  });
2305
2334
 
@@ -2348,6 +2377,56 @@ describe("JantComposeDialog", () => {
2348
2377
  );
2349
2378
  });
2350
2379
 
2380
+ it("includes quiet reply intent when submitting multiple reply posts", async () => {
2381
+ const el = await createElement();
2382
+ await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
2383
+ contentHtml: "<p>Parent</p>",
2384
+ dateText: "Mar 14",
2385
+ });
2386
+
2387
+ (
2388
+ el as unknown as {
2389
+ _addThreadItem: () => void;
2390
+ }
2391
+ )._addThreadItem();
2392
+ await flushUpdates(el);
2393
+
2394
+ el._quietReply = true;
2395
+ const editors = Array.from(
2396
+ el.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
2397
+ );
2398
+ expect(editors).toHaveLength(2);
2399
+
2400
+ editors.forEach((editor, index) => {
2401
+ editor._bodyJson = {
2402
+ type: "doc",
2403
+ content: [
2404
+ {
2405
+ type: "paragraph",
2406
+ content: [{ type: "text", text: `Quiet reply ${index + 1}` }],
2407
+ },
2408
+ ],
2409
+ };
2410
+ });
2411
+ await Promise.all(editors.map((editor) => editor.updateComplete));
2412
+ await el.updateComplete;
2413
+
2414
+ let receivedDetail: ComposeSubmitDetail | null = null;
2415
+ el.addEventListener("jant:compose-submit-deferred", (event) => {
2416
+ receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
2417
+ });
2418
+
2419
+ requireElement(
2420
+ el.querySelector<HTMLButtonElement>(".compose-publish-main"),
2421
+ "expected reply button",
2422
+ ).click();
2423
+
2424
+ expect(receivedDetail).not.toBeNull();
2425
+ const detail = receivedDetail as unknown as ComposeSubmitDetail;
2426
+ expect(detail.quietReply).toBe(true);
2427
+ expect(detail.threadPosts?.[0]?.quietReply).toBe(true);
2428
+ });
2429
+
2351
2430
  it("does not add more thread items than the shared limit", async () => {
2352
2431
  const el = await createElement();
2353
2432
  el._threadItems = Array.from({ length: MAX_THREAD_POSTS }, (_, index) => ({
@@ -2364,6 +2443,34 @@ describe("JantComposeDialog", () => {
2364
2443
  expect(el._threadItems).toHaveLength(MAX_THREAD_POSTS);
2365
2444
  });
2366
2445
 
2446
+ it("scrolls the thread composer to the bottom after adding an item", async () => {
2447
+ const el = await createElement();
2448
+ el._threadItems = [
2449
+ { id: "thread-1", format: "note" },
2450
+ { id: "thread-2", format: "note" },
2451
+ ];
2452
+ await el.updateComplete;
2453
+
2454
+ const threadLayout = requireElement(
2455
+ el.querySelector<HTMLElement>(".compose-thread-compose-layout"),
2456
+ "expected thread compose layout",
2457
+ );
2458
+ Object.defineProperty(threadLayout, "scrollHeight", {
2459
+ configurable: true,
2460
+ value: 640,
2461
+ });
2462
+ threadLayout.scrollTop = 120;
2463
+
2464
+ (
2465
+ el as unknown as {
2466
+ _addThreadItem: () => void;
2467
+ }
2468
+ )._addThreadItem();
2469
+ await flushUpdates(el);
2470
+
2471
+ expect(threadLayout.scrollTop).toBe(640);
2472
+ });
2473
+
2367
2474
  it("omits visibility from locked edit submissions", async () => {
2368
2475
  const el = await createElement();
2369
2476
  const editor = requireElement(
@@ -2978,6 +3085,67 @@ describe("JantComposeDialog", () => {
2978
3085
  fetchSpy.mockRestore();
2979
3086
  });
2980
3087
 
3088
+ it("opens a prefixed draft preview from the overflow menu without loading the editor", async () => {
3089
+ document.documentElement.dataset.sitePathPrefix = "/blog";
3090
+ const el = await createElement();
3091
+ el._draftsPanelOpen = true;
3092
+ el._draftsLoading = false;
3093
+ el._drafts = [
3094
+ {
3095
+ id: "pst_draft",
3096
+ slug: "draft-slug",
3097
+ format: "note",
3098
+ title: "Preview me",
3099
+ bodyText: "Draft body",
3100
+ bodyHtml: "<p>Draft body</p>",
3101
+ url: null,
3102
+ quoteText: null,
3103
+ replyToId: null,
3104
+ updatedAt: 0,
3105
+ mediaAttachments: [],
3106
+ },
3107
+ ];
3108
+ await el.updateComplete;
3109
+
3110
+ const trigger = requireElement(
3111
+ el.querySelector<HTMLButtonElement>(".compose-draft-more"),
3112
+ "expected draft actions trigger",
3113
+ );
3114
+ expect(trigger.getAttribute("aria-label")).toBe("Draft actions");
3115
+ expect(trigger.getAttribute("aria-haspopup")).toBe("menu");
3116
+ expect(trigger.getAttribute("aria-expanded")).toBe("false");
3117
+
3118
+ trigger.click();
3119
+ await el.updateComplete;
3120
+
3121
+ expect(trigger.getAttribute("aria-expanded")).toBe("true");
3122
+ const menu = requireElement(
3123
+ el.querySelector<HTMLElement>('[role="menu"]'),
3124
+ "expected draft actions menu",
3125
+ );
3126
+ const items = menu.querySelectorAll<HTMLElement>('[role="menuitem"]');
3127
+ expect(items).toHaveLength(2);
3128
+ expect(items[0]?.textContent?.trim()).toBe("Preview");
3129
+ expect(items[1]?.textContent?.trim()).toBe("Delete Draft");
3130
+
3131
+ const previewLink = requireElement(
3132
+ menu.querySelector<HTMLAnchorElement>('a[role="menuitem"]'),
3133
+ "expected preview link",
3134
+ );
3135
+ expect(previewLink.getAttribute("href")).toBe("/blog/preview/draft-slug");
3136
+ expect(previewLink.target).toBe("_blank");
3137
+ expect(previewLink.rel).toBe("noopener noreferrer");
3138
+
3139
+ const fetchSpy = vi.spyOn(globalThis, "fetch");
3140
+ previewLink.addEventListener("click", (event) => event.preventDefault());
3141
+ previewLink.click();
3142
+ await el.updateComplete;
3143
+
3144
+ expect(fetchSpy).not.toHaveBeenCalled();
3145
+ expect(el._draftsPanelOpen).toBe(true);
3146
+ expect(el._draftMenuOpenId).toBeNull();
3147
+ });
3148
+
2981
3149
  it("does not dispatch submit when loading", async () => {
2982
3150
  const el = await createElement();
2983
3151
  el._loading = true;
@@ -3103,6 +3271,17 @@ describe("JantComposeDialog", () => {
3103
3271
  );
3104
3272
  });
3105
3273
 
3274
+ it("keeps passive footnote references quiet until the editor selects them", () => {
3275
+ const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
3276
+
3277
+ expect(css).toMatch(
3278
+ /\.compose-tiptap-body \.tiptap \.tiptap-footnote-reference\s*\{[^}]*color:\s*var\(--site-footnote-marker\);/,
3279
+ );
3280
+ expect(css).toMatch(
3281
+ /\.tiptap-footnote-reference\.ProseMirror-selectednode\s*\{[^}]*color:\s*var\(--site-accent\);[^}]*outline:\s*1px solid var\(--site-accent\);/,
3282
+ );
3283
+ });
3284
+
3106
3285
  it("remove button clears attachment", async () => {
3107
3286
  const el = await createElement();
3108
3287
  const editor = requireElement(
@@ -3879,6 +4058,57 @@ describe("JantComposeDialog", () => {
3879
4058
  expect(tiptap.getText()).toBe("");
3880
4059
  });
3881
4060
 
4061
+ it("Escape cancels the table size picker without closing compose", async () => {
4062
+ const el = await createElement();
4063
+ const editor = requireElement(
4064
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
4065
+ "expected compose editor",
4066
+ );
4067
+ const tiptap = requireEditor(editor);
4068
+
4069
+ tiptap.commands.focus("end");
4070
+ tiptap.commands.insertContent("/tab");
4071
+ await flushUpdates(el);
4072
+
4073
+ const tableItem = Array.from(
4074
+ document.querySelectorAll<HTMLElement>(".tiptap-slash-item"),
4075
+ ).find((item) => item.textContent?.includes("Table"));
4076
+ requireElement(
4077
+ tableItem ?? null,
4078
+ "expected Table slash item",
4079
+ ).dispatchEvent(
4080
+ new globalThis.MouseEvent("mousedown", {
4081
+ bubbles: true,
4082
+ cancelable: true,
4083
+ }),
4084
+ );
4085
+ await vi.waitFor(() => {
4086
+ expect(
4087
+ document.querySelector(".tiptap-table-size-picker"),
4088
+ ).not.toBeNull();
4089
+ });
4090
+
4091
+ const requestCloseSpy = vi.spyOn(el, "requestClose");
4092
+ const currentSize = requireElement(
4093
+ document.querySelector<HTMLButtonElement>(
4094
+ ".tiptap-table-size-cell.is-current",
4095
+ ),
4096
+ "expected current table size",
4097
+ );
4098
+ const escapeEvent = new globalThis.KeyboardEvent("keydown", {
4099
+ key: "Escape",
4100
+ bubbles: true,
4101
+ cancelable: true,
4102
+ });
4103
+ currentSize.dispatchEvent(escapeEvent);
4104
+ await flushUpdates(el);
4105
+
4106
+ expect(escapeEvent.defaultPrevented).toBe(true);
4107
+ expect(requestCloseSpy).not.toHaveBeenCalled();
4108
+ expect(document.querySelector(".tiptap-table-size-picker")).toBeNull();
4109
+ expect(tiptap.getText()).toBe("");
4110
+ });
4111
+
3882
4112
  it("shows a clear empty state when slash commands have no matches", async () => {
3883
4113
  const el = await createElement();
3884
4114
  const editor = requireElement(
@@ -4101,10 +4331,12 @@ describe("JantComposeDialog", () => {
4101
4331
  };
4102
4332
  await editor.updateComplete;
4103
4333
 
4104
- const submitSpy = vi.spyOn(
4105
- el as unknown as { _submit: (status: "published" | "draft") => void },
4106
- "_submit",
4107
- );
4334
+ const submitSpy = vi
4335
+ .spyOn(
4336
+ el as unknown as { _submit: (status: "published" | "draft") => void },
4337
+ "_submit",
4338
+ )
4339
+ .mockImplementation(() => {});
4108
4340
 
4109
4341
  el.dispatchEvent(
4110
4342
  new globalThis.KeyboardEvent("keydown", {
@@ -4117,6 +4349,139 @@ describe("JantComposeDialog", () => {
4117
4349
  expect(submitSpy).toHaveBeenCalledWith("published");
4118
4350
  });
4119
4351
 
4352
+ it("publishes the latest fullscreen content after handing it back", async () => {
4353
+ const el = await createElement();
4354
+ const editor = requireElement(
4355
+ el.querySelector<JantComposeEditor>("jant-compose-editor"),
4356
+ "expected compose editor",
4357
+ );
4358
+ const submitSpy = vi
4359
+ .spyOn(
4360
+ el as unknown as { _submit: (status: "published" | "draft") => void },
4361
+ "_submit",
4362
+ )
4363
+ .mockImplementation(() => {});
4364
+ const setEditorStateSpy = vi.spyOn(editor, "setEditorState");
4365
+
4366
+ (
4367
+ el as unknown as {
4368
+ _handleFullscreenClose: (
4369
+ event: CustomEvent<ComposeFullscreenCloseDetail>,
4370
+ ) => void;
4371
+ }
4372
+ )._handleFullscreenClose(
4373
+ new CustomEvent<ComposeFullscreenCloseDetail>("jant:fullscreen-close", {
4374
+ detail: {
4375
+ json: {
4376
+ type: "doc",
4377
+ content: [
4378
+ {
4379
+ type: "paragraph",
4380
+ content: [{ type: "text", text: "Written while fullscreen" }],
4381
+ },
4382
+ ],
4383
+ },
4384
+ title: "Fullscreen title",
4385
+ showTitle: true,
4386
+ replyExpanded: false,
4387
+ intent: "publish",
4388
+ editorIndex: 0,
4389
+ },
4390
+ }),
4391
+ );
4392
+
4393
+ expect(setEditorStateSpy).toHaveBeenCalled();
4394
+ expect(editor.getData()).toMatchObject({
4395
+ title: "Fullscreen title",
4396
+ body: expect.stringContaining("Written while fullscreen"),
4397
+ });
4398
+ expect(submitSpy).toHaveBeenCalledWith("published");
4399
+ });
4400
+
4401
+ it("does not submit an empty fullscreen post", async () => {
4402
+ const el = await createElement();
4403
+ const submitSpy = vi.spyOn(
4404
+ el as unknown as { _submit: (status: "published" | "draft") => void },
4405
+ "_submit",
4406
+ );
4407
+
4408
+ (
4409
+ el as unknown as {
4410
+ _handleFullscreenClose: (
4411
+ event: CustomEvent<ComposeFullscreenCloseDetail>,
4412
+ ) => void;
4413
+ }
4414
+ )._handleFullscreenClose(
4415
+ new CustomEvent<ComposeFullscreenCloseDetail>("jant:fullscreen-close", {
4416
+ detail: {
4417
+ json: { type: "doc", content: [{ type: "paragraph" }] },
4418
+ title: "",
4419
+ showTitle: false,
4420
+ replyExpanded: false,
4421
+ intent: "publish",
4422
+ editorIndex: 0,
4423
+ },
4424
+ }),
4425
+ );
4426
+
4427
+ expect(submitSpy).not.toHaveBeenCalled();
4428
+ });
4429
+
4430
+ it("tracks the source editor for fullscreen thread editing", async () => {
4431
+ const el = await createElement();
4432
+ el._threadItems = [
4433
+ { id: "thread-1", format: "note" },
4434
+ { id: "thread-2", format: "note" },
4435
+ ];
4436
+ await el.updateComplete;
4437
+
4438
+ const editors = Array.from(
4439
+ el.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
4440
+ );
4441
+ const openDetail: ComposeFullscreenOpenDetail = {
4442
+ json: null,
4443
+ title: "",
4444
+ showTitle: false,
4445
+ };
4446
+ editors[1]?.dispatchEvent(
4447
+ new CustomEvent<ComposeFullscreenOpenDetail>("jant:fullscreen-open", {
4448
+ bubbles: true,
4449
+ detail: openDetail,
4450
+ }),
4451
+ );
4452
+
4453
+ expect(openDetail).toMatchObject({ editorIndex: 1 });
4454
+
4455
+ (
4456
+ el as unknown as {
4457
+ _handleFullscreenClose: (
4458
+ event: CustomEvent<ComposeFullscreenCloseDetail>,
4459
+ ) => void;
4460
+ }
4461
+ )._handleFullscreenClose(
4462
+ new CustomEvent<ComposeFullscreenCloseDetail>("jant:fullscreen-close", {
4463
+ detail: {
4464
+ json: {
4465
+ type: "doc",
4466
+ content: [
4467
+ {
4468
+ type: "paragraph",
4469
+ content: [{ type: "text", text: "Second thread post" }],
4470
+ },
4471
+ ],
4472
+ },
4473
+ title: "",
4474
+ showTitle: false,
4475
+ replyExpanded: false,
4476
+ editorIndex: 1,
4477
+ },
4478
+ }),
4479
+ );
4480
+
4481
+ expect(editors[0]?.getData().body).toBe("");
4482
+ expect(editors[1]?.getData().body).toContain("Second thread post");
4483
+ });
4484
+
4120
4485
  it("Cmd/Ctrl+Enter focuses the link URL when the URL is invalid", async () => {
4121
4486
  const el = await createElement();
4122
4487
  el._format = "link";
@@ -215,6 +215,8 @@ const labels: ComposeLabels = {
215
215
  discardChangesConfirm: "Discard changes?",
216
216
  drafts: "Drafts",
217
217
  draftsEmpty: "No drafts yet. Save a draft to find it here.",
218
+ previewDraft: "Preview",
219
+ draftActions: "Draft actions",
218
220
  deleteDraft: "Delete Draft",
219
221
  draftDeleted: "Draft deleted.",
220
222
  publishFailedDraft: "Couldn't publish. Saved as draft.",
@@ -267,6 +269,20 @@ const labels: ComposeLabels = {
267
269
  replyTitle: "Reply",
268
270
  editTitle: "Edit",
269
271
  slashHint: "Type / for commands",
272
+ tableControls: {
273
+ toolbarLabel: "Table controls",
274
+ addRowAbove: "Add row above",
275
+ addRowBelow: "Add row below",
276
+ addColumnBefore: "Add column before",
277
+ addColumnAfter: "Add column after",
278
+ options: "Table options",
279
+ deleteRow: "Delete row",
280
+ deleteColumn: "Delete column",
281
+ toggleHeaderRow: "Toggle header row",
282
+ deleteTable: "Delete table",
283
+ sizePickerLabel: "Choose table size",
284
+ insertTableSize: "Insert %rows% by %cols% table",
285
+ },
270
286
  collectionFormLabels: {
271
287
  titleLabel: "Title",
272
288
  titlePlaceholder: "My Collection",
@@ -788,6 +804,87 @@ describe("JantComposeEditor", () => {
788
804
  expect(el.querySelector('.compose-tool-btn[title="Title"]')).toBeNull();
789
805
  });
790
806
 
807
+ it.each([
808
+ ["note", ".compose-note-title"],
809
+ ["link", ".compose-link-title"],
810
+ ] as const)(
811
+ "moves from the %s title to the start of the body on Enter",
812
+ async (format, selector) => {
813
+ const el = await createElement(format);
814
+ if (format === "note") {
815
+ el._showTitle = true;
816
+ await el.updateComplete;
817
+ }
818
+
819
+ const titleInput = requireElement(
820
+ el.querySelector<HTMLInputElement>(selector),
821
+ `expected ${format} title input`,
822
+ );
823
+ const editor = requireEditor(el);
824
+ titleInput.focus();
825
+
826
+ const event = new globalThis.KeyboardEvent("keydown", {
827
+ key: "Enter",
828
+ bubbles: true,
829
+ cancelable: true,
830
+ });
831
+ titleInput.dispatchEvent(event);
832
+ await new Promise<void>((resolve) => {
833
+ globalThis.requestAnimationFrame(() => resolve());
834
+ });
835
+
836
+ expect(event.defaultPrevented).toBe(true);
837
+ expect(document.activeElement).toBe(editor.view.dom);
838
+ expect(editor.state.selection.from).toBe(1);
839
+ },
840
+ );
841
+
842
+ it("keeps focus in the title while an IME composition is active", async () => {
843
+ const el = await createElement("note");
844
+ el._showTitle = true;
845
+ await el.updateComplete;
846
+
847
+ const titleInput = requireElement(
848
+ el.querySelector<HTMLInputElement>(".compose-note-title"),
849
+ "expected note title input",
850
+ );
851
+ titleInput.focus();
852
+
853
+ const event = new globalThis.KeyboardEvent("keydown", {
854
+ key: "Enter",
855
+ isComposing: true,
856
+ bubbles: true,
857
+ cancelable: true,
858
+ });
859
+ titleInput.dispatchEvent(event);
860
+
861
+ expect(event.defaultPrevented).toBe(false);
862
+ expect(document.activeElement).toBe(titleInput);
863
+ });
864
+
865
+ it("leaves Mod-Enter available for the publish shortcut", async () => {
866
+ const el = await createElement("note");
867
+ el._showTitle = true;
868
+ await el.updateComplete;
869
+
870
+ const titleInput = requireElement(
871
+ el.querySelector<HTMLInputElement>(".compose-note-title"),
872
+ "expected note title input",
873
+ );
874
+ titleInput.focus();
875
+
876
+ const event = new globalThis.KeyboardEvent("keydown", {
877
+ key: "Enter",
878
+ metaKey: true,
879
+ bubbles: true,
880
+ cancelable: true,
881
+ });
882
+ titleInput.dispatchEvent(event);
883
+
884
+ expect(event.defaultPrevented).toBe(false);
885
+ expect(document.activeElement).toBe(titleInput);
886
+ });
887
+
791
888
  it("keeps title after rate and places fullscreen at the far right of the toolbar", async () => {
792
889
  const el = await createElement("note");
793
890
  const toolTitles = [
@@ -90,6 +90,110 @@ describe("JantComposeFullscreen", () => {
90
90
  ).toBeNull();
91
91
  });
92
92
 
93
+ it.each(["metaKey", "ctrlKey"] as const)(
94
+ "publishes with %s+Enter and returns the latest editor state",
95
+ async (modifier) => {
96
+ const el = document.createElement(
97
+ "jant-compose-fullscreen",
98
+ ) as JantComposeFullscreen;
99
+ el.labels = labels;
100
+ document.body.appendChild(el);
101
+ await flush(el);
102
+
103
+ document.dispatchEvent(
104
+ new CustomEvent("jant:fullscreen-open", {
105
+ detail: {
106
+ json: null,
107
+ title: "Keyboard post",
108
+ showTitle: true,
109
+ labels,
110
+ editorIndex: 2,
111
+ },
112
+ }),
113
+ );
114
+ await flush(el);
115
+
116
+ requireEditor(el).commands.setContent({
117
+ type: "doc",
118
+ content: [
119
+ {
120
+ type: "paragraph",
121
+ content: [{ type: "text", text: "Publish from fullscreen" }],
122
+ },
123
+ ],
124
+ });
125
+
126
+ let detail: ComposeFullscreenCloseDetail | null = null;
127
+ document.addEventListener(
128
+ "jant:fullscreen-close",
129
+ (event) => {
130
+ detail = (event as CustomEvent<ComposeFullscreenCloseDetail>).detail;
131
+ },
132
+ { once: true },
133
+ );
134
+
135
+ el.querySelector(".compose-fullscreen-dialog")?.dispatchEvent(
136
+ new globalThis.KeyboardEvent("keydown", {
137
+ key: "Enter",
138
+ [modifier]: true,
139
+ bubbles: true,
140
+ cancelable: true,
141
+ }),
142
+ );
143
+ await flush(el);
144
+
145
+ expect(detail).toMatchObject({
146
+ title: "Keyboard post",
147
+ intent: "publish",
148
+ editorIndex: 2,
149
+ json: {
150
+ type: "doc",
151
+ content: [
152
+ {
153
+ type: "paragraph",
154
+ content: [{ type: "text", text: "Publish from fullscreen" }],
155
+ },
156
+ ],
157
+ },
158
+ });
159
+ expect(el.querySelector(".compose-fullscreen-dialog")).toBeNull();
160
+ },
161
+ );
162
+
163
+ it("does not publish while an IME composition is active", async () => {
164
+ const el = document.createElement(
165
+ "jant-compose-fullscreen",
166
+ ) as JantComposeFullscreen;
167
+ el.labels = labels;
168
+ document.body.appendChild(el);
169
+ await flush(el);
170
+
171
+ document.dispatchEvent(
172
+ new CustomEvent("jant:fullscreen-open", {
173
+ detail: {
174
+ json: null,
175
+ title: "",
176
+ showTitle: false,
177
+ labels,
178
+ },
179
+ }),
180
+ );
181
+ await flush(el);
182
+
183
+ el.querySelector(".compose-fullscreen-dialog")?.dispatchEvent(
184
+ new globalThis.KeyboardEvent("keydown", {
185
+ key: "Enter",
186
+ metaKey: true,
187
+ isComposing: true,
188
+ bubbles: true,
189
+ cancelable: true,
190
+ }),
191
+ );
192
+ await flush(el);
193
+
194
+ expect(el.querySelector(".compose-fullscreen-dialog")).not.toBeNull();
195
+ });
196
+
93
197
  it("reveals the title on demand and returns its visibility on close", async () => {
94
198
  const el = document.createElement(
95
199
  "jant-compose-fullscreen",