@jant/core 0.6.15 → 0.6.16

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 (112) hide show
  1. package/bin/commands/import-site.js +7 -0
  2. package/dist/app-C75Zc8dD.js +6 -0
  3. package/dist/{app-C-oi_t8W.js → app-C7_4oab9.js} +480 -196
  4. package/dist/client/.vite/manifest.json +8 -8
  5. package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
  6. package/dist/client/_assets/{client-DxY5BFe8.js → client-Bq0vre8Y.js} +3 -3
  7. package/dist/client/_assets/client-D3VZyG4L.css +2 -0
  8. package/dist/client/_assets/{client-auth-CakPESv9.js → client-auth-F2rb9WfF.js} +1015 -977
  9. package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
  10. package/dist/{export-CsFx6F_M.js → export-Dl5KCiEs.js} +93 -7
  11. package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
  12. package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
  13. package/dist/{github-sync-ppWXN3jb.js → github-sync-Bqg62IvV.js} +3 -3
  14. package/dist/{github-sync-Cq1pzzOI.js → github-sync-HdK2EgvJ.js} +3 -3
  15. package/dist/index.js +5 -5
  16. package/dist/node.js +6 -6
  17. package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
  18. package/package.json +1 -1
  19. package/src/__tests__/mise-config.test.ts +22 -0
  20. package/src/client/__tests__/compose-bridge.test.ts +252 -2
  21. package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
  22. package/src/client/__tests__/toast.test.ts +65 -1
  23. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  24. package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
  25. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2015 -318
  26. package/src/client/components/__tests__/jant-compose-editor.test.ts +53 -62
  27. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +35 -89
  28. package/src/client/components/compose-format-convert.ts +5 -1
  29. package/src/client/components/compose-types.ts +47 -3
  30. package/src/client/components/jant-command-palette.ts +12 -6
  31. package/src/client/components/jant-compose-dialog.ts +1464 -680
  32. package/src/client/components/jant-compose-editor.ts +225 -93
  33. package/src/client/components/jant-compose-fullscreen.ts +3 -5
  34. package/src/client/components/jant-post-menu.ts +91 -21
  35. package/src/client/compose-bridge.ts +143 -122
  36. package/src/client/compose-launch.ts +13 -0
  37. package/src/client/compose-shortcuts.ts +5 -1
  38. package/src/client/post-refresh.ts +135 -0
  39. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  40. package/src/client/tiptap/link-input-rules.ts +4 -4
  41. package/src/client/toast.ts +130 -38
  42. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  43. package/src/db/__tests__/thread-activity.test.ts +113 -0
  44. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  45. package/src/db/migrations/0031_many_ego.sql +3 -0
  46. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  47. package/src/db/migrations/meta/_journal.json +7 -0
  48. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  49. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  50. package/src/db/migrations/pg/meta/_journal.json +7 -0
  51. package/src/db/pg/schema.ts +18 -0
  52. package/src/db/schema.ts +20 -0
  53. package/src/db/thread-activity.ts +101 -0
  54. package/src/i18n/locales/public/en.po +89 -21
  55. package/src/i18n/locales/public/en.ts +1 -1
  56. package/src/i18n/locales/public/zh-Hans.po +88 -20
  57. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  58. package/src/i18n/locales/public/zh-Hant.po +88 -20
  59. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  60. package/src/lib/__tests__/feed.test.ts +168 -0
  61. package/src/lib/__tests__/markdown.test.ts +16 -0
  62. package/src/lib/__tests__/slug.test.ts +18 -0
  63. package/src/lib/__tests__/translit.test.ts +87 -0
  64. package/src/lib/__tests__/url.test.ts +53 -0
  65. package/src/lib/feed.ts +113 -11
  66. package/src/lib/hugo-markdown.ts +5 -0
  67. package/src/lib/link-preview.ts +25 -0
  68. package/src/lib/markdown-manager.ts +6 -1
  69. package/src/lib/post-display.ts +42 -9
  70. package/src/lib/timeline.ts +39 -9
  71. package/src/lib/tiptap-render.ts +4 -2
  72. package/src/lib/translit.ts +288 -0
  73. package/src/lib/url.ts +123 -12
  74. package/src/lib/view.ts +28 -0
  75. package/src/routes/api/__tests__/palette.test.ts +44 -0
  76. package/src/routes/api/posts.ts +13 -9
  77. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  78. package/src/routes/api/public/posts.ts +7 -0
  79. package/src/routes/compose.tsx +6 -2
  80. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  81. package/src/routes/pages/__tests__/archive-params.test.ts +292 -0
  82. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  83. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  84. package/src/routes/pages/archive.tsx +65 -17
  85. package/src/routes/pages/page.tsx +5 -2
  86. package/src/services/__tests__/collection.test.ts +32 -1
  87. package/src/services/__tests__/post-timeline.test.ts +142 -11
  88. package/src/services/__tests__/post.test.ts +277 -0
  89. package/src/services/collection.ts +8 -10
  90. package/src/services/export-theme/assets/client-site.css +1 -1
  91. package/src/services/export.ts +3 -5
  92. package/src/services/path.ts +4 -1
  93. package/src/services/post.ts +297 -100
  94. package/src/services/search.ts +9 -0
  95. package/src/styles/components.css +0 -14
  96. package/src/styles/site-media.css +4 -4
  97. package/src/styles/tokens.css +62 -1
  98. package/src/styles/ui.css +1363 -1283
  99. package/src/types/entities.ts +5 -0
  100. package/src/types/props.ts +12 -0
  101. package/src/types/views.ts +6 -0
  102. package/src/ui/compose/ComposeDialog.tsx +17 -17
  103. package/src/ui/feed/LinkPreview.tsx +2 -7
  104. package/src/ui/feed/PostStatusBadges.tsx +42 -3
  105. package/src/ui/pages/ArchivePage.tsx +131 -24
  106. package/src/ui/pages/__tests__/ArchivePage.test.tsx +63 -0
  107. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  108. package/src/ui/shared/PostFooter.tsx +35 -12
  109. package/src/ui/shared/custom-icons.ts +5 -0
  110. package/src/ui/shared/post-article-attributes.ts +6 -0
  111. package/dist/app-BxCOR3Uc.js +0 -6
  112. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
