@jant/core 0.6.11 → 0.6.13

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 (139) hide show
  1. package/bin/commands/import-site.js +47 -30
  2. package/bin/commands/migrate-text-attachments.js +1 -3
  3. package/bin/commands/site/export.js +1 -3
  4. package/bin/commands/telegram/register-webhooks.js +9 -3
  5. package/bin/lib/site-snapshot.js +1 -5
  6. package/dist/app-CWJFPjuR.js +6 -0
  7. package/dist/{app-CpmficmQ.js → app-HnzmsaqU.js} +721 -473
  8. package/dist/client/.vite/manifest.json +3 -3
  9. package/dist/client/_assets/{client-Dd9U383b.js → client-3_OaxhTs.js} +1 -1
  10. package/dist/client/_assets/client-DCrcHVJl.css +2 -0
  11. package/dist/client/_assets/{client-auth-DkpSdIDz.js → client-auth-TsYq90xm.js} +625 -561
  12. package/dist/{export-Ba7NJImL.js → export-Dfbq9aot.js} +12 -15
  13. package/dist/{github-sync-Cb4_6_i7.js → github-sync--PSoE-Ne.js} +1 -1
  14. package/dist/{github-sync-BD4w2m8-.js → github-sync-BGTLx8Mf.js} +2 -2
  15. package/dist/index.js +3 -3
  16. package/dist/node.js +4 -4
  17. package/package.json +1 -1
  18. package/src/__tests__/export-service.test.ts +1 -2
  19. package/src/__tests__/helpers/app.ts +3 -0
  20. package/src/__tests__/helpers/export-fixtures.ts +0 -1
  21. package/src/__tests__/import-site-command.test.ts +76 -2
  22. package/src/__tests__/vite-dev-plugins.test.ts +34 -0
  23. package/src/client/__tests__/compose-launch.test.ts +50 -1
  24. package/src/client/__tests__/compose-shortcuts.test.ts +74 -18
  25. package/src/client/__tests__/site-header-nav.test.ts +36 -2
  26. package/src/client/components/__tests__/compose-title-from-heading.test.ts +57 -0
  27. package/src/client/components/__tests__/jant-command-palette.test.ts +63 -0
  28. package/src/client/components/__tests__/jant-compose-dialog.test.ts +33 -0
  29. package/src/client/components/__tests__/jant-compose-editor-rehost-notice.test.ts +20 -1
  30. package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
  31. package/src/client/components/__tests__/jant-nav-manager.test.ts +181 -20
  32. package/src/client/components/__tests__/jant-settings-avatar.test.ts +6 -0
  33. package/src/client/components/__tests__/jant-settings-general.test.ts +72 -0
  34. package/src/client/components/compose-title-from-heading.ts +66 -0
  35. package/src/client/components/compose-types.ts +8 -0
  36. package/src/client/components/inline-image-issues.ts +17 -0
  37. package/src/client/components/jant-command-palette.ts +67 -2
  38. package/src/client/components/jant-compose-dialog.ts +10 -2
  39. package/src/client/components/jant-compose-editor.ts +78 -1
  40. package/src/client/components/jant-compose-fullscreen.ts +2 -0
  41. package/src/client/components/jant-nav-manager.ts +382 -152
  42. package/src/client/components/jant-settings-general.ts +56 -0
  43. package/src/client/components/nav-manager-types.ts +14 -10
  44. package/src/client/components/settings-types.ts +30 -0
  45. package/src/client/compose-launch.ts +30 -0
  46. package/src/client/compose-shortcuts.ts +46 -3
  47. package/src/client/site-header-nav.d.ts +1 -0
  48. package/src/client/site-header-nav.js +105 -47
  49. package/src/client/tiptap/__tests__/block-insertion.test.ts +35 -0
  50. package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
  51. package/src/client/tiptap/__tests__/rehost-images.test.ts +66 -13
  52. package/src/client/tiptap/create-editor.ts +7 -0
  53. package/src/client/tiptap/extensions.ts +7 -2
  54. package/src/client/tiptap/image-input-rules.ts +48 -0
  55. package/src/client/tiptap/image-node.ts +189 -7
  56. package/src/client/tiptap/rehost-images.ts +5 -1
  57. package/src/client-auth.ts +0 -1
  58. package/src/i18n/__tests__/fallback.test.ts +24 -0
  59. package/src/i18n/locales/public/en.po +20 -4
  60. package/src/i18n/locales/public/en.ts +1 -1
  61. package/src/i18n/locales/public/zh-Hans.po +22 -6
  62. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  63. package/src/i18n/locales/public/zh-Hant.po +22 -6
  64. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  65. package/src/i18n/locales/settings/en.po +77 -20
  66. package/src/i18n/locales/settings/en.ts +1 -1
  67. package/src/i18n/locales/settings/zh-Hans.po +85 -28
  68. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  69. package/src/i18n/locales/settings/zh-Hant.po +79 -22
  70. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  71. package/src/lib/__tests__/feed.test.ts +96 -10
  72. package/src/lib/__tests__/navigation.test.ts +4 -37
  73. package/src/lib/__tests__/resolve-config.test.ts +6 -1
  74. package/src/lib/__tests__/view.test.ts +6 -30
  75. package/src/lib/feed.ts +63 -3
  76. package/src/lib/github-sync-site-config.ts +0 -2
  77. package/src/lib/hugo-markdown.ts +1 -1
  78. package/src/lib/navigation.ts +5 -22
  79. package/src/lib/post-display.ts +14 -8
  80. package/src/lib/resolve-config.ts +9 -2
  81. package/src/lib/site-header-fragment.tsx +37 -0
  82. package/src/lib/view.ts +2 -7
  83. package/src/node/__tests__/cli-site-snapshot.test.ts +45 -45
  84. package/src/preset.css +60 -18
  85. package/src/routes/api/__tests__/nav-items.test.ts +26 -0
  86. package/src/routes/api/export.ts +0 -2
  87. package/src/routes/api/internal/__tests__/uploads.test.ts +9 -13
  88. package/src/routes/api/internal/sites.ts +5 -5
  89. package/src/routes/api/nav-items.ts +25 -4
  90. package/src/routes/dash/settings.tsx +48 -3
  91. package/src/routes/feed/__tests__/feed.test.ts +42 -0
  92. package/src/routes/feed/__tests__/sitemap.test.ts +2 -13
  93. package/src/routes/feed/feed.ts +1 -0
  94. package/src/routes/feed/sitemap.ts +2 -6
  95. package/src/routes/pages/__tests__/draft-about-edit.test.ts +65 -0
  96. package/src/routes/pages/archive.tsx +1 -0
  97. package/src/routes/pages/collection.tsx +1 -0
  98. package/src/routes/pages/featured.tsx +0 -4
  99. package/src/routes/pages/home.tsx +4 -51
  100. package/src/routes/pages/latest.tsx +2 -51
  101. package/src/routes/pages/page.tsx +44 -12
  102. package/src/services/__tests__/about-page.test.ts +116 -0
  103. package/src/services/__tests__/github-app-installations.test.ts +10 -10
  104. package/src/services/__tests__/navigation.test.ts +231 -1
  105. package/src/services/__tests__/post-timeline.test.ts +175 -2
  106. package/src/services/__tests__/post.test.ts +33 -0
  107. package/src/services/__tests__/settings.test.ts +5 -20
  108. package/src/services/about-page.ts +143 -0
  109. package/src/services/export-theme/assets/client-site.js +10 -10
  110. package/src/services/export-theme/layouts/_default/rss.xml +7 -16
  111. package/src/services/export-theme/layouts/index.html +26 -39
  112. package/src/services/export-theme/layouts/partials/feed-post-content.xml +25 -1
  113. package/src/services/export-theme/styles/main.css +46 -14
  114. package/src/services/export.ts +5 -10
  115. package/src/services/github-app-installations.ts +1 -3
  116. package/src/services/index.ts +27 -17
  117. package/src/services/navigation.ts +186 -1
  118. package/src/services/post.ts +103 -63
  119. package/src/services/settings.ts +0 -10
  120. package/src/services/site-admin.ts +0 -2
  121. package/src/services/upload-session.ts +4 -18
  122. package/src/styles/components.css +20 -0
  123. package/src/styles/tokens.css +28 -10
  124. package/src/styles/ui.css +349 -13
  125. package/src/types/bindings.ts +0 -1
  126. package/src/types/config.ts +3 -7
  127. package/src/types/views.ts +13 -0
  128. package/src/ui/compose/ComposeDialog.tsx +28 -0
  129. package/src/ui/dash/appearance/NavigationContent.tsx +58 -1
  130. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
  131. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  132. package/src/ui/dash/settings/GeneralContent.tsx +44 -0
  133. package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
  134. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
  135. package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
  136. package/src/ui/layouts/SiteLayout.tsx +249 -216
  137. package/dist/app-DqKkZenB.js +0 -6
  138. package/dist/client/_assets/client-BhHHVvSY.css +0 -2
  139. package/src/client/nav-manager-bridge.ts +0 -54
@@ -0,0 +1,66 @@
1
+ import type { JSONContent } from "@tiptap/core";
2
+
3
+ export interface LeadingH1TitlePromotion {
4
+ title: string;
5
+ bodyJson: JSONContent | null;
6
+ headingIndex: number;
7
+ }
8
+
9
+ function textFromNode(node: JSONContent): string {
10
+ if (typeof node.text === "string") return node.text;
11
+ if (!node.content) return node.type === "hardBreak" ? " " : "";
12
+ return node.content.map(textFromNode).join("");
13
+ }
14
+
15
+ function hasOnlyEmptyInlineContent(node: JSONContent): boolean {
16
+ if (!node.content || node.content.length === 0) return true;
17
+ return node.content.every((child) => {
18
+ if (child.type === "text") return !child.text?.trim();
19
+ if (child.type === "hardBreak") return true;
20
+ return false;
21
+ });
22
+ }
23
+
24
+ function isIgnorableLeadingBlock(node: JSONContent): boolean {
25
+ return (
26
+ (node.type === "paragraph" || node.type === "heading") &&
27
+ hasOnlyEmptyInlineContent(node)
28
+ );
29
+ }
30
+
31
+ function normalizeTitleText(text: string): string {
32
+ return text.replace(/\s+/g, " ").trim();
33
+ }
34
+
35
+ export function promoteLeadingH1Title(
36
+ bodyJson: JSONContent | null,
37
+ ): LeadingH1TitlePromotion | null {
38
+ const content = bodyJson?.content;
39
+ if (!bodyJson || !content?.length) return null;
40
+
41
+ const headingIndex = content.findIndex((node) => {
42
+ return !isIgnorableLeadingBlock(node);
43
+ });
44
+
45
+ if (headingIndex < 0) return null;
46
+
47
+ const heading = content[headingIndex];
48
+ if (heading?.type !== "heading" || heading.attrs?.level !== 1) return null;
49
+
50
+ const title = normalizeTitleText(textFromNode(heading));
51
+ if (!title) return null;
52
+
53
+ const nextContent = content.slice(headingIndex + 1);
54
+ if (nextContent.length === 0) {
55
+ return { title, bodyJson: null, headingIndex };
56
+ }
57
+
58
+ return {
59
+ title,
60
+ bodyJson: {
61
+ ...bodyJson,
62
+ content: nextContent,
63
+ },
64
+ headingIndex,
65
+ };
66
+ }
@@ -149,6 +149,14 @@ export interface ComposeLabels {
149
149
  imageNotRehosted?: string;
150
150
  /** Toast when several pasted remote images couldn't be rehosted (uses a {count} placeholder). */
151
151
  imagesNotRehosted?: string;
152
+ /** Inline editor label shown when an image node cannot load. */
153
+ brokenImageUnavailable?: string;
154
+ /** Button label for removing a broken inline image. */
155
+ brokenImageDelete?: string;
156
+ /** Button label for replacing a broken inline image. */
157
+ brokenImageReplace?: string;
158
+ /** Button label for opening the original broken image URL. */
159
+ brokenImageOpen?: string;
152
160
  loadingPost: string;
153
161
  loadPostFailed: string;
154
162
  published: string;
@@ -0,0 +1,17 @@
1
+ import type { ImageNodeLabels } from "../tiptap/image-node.js";
2
+ import type { ComposeLabels } from "./compose-types.js";
3
+
4
+ export function getInlineImageNodeLabels(
5
+ labels: ComposeLabels,
6
+ ): Partial<ImageNodeLabels> {
7
+ const imageLabels: Partial<ImageNodeLabels> = {};
8
+ if (labels.brokenImageUnavailable) {
9
+ imageLabels.unavailable = labels.brokenImageUnavailable;
10
+ }
11
+ if (labels.brokenImageDelete) imageLabels.delete = labels.brokenImageDelete;
12
+ if (labels.brokenImageReplace) {
13
+ imageLabels.replace = labels.brokenImageReplace;
14
+ }
15
+ if (labels.brokenImageOpen) imageLabels.open = labels.brokenImageOpen;
16
+ return imageLabels;
17
+ }
@@ -5,6 +5,7 @@
5
5
  * - Navigate (default): filter preloaded posts, collections, system pages
6
6
  * - Command (`>` prefix): run actions like New Post
7
7
  * - Search (`?` prefix): redirects to /search?q=... on Enter
8
+ * - Path (`/` prefix): opens a local path, with search as the second option
8
9
  */
9
10
 
10
11
  import { LitElement, html, nothing } from "lit";
@@ -45,10 +46,24 @@ const ICONS = {
45
46
  system: `<svg ${ICON_ATTRS}><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>`,
46
47
  // zap/lightning for commands
47
48
  command: `<svg ${ICON_ATTRS}><path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></svg>`,
49
+ // corner-down-right for local path navigation
50
+ path: `<svg ${ICON_ATTRS}><polyline points="15 10 20 15 15 20"/><path d="M4 4v7a4 4 0 0 0 4 4h12"/></svg>`,
48
51
  // search
49
52
  search: `<svg ${ICON_ATTRS}><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>`,
50
53
  };
51
54
 
55
+ export function getPathCommandTarget(query: string): string {
56
+ const stripped = query.trim().replace(/^[//]+/, "");
57
+ return stripped ? `/${stripped}` : "/";
58
+ }
59
+
60
+ export function getPathCommandSearchQuery(query: string): string {
61
+ return query
62
+ .trim()
63
+ .replace(/^[//]+/, "")
64
+ .trim();
65
+ }
66
+
52
67
  // ---------------------------------------------------------------------------
53
68
  // History helpers
54
69
  // ---------------------------------------------------------------------------
@@ -208,12 +223,13 @@ export class JantCommandPalette extends LitElement {
208
223
  // Mode detection
209
224
  // -----------------------------------------------------------------------
210
225
 
211
- get #mode(): "navigate" | "command" | "search" {
212
- // Accept both halfwidth (>, ?) and fullwidth (>, ?) prefixes so CJK
226
+ get #mode(): "navigate" | "command" | "search" | "path" {
227
+ // Accept both halfwidth and fullwidth prefixes so CJK
213
228
  // users don't have to toggle their IME to enter command/search mode.
214
229
  const first = this._query.charAt(0);
215
230
  if (first === ">" || first === ">") return "command";
216
231
  if (first === "?" || first === "?") return "search";
232
+ if (first === "/" || first === "/") return "path";
217
233
  return "navigate";
218
234
  }
219
235
 
@@ -270,6 +286,8 @@ export class JantCommandPalette extends LitElement {
270
286
  icon: string;
271
287
  /** For search-mode items: the query to execute */
272
288
  searchQuery?: string;
289
+ /** For path-mode items: the local path to open */
290
+ pathTarget?: string;
273
291
  }> {
274
292
  const mode = this.#mode;
275
293
 
@@ -299,12 +317,41 @@ export class JantCommandPalette extends LitElement {
299
317
  ];
300
318
  }
301
319
 
320
+ if (mode === "path") {
321
+ const target = getPathCommandTarget(this._query);
322
+ const searchQuery = getPathCommandSearchQuery(this._query);
323
+ const items: Array<{
324
+ label: string;
325
+ secondary?: string;
326
+ icon: string;
327
+ searchQuery?: string;
328
+ pathTarget?: string;
329
+ }> = [
330
+ {
331
+ label: `Go to ${target}`,
332
+ icon: ICONS.path,
333
+ pathTarget: target,
334
+ },
335
+ ];
336
+
337
+ if (searchQuery) {
338
+ items.push({
339
+ label: `Search for "${searchQuery}"`,
340
+ icon: ICONS.search,
341
+ searchQuery,
342
+ });
343
+ }
344
+
345
+ return items;
346
+ }
347
+
302
348
  // Navigate mode — show all items when no query (autocomplete)
303
349
  const navItems: Array<{
304
350
  label: string;
305
351
  secondary?: string;
306
352
  icon: string;
307
353
  searchQuery?: string;
354
+ pathTarget?: string;
308
355
  }> = this.#navigateItems.map((item) => ({
309
356
  label: item.title,
310
357
  secondary: item.type === "system" ? item.path : item.path,
@@ -351,6 +398,24 @@ export class JantCommandPalette extends LitElement {
351
398
  return;
352
399
  }
353
400
 
401
+ if (mode === "path") {
402
+ const displayItem = this.#displayItems[index];
403
+ if (displayItem?.pathTarget) {
404
+ saveHistory(NAV_HISTORY_KEY, displayItem.pathTarget);
405
+ this.close();
406
+ window.location.href = displayItem.pathTarget;
407
+ return;
408
+ }
409
+
410
+ if (displayItem?.searchQuery) {
411
+ const q = displayItem.searchQuery;
412
+ saveHistory(SEARCH_HISTORY_KEY, q);
413
+ this.close();
414
+ window.location.href = `/search?q=${encodeURIComponent(q)}`;
415
+ }
416
+ return;
417
+ }
418
+
354
419
  // Navigate mode — check if the selected item is a search fallback
355
420
  const displayItem = this.#displayItems[index];
356
421
  if (displayItem?.searchQuery) {
@@ -1321,6 +1321,7 @@ export class JantComposeDialog extends LitElement {
1321
1321
  index: number,
1322
1322
  status: "published" | "draft",
1323
1323
  ): ComposeSubmitDetail {
1324
+ editor.promoteLeadingH1Title({ force: true });
1324
1325
  const editorData = editor.getData();
1325
1326
  const mediaAttachments = new Map(
1326
1327
  (editorData.attachments ?? []).map((a) => [a.clientId, a]),
@@ -1389,6 +1390,7 @@ export class JantComposeDialog extends LitElement {
1389
1390
  const editor = this._editor;
1390
1391
  if (!editor) return null;
1391
1392
 
1393
+ editor.promoteLeadingH1Title({ force: true });
1392
1394
  const editorData = editor.getData();
1393
1395
  const mediaAttachments = new Map(
1394
1396
  (editorData.attachments ?? []).map((attachment) => [
@@ -5432,7 +5434,11 @@ export class JantComposeDialog extends LitElement {
5432
5434
 
5433
5435
  return html`
5434
5436
  <div
5435
- class="compose-thread-layout compose-thread-compose-layout"
5437
+ class=${classMap({
5438
+ "compose-thread-layout": true,
5439
+ "compose-thread-compose-layout": true,
5440
+ "compose-reply-compose-layout": isReply,
5441
+ })}
5436
5442
  @jant:compose-content-changed=${() => this._scheduleDraftSave()}
5437
5443
  >
5438
5444
  ${isReply ? this._renderReplyContext() : nothing}
@@ -5502,7 +5508,9 @@ export class JantComposeDialog extends LitElement {
5502
5508
  ? this._renderThreadComposeLayout()
5503
5509
  : isReply
5504
5510
  ? html`
5505
- <div class="compose-thread-layout">
5511
+ <div
5512
+ class="compose-thread-layout compose-reply-compose-layout"
5513
+ >
5506
5514
  ${this._renderReplyContext()}
5507
5515
  <div
5508
5516
  class="compose-editor-row"
@@ -31,6 +31,10 @@ import type {
31
31
  ComposeFullscreenOpenDetail,
32
32
  } from "./compose-types.js";
33
33
  import type { ComposeConvertFields } from "./compose-format-convert.js";
34
+ import {
35
+ promoteLeadingH1Title as getLeadingH1TitlePromotion,
36
+ type LeadingH1TitlePromotion,
37
+ } from "./compose-title-from-heading.js";
34
38
  import {
35
39
  UPLOAD_ACCEPT,
36
40
  getMediaCategory,
@@ -59,6 +63,7 @@ import {
59
63
  markSlashCommandDiscovered,
60
64
  scheduleSlashCommandHint,
61
65
  } from "../slash-discovery.js";
66
+ import { getInlineImageNodeLabels } from "./inline-image-issues.js";
62
67
 
63
68
  interface ComposeFilePickerCloseDetail {
64
69
  cancelled: boolean;
@@ -631,6 +636,67 @@ export class JantComposeEditor extends LitElement {
631
636
  return this._isEmptyDoc(json) ? null : json;
632
637
  }
633
638
 
639
+ private _isSelectionInsideTopLevelBlock(index: number): boolean {
640
+ const editor = this._editor;
641
+ if (!editor) return false;
642
+ return editor.state.selection.$from.index(0) === index;
643
+ }
644
+
645
+ private _removedTopLevelSize(promotion: LeadingH1TitlePromotion): number {
646
+ const doc = this._editor?.state.doc;
647
+ if (!doc) return 0;
648
+
649
+ let size = 0;
650
+ for (let index = 0; index <= promotion.headingIndex; index += 1) {
651
+ size += doc.child(index)?.nodeSize ?? 0;
652
+ }
653
+ return size;
654
+ }
655
+
656
+ promoteLeadingH1Title(options: { force?: boolean } = {}): boolean {
657
+ if (this.format !== "note" || this._title.trim()) return false;
658
+
659
+ const currentBodyJson = this._editor?.getJSON() ?? this._bodyJson;
660
+ const promotion = getLeadingH1TitlePromotion(currentBodyJson);
661
+ if (!promotion) return false;
662
+
663
+ if (
664
+ !options.force &&
665
+ this._isSelectionInsideTopLevelBlock(promotion.headingIndex)
666
+ ) {
667
+ return false;
668
+ }
669
+
670
+ const editor = this._editor;
671
+ const wasFocused = Boolean(editor?.isFocused);
672
+ const previousSelectionFrom = editor?.state.selection.from ?? null;
673
+ const removedSize = this._removedTopLevelSize(promotion);
674
+
675
+ this._title = promotion.title;
676
+ this._showTitle = true;
677
+ this._bodyJson = promotion.bodyJson;
678
+
679
+ if (editor) {
680
+ if (promotion.bodyJson) {
681
+ editor.commands.setContent(promotion.bodyJson);
682
+ } else {
683
+ editor.commands.clearContent();
684
+ }
685
+
686
+ if (wasFocused && previousSelectionFrom !== null) {
687
+ const max = editor.state.doc.content.size;
688
+ if (max > 0) {
689
+ editor.commands.setTextSelection(
690
+ Math.max(1, Math.min(previousSelectionFrom - removedSize, max)),
691
+ );
692
+ editor.commands.focus();
693
+ }
694
+ }
695
+ }
696
+
697
+ return true;
698
+ }
699
+
634
700
  getNormalizedBodyJson(): JSONContent | null {
635
701
  return this._normalizeDocJson(this._bodyJson);
636
702
  }
@@ -913,6 +979,7 @@ export class JantComposeEditor extends LitElement {
913
979
  onUpdate: (json) => {
914
980
  this._bodyJson = json;
915
981
  this._ensureScrollBuffer();
982
+ this.promoteLeadingH1Title();
916
983
  hideSlashCommandHint(this);
917
984
  },
918
985
  onFocus: () => {
@@ -921,6 +988,9 @@ export class JantComposeEditor extends LitElement {
921
988
  scheduleSlashCommandHint(this);
922
989
  }
923
990
  },
991
+ onBlur: () => {
992
+ this.promoteLeadingH1Title({ force: true });
993
+ },
924
994
  onSelectionUpdate: (selection) => {
925
995
  this._lastEditorSelection = selection;
926
996
  },
@@ -935,6 +1005,7 @@ export class JantComposeEditor extends LitElement {
935
1005
  shouldRehost: (src) => this.#shouldRehostSrc(src),
936
1006
  rehost: (src) => this.#rehostInlineImage(src),
937
1007
  },
1008
+ imageNodeLabels: getInlineImageNodeLabels(this.labels),
938
1009
  });
939
1010
  this._lastEditorSelection = this._readEditorSelection();
940
1011
 
@@ -1851,11 +1922,14 @@ export class JantComposeEditor extends LitElement {
1851
1922
  }
1852
1923
 
1853
1924
  private _renderAttachmentPreviewFallback(category: "image" | "video") {
1925
+ const unavailableLabel =
1926
+ this.labels.brokenImageUnavailable ?? "Image unavailable";
1927
+
1854
1928
  return html`
1855
1929
  <div
1856
1930
  class="compose-attachment-preview-fallback"
1857
1931
  data-preview-failed=${category}
1858
- aria-hidden="true"
1932
+ aria-label=${unavailableLabel}
1859
1933
  >
1860
1934
  <svg
1861
1935
  width="28"
@@ -1871,6 +1945,9 @@ export class JantComposeEditor extends LitElement {
1871
1945
  <circle cx="9" cy="10" r="1.25" />
1872
1946
  <path d="m7 16 3.4-3.45 2.65 2.45 2.45-3.05 1.95 2.5" />
1873
1947
  </svg>
1948
+ <span class="compose-attachment-preview-fallback-label">
1949
+ ${unavailableLabel}
1950
+ </span>
1874
1951
  ${category === "video"
1875
1952
  ? html`
1876
1953
  <div class="compose-attachment-play-icon">
@@ -21,6 +21,7 @@ import type {
21
21
  } from "./compose-types.js";
22
22
  import { createTiptapEditor } from "../tiptap/create-editor.js";
23
23
  import { uploadAndInsertInlineImage } from "../tiptap/inline-image-upload.js";
24
+ import { getInlineImageNodeLabels } from "./inline-image-issues.js";
24
25
 
25
26
  const ESCAPE_OVERLAY_SELECTOR = ".tiptap-slash-menu, .tiptap-link-input";
26
27
 
@@ -159,6 +160,7 @@ export class JantComposeFullscreen extends LitElement {
159
160
  shouldInsertInline: (file) => file.type.startsWith("image/"),
160
161
  uploadInlineImage: (file) => this._uploadAndInsertImage(file),
161
162
  },
163
+ imageNodeLabels: getInlineImageNodeLabels(this.labels),
162
164
  });
163
165
 
164
166
  const selection = this._selection;