@@ -8,6 +8,7 @@ type ComposeHarness = HTMLElement & {
8
8
  clearLocalDraftFromStorage?: () => void;
9
9
  clearEditDraftFromStorage?: (postId: string) => void;
10
10
  openEdit?: (id: string) => Promise<void>;
11
+ openDraft?: (id: string) => Promise<void>;
11
12
  openNew?: (options?: { restoreDraft?: boolean }) => Promise<void>;
12
13
  openReply?: (
13
14
  id: string,
@@ -42,9 +43,18 @@ async function flushBridgeWork(times = 4) {
42
43
  }
43
44
  }
44
45
 
46
+ function renderPostView(postId: string) {
47
+ const postView = document.createElement("div");
48
+ postView.dataset.postView = "";
49
+ postView.dataset.postViewId = postId;
50
+ document.body.appendChild(postView);
51
+ return postView;
52
+ }
53
+
45
54
  describe("compose bridge", () => {
46
55
  beforeEach(() => {
47
56
  document.body.innerHTML = "";
57
+ delete document.documentElement.dataset.sitePathPrefix;
48
58
  vi.restoreAllMocks();
49
59
  globalThis.sessionStorage.clear();
50
60
  });
@@ -517,7 +527,12 @@ describe("compose bridge", () => {
517
527
  undefined,
518
528
  "pst_root",
519
529
  { kind: "timeline-item", id: "pst_root" },
520
- { restoreDraft: true, initialFormat: "quote" },
530
+ {
531
+ restoreDraft: true,
532
+ initialFormat: "quote",
533
+ restoreToast: false,
534
+ restoreMedia: [],
535
+ },
521
536
  );
522
537
  expect(composeEl.openNew).not.toHaveBeenCalled();
523
538
  expect(composeEl.clearLocalDraftFromStorage).not.toHaveBeenCalled();
@@ -716,6 +731,241 @@ describe("compose bridge", () => {
716
731
 
717
732
  await flushBridgeWork();
718
733
 
719
- expect(fetchSpy).toHaveBeenCalledTimes(2);
734
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
735
+ });
736
+
737
+ it("keeps an updated draft on its authenticated preview path", async () => {
738
+ const composeEl = document.createElement(
739
+ "jant-compose-dialog",
740
+ ) as ComposeHarness;
741
+ composeEl.refreshCollections = vi.fn(async () => true);
742
+ composeEl.pageMode = false;
743
+ document.body.appendChild(composeEl);
744
+ const previewBar = document.createElement("aside");
745
+ previewBar.dataset.previewStatus = "";
746
+ document.body.appendChild(previewBar);
747
+ document.documentElement.dataset.sitePathPrefix = "/blog";
748
+ globalThis.history.replaceState({}, "", "/blog/preview/draft-post");
749
+
750
+ const assignSpy = vi
751
+ .spyOn(globalThis.location, "assign")
752
+ .mockImplementation(() => {});
753
+ vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
754
+ const raw =
755
+ typeof input === "string"
756
+ ? input
757
+ : input instanceof URL
758
+ ? input.toString()
759
+ : input.url;
760
+ const url = new URL(raw, "http://localhost");
761
+
762
+ expect(url.pathname).toBe("/api/posts/pst_draft");
763
+ expect(init?.method).toBe("PUT");
764
+ expect(JSON.parse(String(init?.body))).toMatchObject({
765
+ status: "draft",
766
+ });
767
+
768
+ return new Response(
769
+ JSON.stringify({
770
+ id: "pst_draft",
771
+ slug: "draft-post",
772
+ status: "draft",
773
+ }),
774
+ {
775
+ status: 200,
776
+ headers: { "Content-Type": "application/json" },
777
+ },
778
+ );
779
+ });
780
+
781
+ composeEl.dispatchEvent(
782
+ new CustomEvent("jant:compose-submit-deferred", {
783
+ bubbles: true,
784
+ detail: {
785
+ format: "note",
786
+ title: "Draft post",
787
+ body: "Updated draft body",
788
+ url: "",
789
+ quoteText: "",
790
+ quoteAuthor: "",
791
+ slug: "draft-post",
792
+ status: "draft",
793
+ visibility: "public",
794
+ rating: 0,
795
+ collectionIds: [],
796
+ attachments: [],
797
+ pendingAttachments: [],
798
+ editPostId: "pst_draft",
799
+ draftSourceId: "pst_draft",
800
+ },
801
+ }),
802
+ );
803
+
804
+ await flushBridgeWork();
805
+
806
+ expect(assignSpy).toHaveBeenCalledWith("/blog/preview/draft-post");
807
+ });
808
+
809
+ it("keeps a draft-panel save in place and signals the updated list", async () => {
810
+ const composeEl = document.createElement(
811
+ "jant-compose-dialog",
812
+ ) as ComposeHarness;
813
+ const refreshCollections = vi.fn(async () => true);
814
+ composeEl.refreshCollections = refreshCollections;
815
+ composeEl.pageMode = false;
816
+ document.body.appendChild(composeEl);
817
+
818
+ const assignSpy = vi
819
+ .spyOn(globalThis.location, "assign")
820
+ .mockImplementation(() => {});
821
+ const completeSpy = vi.fn();
822
+ document.addEventListener("jant:compose-submit-complete", completeSpy, {
823
+ once: true,
824
+ });
825
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(
826
+ new Response(
827
+ JSON.stringify({
828
+ id: "pst_draft",
829
+ slug: "draft-post",
830
+ status: "draft",
831
+ }),
832
+ {
833
+ status: 200,
834
+ headers: { "Content-Type": "application/json" },
835
+ },
836
+ ),
837
+ );
838
+
839
+ composeEl.dispatchEvent(
840
+ new CustomEvent("jant:compose-submit-deferred", {
841
+ bubbles: true,
842
+ detail: {
843
+ format: "note",
844
+ title: "Draft post",
845
+ body: "Updated draft body",
846
+ url: "",
847
+ quoteText: "",
848
+ quoteAuthor: "",
849
+ slug: "draft-post",
850
+ status: "draft",
851
+ visibility: "public",
852
+ rating: 0,
853
+ collectionIds: [],
854
+ attachments: [],
855
+ pendingAttachments: [],
856
+ editPostId: "pst_draft",
857
+ draftSourceId: "pst_draft",
858
+ },
859
+ }),
860
+ );
861
+
862
+ await flushBridgeWork();
863
+
864
+ expect(assignSpy).not.toHaveBeenCalled();
865
+ expect(refreshCollections).toHaveBeenCalled();
866
+ expect(completeSpy).toHaveBeenCalledOnce();
867
+ });
868
+
869
+ it("reopens a failed server draft through the draft loader", async () => {
870
+ const composeEl = document.createElement(
871
+ "jant-compose-dialog",
872
+ ) as ComposeHarness;
873
+ composeEl.refreshCollections = vi.fn(async () => true);
874
+ composeEl.pageMode = false;
875
+ composeEl.openDraft = vi.fn(async () => {});
876
+ composeEl.openEdit = vi.fn(async () => {});
877
+ document.body.appendChild(composeEl);
878
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(
879
+ new Response(JSON.stringify({ error: "Could not save draft." }), {
880
+ status: 500,
881
+ headers: { "Content-Type": "application/json" },
882
+ }),
883
+ );
884
+
885
+ composeEl.dispatchEvent(
886
+ new CustomEvent("jant:compose-submit-deferred", {
887
+ bubbles: true,
888
+ detail: {
889
+ format: "note",
890
+ title: "Draft post",
891
+ body: "Updated draft body",
892
+ url: "",
893
+ quoteText: "",
894
+ quoteAuthor: "",
895
+ status: "draft",
896
+ visibility: "public",
897
+ rating: 0,
898
+ collectionIds: [],
899
+ attachments: [],
900
+ pendingAttachments: [],
901
+ editPostId: "pst_draft",
902
+ draftSourceId: "pst_draft",
903
+ },
904
+ }),
905
+ );
906
+
907
+ await flushBridgeWork();
908
+
909
+ expect(composeEl.openDraft).toHaveBeenCalledWith("pst_draft");
910
+ expect(composeEl.openEdit).not.toHaveBeenCalled();
911
+ });
912
+
913
+ it("sends a published draft to its public permalink", async () => {
914
+ const composeEl = document.createElement(
915
+ "jant-compose-dialog",
916
+ ) as ComposeHarness;
917
+ composeEl.refreshCollections = vi.fn(async () => true);
918
+ composeEl.pageMode = false;
919
+ document.body.appendChild(composeEl);
920
+ renderPostView("pst_draft");
921
+ const previewBar = document.createElement("aside");
922
+ previewBar.dataset.previewStatus = "";
923
+ document.body.appendChild(previewBar);
924
+ document.documentElement.dataset.sitePathPrefix = "/blog";
925
+ globalThis.history.replaceState({}, "", "/blog/preview/draft-post");
926
+
927
+ const assignSpy = vi
928
+ .spyOn(globalThis.location, "assign")
929
+ .mockImplementation(() => {});
930
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(
931
+ new Response(
932
+ JSON.stringify({
933
+ id: "pst_draft",
934
+ slug: "published-post",
935
+ status: "published",
936
+ }),
937
+ {
938
+ status: 200,
939
+ headers: { "Content-Type": "application/json" },
940
+ },
941
+ ),
942
+ );
943
+
944
+ composeEl.dispatchEvent(
945
+ new CustomEvent("jant:compose-submit-deferred", {
946
+ bubbles: true,
947
+ detail: {
948
+ format: "note",
949
+ title: "Published post",
950
+ body: "Ready to publish",
951
+ url: "",
952
+ quoteText: "",
953
+ quoteAuthor: "",
954
+ slug: "published-post",
955
+ status: "published",
956
+ visibility: "public",
957
+ rating: 0,
958
+ collectionIds: [],
959
+ attachments: [],
960
+ pendingAttachments: [],
961
+ editPostId: "pst_draft",
962
+ draftSourceId: "pst_draft",
963
+ },
964
+ }),
965
+ );
966
+
967
+ await flushBridgeWork();
968
+
969
+ expect(assignSpy).toHaveBeenCalledWith("/blog/published-post");
720
970
  });
721
971
  });
@@ -8,6 +8,7 @@ type ComposeHarness = HTMLElement & {
8
8
  openNew: (options?: unknown) => Promise<void>;
9
9
  openReply: (...args: unknown[]) => Promise<void>;
10
10
  openEdit: (id: string) => Promise<void>;
11
+ openDraft: (id: string) => Promise<void>;
11
12
  };
12
13
 
13
14
  function dispatchShortcut(
@@ -30,6 +31,7 @@ function createComposeHarness(): ComposeHarness {
30
31
  composeEl.openNew = vi.fn(async () => {});
31
32
  composeEl.openReply = vi.fn(async () => {});
32
33
  composeEl.openEdit = vi.fn(async () => {});
34
+ composeEl.openDraft = vi.fn(async () => {});
33
35
  document.body.appendChild(composeEl);
34
36
  return composeEl;
35
37
  }
@@ -284,6 +286,24 @@ describe("compose shortcuts", () => {
284
286
  expect(window.location.search).toBe("");
285
287
  });
286
288
 
289
+ it("loads the current draft through the drafts workflow on preview pages", async () => {
290
+ const composeEl = createComposeHarness();
291
+ document.body.setAttribute("data-authenticated", "true");
292
+ const previewBar = document.createElement("aside");
293
+ previewBar.dataset.previewStatus = "";
294
+ document.body.appendChild(previewBar);
295
+ renderThreadDetailPage();
296
+ globalThis.history.pushState({}, "", "/preview/draft-post?edit=1");
297
+
298
+ composeShortcutsTestOnly.openEditFromQueryParam();
299
+ await Promise.resolve();
300
+
301
+ expect(composeEl.openDraft).toHaveBeenCalledWith("post-current");
302
+ expect(composeEl.openEdit).not.toHaveBeenCalled();
303
+ expect(window.location.pathname).toBe("/preview/draft-post");
304
+ expect(window.location.search).toBe("");
305
+ });
306
+
287
307
  it("keeps using the hovered post for reply shortcuts in the timeline", () => {
288
308
  const composeEl = createComposeHarness();
289
309
  const article = document.createElement("article");
@@ -5,6 +5,7 @@ import {
5
5
  QUEUED_TOAST_STORAGE_KEY,
6
6
  consumeQueuedToast,
7
7
  queueToastForNextPage,
8
+ showToast,
8
9
  showToastWithAction,
9
10
  } from "../toast.js";
10
11
 
@@ -46,6 +47,69 @@ describe("toast", () => {
46
47
  expect(action?.getAttribute("href")).toBe("/published-post");
47
48
  });
48
49
 
50
+ it("renders error toasts without a copy button — text is selectable instead", () => {
51
+ showToast("Publish failed.", "error");
52
+
53
+ const toast = document.querySelector(".toast-error");
54
+ expect(toast).not.toBeNull();
55
+ expect(toast?.querySelector("button")).toBeNull();
56
+ });
57
+
58
+ it("pauses auto-dismiss while hovered and resumes the remaining time", () => {
59
+ vi.useFakeTimers();
60
+ showToast("Publish failed.", "error");
61
+ const toast = document.querySelector<HTMLElement>(".toast");
62
+ if (!toast) throw new Error("toast not rendered");
63
+
64
+ vi.advanceTimersByTime(2000);
65
+ toast.dispatchEvent(new Event("pointerenter"));
66
+ vi.advanceTimersByTime(60_000);
67
+ expect(toast.classList.contains("toast-out")).toBe(false);
68
+
69
+ toast.dispatchEvent(new Event("pointerleave"));
70
+ vi.advanceTimersByTime(999);
71
+ expect(toast.classList.contains("toast-out")).toBe(false);
72
+ vi.advanceTimersByTime(1);
73
+ expect(toast.classList.contains("toast-out")).toBe(true);
74
+ });
75
+
76
+ it("pauses auto-dismiss while focus is inside the toast", () => {
77
+ vi.useFakeTimers();
78
+ showToastWithAction("Collection created.", {
79
+ label: "Edit Navigation",
80
+ href: "/settings/navigation",
81
+ });
82
+ const toast = document.querySelector<HTMLElement>(".toast");
83
+ if (!toast) throw new Error("toast not rendered");
84
+
85
+ toast.dispatchEvent(new Event("focusin"));
86
+ vi.advanceTimersByTime(60_000);
87
+ expect(toast.classList.contains("toast-out")).toBe(false);
88
+
89
+ toast.dispatchEvent(new Event("focusout"));
90
+ vi.advanceTimersByTime(5000);
91
+ expect(toast.classList.contains("toast-out")).toBe(true);
92
+ });
93
+
94
+ it("mounts toasts inside an open modal dialog and restores them on close", async () => {
95
+ const dialog = document.createElement("dialog");
96
+ document.body.appendChild(dialog);
97
+ dialog.showModal();
98
+
99
+ showToast("Publish failed.", "error");
100
+
101
+ const container = document.getElementById("toast-container");
102
+ expect(container?.parentElement).toBe(dialog);
103
+ expect(container?.querySelector(".toast span")?.textContent).toBe(
104
+ "Publish failed.",
105
+ );
106
+
107
+ dialog.close();
108
+ // the dialog close event is dispatched in a queued task
109
+ await new Promise((resolve) => setTimeout(resolve, 0));
110
+ expect(container?.parentElement).toBe(document.body);
111
+ });
112
+
49
113
  it("keeps action toasts available longer than passive notifications", () => {
50
114
  vi.useFakeTimers();
51
115
  showToastWithAction("Collection created.", {
@@ -57,7 +121,7 @@ describe("toast", () => {
57
121
  vi.advanceTimersByTime(3000);
58
122
  expect(toast?.classList.contains("toast-out")).toBe(false);
59
123
 
60
- vi.advanceTimersByTime(5000);
124
+ vi.advanceTimersByTime(2000);
61
125
  expect(toast?.classList.contains("toast-out")).toBe(true);
62
126
  });
63
127
  });
@@ -55,7 +55,6 @@ describe("convertComposeFormat", () => {
55
55
  "quote",
56
56
  fields({
57
57
  title: "My thoughts",
58
- showTitle: true,
59
58
  bodyJson: doc(paragraph("body")),
60
59
  }),
61
60
  );
@@ -216,7 +215,6 @@ describe("convertComposeFormat", () => {
216
215
  "link",
217
216
  fields({
218
217
  title: "Keep me",
219
- showTitle: true,
220
218
  bodyJson: doc(paragraph("body")),
221
219
  }),
222
220
  );
@@ -236,7 +234,6 @@ describe("convertComposeFormat", () => {
236
234
  }),
237
235
  );
238
236
  expect(result.title).toBe("Keep me");
239
- expect(result.showTitle).toBe(true);
240
237
  expect(result.url).toBe("");
241
238
  expect(firstBlock(result)).toEqual({
242
239
  type: "paragraph",
@@ -317,22 +314,6 @@ describe("convertComposeFormat", () => {
317
314
  expect(result.bodyJson).toBeNull();
318
315
  });
319
316
 
320
- it("sets showTitle only for a note that ends up with a title", () => {
321
- const toNote = convertComposeFormat(
322
- "link",
323
- "note",
324
- fields({ title: "T", url: "https://example.com" }),
325
- );
326
- expect(toNote.showTitle).toBe(true);
327
-
328
- const toQuote = convertComposeFormat(
329
- "note",
330
- "quote",
331
- fields({ title: "T" }),
332
- );
333
- expect(toQuote.showTitle).toBe(false);
334
- });
335
-
336
317
  it("treats a non-attribution last line as part of the quote text", () => {
337
318
  const result = convertComposeFormat(
338
319
  "note",
@@ -11,6 +11,7 @@ interface MockPaletteItem {
11
11
  title: string;
12
12
  path: string;
13
13
  type: "post" | "collection" | "system";
14
+ status?: "draft" | "published";
14
15
  }
15
16
 
16
17
  function mockPaletteApi(items: MockPaletteItem[] = []) {
@@ -44,6 +45,7 @@ describe("JantCommandPalette slash path mode", () => {
44
45
  beforeEach(() => {
45
46
  vi.restoreAllMocks();
46
47
  document.body.innerHTML = "";
48
+ delete document.documentElement.dataset.sitePathPrefix;
47
49
  globalThis.localStorage.clear();
48
50
  globalThis.history.replaceState({}, "", "/");
49
51
  });
@@ -73,6 +75,7 @@ describe("JantCommandPalette persistent search action", () => {
73
75
  beforeEach(() => {
74
76
  vi.restoreAllMocks();
75
77
  document.body.innerHTML = "";
78
+ delete document.documentElement.dataset.sitePathPrefix;
76
79
  globalThis.localStorage.clear();
77
80
  globalThis.history.replaceState({}, "", "/");
78
81
  });
@@ -187,4 +190,52 @@ describe("JantCommandPalette persistent search action", () => {
187
190
 
188
191
  expect(globalThis.location.pathname).toBe("/match-1");
189
192
  });
193
+
194
+ it("opens draft post matches through the authenticated preview route", async () => {
195
+ const el = await renderPalette("draft", [
196
+ {
197
+ title: "Draft post",
198
+ path: "draft-post",
199
+ type: "post",
200
+ status: "draft",
201
+ },
202
+ ]);
203
+ const dialog = el.querySelector<HTMLDialogElement>(".command-palette");
204
+ expect(dialog?.textContent).toContain("Draft · draft-post");
205
+
206
+ dialog?.dispatchEvent(
207
+ new globalThis.KeyboardEvent("keydown", {
208
+ key: "Enter",
209
+ bubbles: true,
210
+ cancelable: true,
211
+ }),
212
+ );
213
+
214
+ expect(globalThis.location.pathname).toBe("/preview/draft-post");
215
+ expect(globalThis.location.search).toBe("?edit=1");
216
+ });
217
+
218
+ it("keeps the site path prefix when opening a draft preview", async () => {
219
+ document.documentElement.dataset.sitePathPrefix = "/blog";
220
+ const el = await renderPalette("draft", [
221
+ {
222
+ title: "Draft post",
223
+ path: "draft-post",
224
+ type: "post",
225
+ status: "draft",
226
+ },
227
+ ]);
228
+ const dialog = el.querySelector<HTMLDialogElement>(".command-palette");
229
+
230
+ dialog?.dispatchEvent(
231
+ new globalThis.KeyboardEvent("keydown", {
232
+ key: "Enter",
233
+ bubbles: true,
234
+ cancelable: true,
235
+ }),
236
+ );
237
+
238
+ expect(globalThis.location.pathname).toBe("/blog/preview/draft-post");
239
+ expect(globalThis.location.search).toBe("?edit=1");
240
+ });
190
241
  });