@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.
- package/bin/commands/import-site.js +7 -0
- package/dist/app-C75Zc8dD.js +6 -0
- package/dist/{app-C-oi_t8W.js → app-C7_4oab9.js} +480 -196
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-DxY5BFe8.js → client-Bq0vre8Y.js} +3 -3
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/{client-auth-CakPESv9.js → client-auth-F2rb9WfF.js} +1015 -977
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-CsFx6F_M.js → export-Dl5KCiEs.js} +93 -7
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-ppWXN3jb.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Cq1pzzOI.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +6 -6
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- package/package.json +1 -1
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/client/__tests__/compose-bridge.test.ts +252 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
- package/src/client/__tests__/toast.test.ts +65 -1
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2015 -318
- package/src/client/components/__tests__/jant-compose-editor.test.ts +53 -62
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +35 -89
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +47 -3
- package/src/client/components/jant-command-palette.ts +12 -6
- package/src/client/components/jant-compose-dialog.ts +1464 -680
- package/src/client/components/jant-compose-editor.ts +225 -93
- package/src/client/components/jant-compose-fullscreen.ts +3 -5
- package/src/client/components/jant-post-menu.ts +91 -21
- package/src/client/compose-bridge.ts +143 -122
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -1
- package/src/client/post-refresh.ts +135 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/toast.ts +130 -38
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +18 -0
- package/src/db/schema.ts +20 -0
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/locales/public/en.po +89 -21
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +88 -20
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +88 -20
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +168 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/slug.test.ts +18 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -0
- package/src/lib/feed.ts +113 -11
- package/src/lib/hugo-markdown.ts +5 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/post-display.ts +42 -9
- package/src/lib/timeline.ts +39 -9
- package/src/lib/tiptap-render.ts +4 -2
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +28 -0
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/posts.ts +13 -9
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/posts.ts +7 -0
- package/src/routes/compose.tsx +6 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/pages/__tests__/archive-params.test.ts +292 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/archive.tsx +65 -17
- package/src/routes/pages/page.tsx +5 -2
- package/src/services/__tests__/collection.test.ts +32 -1
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +277 -0
- package/src/services/collection.ts +8 -10
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export.ts +3 -5
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +297 -100
- package/src/services/search.ts +9 -0
- package/src/styles/components.css +0 -14
- package/src/styles/site-media.css +4 -4
- package/src/styles/tokens.css +62 -1
- package/src/styles/ui.css +1363 -1283
- package/src/types/entities.ts +5 -0
- package/src/types/props.ts +12 -0
- package/src/types/views.ts +6 -0
- package/src/ui/compose/ComposeDialog.tsx +17 -17
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- package/src/ui/pages/ArchivePage.tsx +131 -24
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +63 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/PostFooter.tsx +35 -12
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/post-article-attributes.ts +6 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
|
@@ -243,13 +243,13 @@ const labels: ComposeLabels = {
|
|
|
243
243
|
publishDateInvalid: "Enter a valid date.",
|
|
244
244
|
publishDateFutureError:
|
|
245
245
|
"Choose today or an earlier date, or leave it blank to publish now.",
|
|
246
|
-
publishDateSummaryNow: "
|
|
246
|
+
publishDateSummaryNow: "Now",
|
|
247
247
|
publishDateSummaryAction: "Edit publish date",
|
|
248
248
|
publishSlugLabel: "Custom link",
|
|
249
249
|
publishSlugPlaceholder: "your-post-link",
|
|
250
250
|
publishSlugHint: "Leave blank to generate one automatically.",
|
|
251
251
|
publishSlugAuto: "Generate automatically",
|
|
252
|
-
publishSlugSummaryAuto: "Auto
|
|
252
|
+
publishSlugSummaryAuto: "Auto",
|
|
253
253
|
publishSlugSummaryAction: "Edit custom link",
|
|
254
254
|
publishSlugReset: "Reset link",
|
|
255
255
|
publishSlugSuggested: "Suggested link",
|
|
@@ -265,9 +265,9 @@ const labels: ComposeLabels = {
|
|
|
265
265
|
threadLimitReached: "Threads can include up to 20 posts.",
|
|
266
266
|
showMore: "Show more",
|
|
267
267
|
showLess: "Show less",
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
268
|
+
closeCompose: "Close compose",
|
|
269
|
+
editing: "Editing",
|
|
270
|
+
composeDialogLabel: "Compose",
|
|
271
271
|
slashHint: "Type / for commands",
|
|
272
272
|
tableControls: {
|
|
273
273
|
toolbarLabel: "Table controls",
|
|
@@ -308,10 +308,14 @@ const labels: ComposeLabels = {
|
|
|
308
308
|
|
|
309
309
|
async function createElement(
|
|
310
310
|
format: string = "note",
|
|
311
|
+
options: { titleByDefault?: boolean } = {},
|
|
311
312
|
): Promise<JantComposeEditor> {
|
|
312
313
|
const el = document.createElement("jant-compose-editor") as JantComposeEditor;
|
|
313
314
|
el.format = format as "note" | "link" | "quote";
|
|
314
315
|
el.labels = labels;
|
|
316
|
+
if (options.titleByDefault !== undefined) {
|
|
317
|
+
el.titleByDefault = options.titleByDefault;
|
|
318
|
+
}
|
|
315
319
|
document.body.appendChild(el);
|
|
316
320
|
await el.updateComplete;
|
|
317
321
|
return el;
|
|
@@ -795,13 +799,46 @@ describe("JantComposeEditor", () => {
|
|
|
795
799
|
expect(el._attachedTexts[0].bodyJson).toBeNull();
|
|
796
800
|
});
|
|
797
801
|
|
|
798
|
-
it("shows title
|
|
802
|
+
it("shows a note's title field by default on a post that starts a thread", async () => {
|
|
799
803
|
const el = await createElement("note");
|
|
804
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
800
805
|
expect(el.querySelector('.compose-tool-btn[title="Title"]')).not.toBeNull();
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("hides a note's title field by default on a continuation post", async () => {
|
|
809
|
+
const el = await createElement("note", { titleByDefault: false });
|
|
810
|
+
expect(el.querySelector(".compose-note-title")).toBeNull();
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it("toggles the title field from the toolbar", async () => {
|
|
814
|
+
const el = await createElement("note", { titleByDefault: false });
|
|
815
|
+
const toggle = requireElement(
|
|
816
|
+
el.querySelector<HTMLButtonElement>('.compose-tool-btn[title="Title"]'),
|
|
817
|
+
"expected title toggle",
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
toggle.click();
|
|
821
|
+
await el.updateComplete;
|
|
822
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
823
|
+
|
|
824
|
+
toggle.click();
|
|
825
|
+
await el.updateComplete;
|
|
826
|
+
expect(el.querySelector(".compose-note-title")).toBeNull();
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it("drops a hidden title from the submitted data", async () => {
|
|
830
|
+
const el = await createElement("note");
|
|
831
|
+
el._title = "Named";
|
|
832
|
+
el._showTitle = false;
|
|
833
|
+
await el.updateComplete;
|
|
834
|
+
expect(el.getData().title).toBe("");
|
|
835
|
+
});
|
|
801
836
|
|
|
802
|
-
|
|
837
|
+
it("keeps the title field visible when a post arrives with one", async () => {
|
|
838
|
+
const el = await createElement("note", { titleByDefault: false });
|
|
839
|
+
el.populate({ format: "note", title: "Loaded title" });
|
|
803
840
|
await el.updateComplete;
|
|
804
|
-
expect(el.querySelector(
|
|
841
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
805
842
|
});
|
|
806
843
|
|
|
807
844
|
it.each([
|
|
@@ -812,7 +849,6 @@ describe("JantComposeEditor", () => {
|
|
|
812
849
|
async (format, selector) => {
|
|
813
850
|
const el = await createElement(format);
|
|
814
851
|
if (format === "note") {
|
|
815
|
-
el._showTitle = true;
|
|
816
852
|
await el.updateComplete;
|
|
817
853
|
}
|
|
818
854
|
|
|
@@ -841,7 +877,6 @@ describe("JantComposeEditor", () => {
|
|
|
841
877
|
|
|
842
878
|
it("keeps focus in the title while an IME composition is active", async () => {
|
|
843
879
|
const el = await createElement("note");
|
|
844
|
-
el._showTitle = true;
|
|
845
880
|
await el.updateComplete;
|
|
846
881
|
|
|
847
882
|
const titleInput = requireElement(
|
|
@@ -864,7 +899,6 @@ describe("JantComposeEditor", () => {
|
|
|
864
899
|
|
|
865
900
|
it("leaves Mod-Enter available for the publish shortcut", async () => {
|
|
866
901
|
const el = await createElement("note");
|
|
867
|
-
el._showTitle = true;
|
|
868
902
|
await el.updateComplete;
|
|
869
903
|
|
|
870
904
|
const titleInput = requireElement(
|
|
@@ -885,7 +919,7 @@ describe("JantComposeEditor", () => {
|
|
|
885
919
|
expect(document.activeElement).toBe(titleInput);
|
|
886
920
|
});
|
|
887
921
|
|
|
888
|
-
it("
|
|
922
|
+
it("places fullscreen at the far right of the toolbar", async () => {
|
|
889
923
|
const el = await createElement("note");
|
|
890
924
|
const toolTitles = [
|
|
891
925
|
...el.querySelectorAll<HTMLButtonElement>(".compose-tool-btn"),
|
|
@@ -924,7 +958,6 @@ describe("JantComposeEditor", () => {
|
|
|
924
958
|
it("getData returns current field values", async () => {
|
|
925
959
|
const el = await createElement("note");
|
|
926
960
|
el._title = "Test Title";
|
|
927
|
-
el._showTitle = true;
|
|
928
961
|
el._bodyJson = {
|
|
929
962
|
type: "doc",
|
|
930
963
|
content: [
|
|
@@ -994,31 +1027,11 @@ describe("JantComposeEditor", () => {
|
|
|
994
1027
|
expect(data.attachmentOrder).toEqual(["t2"]);
|
|
995
1028
|
});
|
|
996
1029
|
|
|
997
|
-
it("getData
|
|
998
|
-
const el = await createElement("note");
|
|
999
|
-
el._title = "Hidden Title";
|
|
1000
|
-
el._showTitle = false;
|
|
1001
|
-
|
|
1002
|
-
const data = el.getData();
|
|
1003
|
-
expect(data.title).toBe("");
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
it("preserves title in memory when toggling off and restores on toggle on", async () => {
|
|
1030
|
+
it("getData returns the note title as typed", async () => {
|
|
1007
1031
|
const el = await createElement("note");
|
|
1008
|
-
el._title = "
|
|
1009
|
-
el._showTitle = true;
|
|
1010
|
-
await el.updateComplete;
|
|
1011
|
-
|
|
1012
|
-
// Toggle off — title stays in memory
|
|
1013
|
-
el._showTitle = false;
|
|
1014
|
-
await el.updateComplete;
|
|
1015
|
-
expect(el._title).toBe("My Title");
|
|
1016
|
-
expect(el.getData().title).toBe("");
|
|
1032
|
+
el._title = "Kept Title";
|
|
1017
1033
|
|
|
1018
|
-
|
|
1019
|
-
el._showTitle = true;
|
|
1020
|
-
await el.updateComplete;
|
|
1021
|
-
expect(el.getData().title).toBe("My Title");
|
|
1034
|
+
expect(el.getData().title).toBe("Kept Title");
|
|
1022
1035
|
});
|
|
1023
1036
|
|
|
1024
1037
|
it("promotes a leading H1 to the note title", async () => {
|
|
@@ -1035,7 +1048,6 @@ describe("JantComposeEditor", () => {
|
|
|
1035
1048
|
const body = JSON.parse(data.body) as JSONContent;
|
|
1036
1049
|
|
|
1037
1050
|
expect(data.title).toBe("My Markdown Title");
|
|
1038
|
-
expect(el._showTitle).toBe(true);
|
|
1039
1051
|
expect(body.content?.[0]).toEqual(tiptapParagraph("Body"));
|
|
1040
1052
|
});
|
|
1041
1053
|
|
|
@@ -1050,7 +1062,6 @@ describe("JantComposeEditor", () => {
|
|
|
1050
1062
|
const body = JSON.parse(data.body) as JSONContent;
|
|
1051
1063
|
|
|
1052
1064
|
expect(data.title).toBe("");
|
|
1053
|
-
expect(el._showTitle).toBe(false);
|
|
1054
1065
|
expect(body.content?.[0]).toEqual(tiptapHeading(1, "Draft title"));
|
|
1055
1066
|
});
|
|
1056
1067
|
|
|
@@ -1058,7 +1069,6 @@ describe("JantComposeEditor", () => {
|
|
|
1058
1069
|
const el = await createElement("note");
|
|
1059
1070
|
const editor = requireEditor(el);
|
|
1060
1071
|
el._title = "Manual title";
|
|
1061
|
-
el._showTitle = true;
|
|
1062
1072
|
|
|
1063
1073
|
editor.commands.setContent(
|
|
1064
1074
|
tiptapDoc(tiptapHeading(1, "Markdown Title"), tiptapParagraph("Body")),
|
|
@@ -1231,6 +1241,7 @@ describe("JantComposeEditor", () => {
|
|
|
1231
1241
|
alt: "",
|
|
1232
1242
|
error: null,
|
|
1233
1243
|
posterUrl: null,
|
|
1244
|
+
remoteUrl: null,
|
|
1234
1245
|
summary: null,
|
|
1235
1246
|
chars: null,
|
|
1236
1247
|
},
|
|
@@ -1264,6 +1275,7 @@ describe("JantComposeEditor", () => {
|
|
|
1264
1275
|
alt: "",
|
|
1265
1276
|
error: null,
|
|
1266
1277
|
posterUrl: null,
|
|
1278
|
+
remoteUrl: null,
|
|
1267
1279
|
summary: null,
|
|
1268
1280
|
chars: null,
|
|
1269
1281
|
},
|
|
@@ -1277,6 +1289,7 @@ describe("JantComposeEditor", () => {
|
|
|
1277
1289
|
alt: "",
|
|
1278
1290
|
error: null,
|
|
1279
1291
|
posterUrl: null,
|
|
1292
|
+
remoteUrl: null,
|
|
1280
1293
|
summary: null,
|
|
1281
1294
|
chars: null,
|
|
1282
1295
|
},
|
|
@@ -1343,7 +1356,7 @@ describe("JantComposeEditor", () => {
|
|
|
1343
1356
|
expect(el._attachmentOrder[0]).toBe("t1");
|
|
1344
1357
|
});
|
|
1345
1358
|
|
|
1346
|
-
it("pastes clipboard files into attachments when no
|
|
1359
|
+
it("pastes clipboard files into attachments when no title is set", async () => {
|
|
1347
1360
|
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1348
1361
|
const el = await createElement("note");
|
|
1349
1362
|
const events: CustomEvent[] = [];
|
|
@@ -1379,7 +1392,6 @@ describe("JantComposeEditor", () => {
|
|
|
1379
1392
|
id: "med_test",
|
|
1380
1393
|
});
|
|
1381
1394
|
const el = await createElement("note");
|
|
1382
|
-
el._showTitle = true;
|
|
1383
1395
|
el._title = "Essay";
|
|
1384
1396
|
await el.updateComplete;
|
|
1385
1397
|
|
|
@@ -1399,30 +1411,9 @@ describe("JantComposeEditor", () => {
|
|
|
1399
1411
|
]);
|
|
1400
1412
|
});
|
|
1401
1413
|
|
|
1402
|
-
it("pastes images as attachments when the title row was toggled off after typing", async () => {
|
|
1403
|
-
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1404
|
-
const el = await createElement("note");
|
|
1405
|
-
el._showTitle = true;
|
|
1406
|
-
el._title = "Essay";
|
|
1407
|
-
await el.updateComplete;
|
|
1408
|
-
el._showTitle = false;
|
|
1409
|
-
await el.updateComplete;
|
|
1410
|
-
|
|
1411
|
-
const image = new File(["image"], "clipboard.png", { type: "image/png" });
|
|
1412
|
-
const { handled } = triggerEditorPaste(el, [image]);
|
|
1413
|
-
await el.updateComplete;
|
|
1414
|
-
|
|
1415
|
-
expect(handled).toBe(true);
|
|
1416
|
-
expect(uploadWithMetadataMock).not.toHaveBeenCalled();
|
|
1417
|
-
expect(el._attachments.map((attachment) => attachment.file.name)).toEqual([
|
|
1418
|
-
"clipboard.png",
|
|
1419
|
-
]);
|
|
1420
|
-
});
|
|
1421
|
-
|
|
1422
1414
|
it("pastes images as attachments when only whitespace is in the title", async () => {
|
|
1423
1415
|
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1424
1416
|
const el = await createElement("note");
|
|
1425
|
-
el._showTitle = true;
|
|
1426
1417
|
el._title = " ";
|
|
1427
1418
|
await el.updateComplete;
|
|
1428
1419
|
|
|
@@ -14,14 +14,13 @@ const labels = {
|
|
|
14
14
|
fullscreen: "Fullscreen",
|
|
15
15
|
exitFullscreen: "Exit fullscreen",
|
|
16
16
|
done: "Done",
|
|
17
|
-
title: "Title",
|
|
18
17
|
titlePlaceholder: "Title",
|
|
19
18
|
bodyPlaceholder: "What's on your mind...",
|
|
20
19
|
showMore: "Show more",
|
|
21
20
|
showLess: "Show less",
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
closeCompose: "Close compose",
|
|
22
|
+
editing: "Editing",
|
|
23
|
+
composeDialogLabel: "Compose",
|
|
25
24
|
} as ComposeLabels;
|
|
26
25
|
|
|
27
26
|
async function flush(el?: JantComposeFullscreen) {
|
|
@@ -58,7 +57,7 @@ describe("JantComposeFullscreen", () => {
|
|
|
58
57
|
}
|
|
59
58
|
});
|
|
60
59
|
|
|
61
|
-
it("
|
|
60
|
+
it("shows the title field when the editor arrived with one open", async () => {
|
|
62
61
|
const el = document.createElement(
|
|
63
62
|
"jant-compose-fullscreen",
|
|
64
63
|
) as JantComposeFullscreen;
|
|
@@ -68,26 +67,39 @@ describe("JantComposeFullscreen", () => {
|
|
|
68
67
|
|
|
69
68
|
document.dispatchEvent(
|
|
70
69
|
new CustomEvent("jant:fullscreen-open", {
|
|
71
|
-
detail: {
|
|
72
|
-
json: null,
|
|
73
|
-
title: "",
|
|
74
|
-
showTitle: false,
|
|
75
|
-
labels,
|
|
76
|
-
},
|
|
70
|
+
detail: { json: null, title: "", showTitle: true, labels },
|
|
77
71
|
}),
|
|
78
72
|
);
|
|
79
73
|
await flush(el);
|
|
80
74
|
|
|
81
|
-
expect(el.querySelector(".compose-fullscreen-title")).toBeNull();
|
|
82
|
-
expect(
|
|
83
|
-
el.querySelector(".compose-fullscreen-title-placeholder"),
|
|
84
|
-
).not.toBeNull();
|
|
75
|
+
expect(el.querySelector(".compose-fullscreen-title")).not.toBeNull();
|
|
85
76
|
expect(el.textContent).not.toContain("Fullscreen");
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("offers a way back to a title the editor had hidden", async () => {
|
|
80
|
+
const el = document.createElement(
|
|
81
|
+
"jant-compose-fullscreen",
|
|
82
|
+
) as JantComposeFullscreen;
|
|
83
|
+
el.labels = labels;
|
|
84
|
+
document.body.appendChild(el);
|
|
85
|
+
await flush(el);
|
|
86
|
+
|
|
87
|
+
document.dispatchEvent(
|
|
88
|
+
new CustomEvent("jant:fullscreen-open", {
|
|
89
|
+
detail: { json: null, title: "", showTitle: false, labels },
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
await flush(el);
|
|
93
|
+
|
|
94
|
+
expect(el.querySelector(".compose-fullscreen-title")).toBeNull();
|
|
95
|
+
const reveal = el.querySelector<HTMLButtonElement>(
|
|
96
|
+
".compose-fullscreen-title-placeholder",
|
|
97
|
+
);
|
|
98
|
+
expect(reveal).not.toBeNull();
|
|
99
|
+
|
|
100
|
+
reveal?.click();
|
|
101
|
+
await flush(el);
|
|
102
|
+
expect(el.querySelector(".compose-fullscreen-title")).not.toBeNull();
|
|
91
103
|
});
|
|
92
104
|
|
|
93
105
|
it.each(["metaKey", "ctrlKey"] as const)(
|
|
@@ -105,7 +117,6 @@ describe("JantComposeFullscreen", () => {
|
|
|
105
117
|
detail: {
|
|
106
118
|
json: null,
|
|
107
119
|
title: "Keyboard post",
|
|
108
|
-
showTitle: true,
|
|
109
120
|
labels,
|
|
110
121
|
editorIndex: 2,
|
|
111
122
|
},
|
|
@@ -173,7 +184,6 @@ describe("JantComposeFullscreen", () => {
|
|
|
173
184
|
detail: {
|
|
174
185
|
json: null,
|
|
175
186
|
title: "",
|
|
176
|
-
showTitle: false,
|
|
177
187
|
labels,
|
|
178
188
|
},
|
|
179
189
|
}),
|
|
@@ -194,66 +204,6 @@ describe("JantComposeFullscreen", () => {
|
|
|
194
204
|
expect(el.querySelector(".compose-fullscreen-dialog")).not.toBeNull();
|
|
195
205
|
});
|
|
196
206
|
|
|
197
|
-
it("reveals the title on demand and returns its visibility on close", async () => {
|
|
198
|
-
const el = document.createElement(
|
|
199
|
-
"jant-compose-fullscreen",
|
|
200
|
-
) as JantComposeFullscreen;
|
|
201
|
-
el.labels = labels;
|
|
202
|
-
document.body.appendChild(el);
|
|
203
|
-
await flush(el);
|
|
204
|
-
|
|
205
|
-
document.dispatchEvent(
|
|
206
|
-
new CustomEvent("jant:fullscreen-open", {
|
|
207
|
-
detail: {
|
|
208
|
-
json: null,
|
|
209
|
-
title: "",
|
|
210
|
-
showTitle: false,
|
|
211
|
-
labels,
|
|
212
|
-
},
|
|
213
|
-
}),
|
|
214
|
-
);
|
|
215
|
-
await flush(el);
|
|
216
|
-
|
|
217
|
-
const toggle = el.querySelector<HTMLButtonElement>(
|
|
218
|
-
".compose-fullscreen-title-placeholder",
|
|
219
|
-
);
|
|
220
|
-
expect(toggle).not.toBeNull();
|
|
221
|
-
toggle?.click();
|
|
222
|
-
await flush(el);
|
|
223
|
-
|
|
224
|
-
const input = el.querySelector<HTMLInputElement>(
|
|
225
|
-
".compose-fullscreen-title",
|
|
226
|
-
);
|
|
227
|
-
expect(input).not.toBeNull();
|
|
228
|
-
if (!input) {
|
|
229
|
-
throw new Error("expected fullscreen title input");
|
|
230
|
-
}
|
|
231
|
-
input.value = "Desk Notes";
|
|
232
|
-
input.dispatchEvent(new Event("input"));
|
|
233
|
-
|
|
234
|
-
let detail: ComposeFullscreenCloseDetail | null = null;
|
|
235
|
-
document.addEventListener(
|
|
236
|
-
"jant:fullscreen-close",
|
|
237
|
-
(event) => {
|
|
238
|
-
detail = (event as CustomEvent<ComposeFullscreenCloseDetail>).detail;
|
|
239
|
-
},
|
|
240
|
-
{ once: true },
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
el.querySelector<HTMLButtonElement>(".compose-fullscreen-done")?.click();
|
|
244
|
-
|
|
245
|
-
expect(detail).toMatchObject({
|
|
246
|
-
json: {
|
|
247
|
-
type: "doc",
|
|
248
|
-
content: [{ type: "paragraph" }],
|
|
249
|
-
},
|
|
250
|
-
title: "Desk Notes",
|
|
251
|
-
showTitle: true,
|
|
252
|
-
replyExpanded: false,
|
|
253
|
-
selection: { from: 1, to: 1 },
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
207
|
it("restores and returns the current editor selection", async () => {
|
|
258
208
|
const el = document.createElement(
|
|
259
209
|
"jant-compose-fullscreen",
|
|
@@ -275,7 +225,6 @@ describe("JantComposeFullscreen", () => {
|
|
|
275
225
|
],
|
|
276
226
|
},
|
|
277
227
|
title: "",
|
|
278
|
-
showTitle: false,
|
|
279
228
|
selection: { from: 4, to: 4 },
|
|
280
229
|
labels,
|
|
281
230
|
},
|
|
@@ -318,7 +267,7 @@ describe("JantComposeFullscreen", () => {
|
|
|
318
267
|
detail: {
|
|
319
268
|
json: null,
|
|
320
269
|
title: "",
|
|
321
|
-
showTitle:
|
|
270
|
+
showTitle: true,
|
|
322
271
|
labels,
|
|
323
272
|
replyContext: {
|
|
324
273
|
contentHtml: "<p>A quiet sentence worth answering.</p>",
|
|
@@ -333,9 +282,10 @@ describe("JantComposeFullscreen", () => {
|
|
|
333
282
|
expect(el.textContent).toContain("A quiet sentence worth answering.");
|
|
334
283
|
expect(el.textContent).toContain("Mar 10, 2026");
|
|
335
284
|
expect(el.textContent).toContain("Show more");
|
|
285
|
+
// The reply's title field lives inside its own row, not the toolbar.
|
|
336
286
|
expect(
|
|
337
287
|
el.querySelector(
|
|
338
|
-
".compose-fullscreen-editor-row .compose-fullscreen-title-
|
|
288
|
+
".compose-fullscreen-editor-row .compose-fullscreen-title-reply",
|
|
339
289
|
),
|
|
340
290
|
).not.toBeNull();
|
|
341
291
|
});
|
|
@@ -353,7 +303,6 @@ describe("JantComposeFullscreen", () => {
|
|
|
353
303
|
detail: {
|
|
354
304
|
json: null,
|
|
355
305
|
title: "Follow-up",
|
|
356
|
-
showTitle: true,
|
|
357
306
|
labels,
|
|
358
307
|
replyContext: {
|
|
359
308
|
contentHtml: "<p>Thread seed.</p>",
|
|
@@ -390,7 +339,6 @@ describe("JantComposeFullscreen", () => {
|
|
|
390
339
|
detail: {
|
|
391
340
|
json: null,
|
|
392
341
|
title: "",
|
|
393
|
-
showTitle: false,
|
|
394
342
|
labels,
|
|
395
343
|
replyContext: {
|
|
396
344
|
contentHtml: "<p>One more thread preview.</p>",
|
|
@@ -436,7 +384,6 @@ describe("JantComposeFullscreen", () => {
|
|
|
436
384
|
detail: {
|
|
437
385
|
json: null,
|
|
438
386
|
title: "",
|
|
439
|
-
showTitle: false,
|
|
440
387
|
labels,
|
|
441
388
|
},
|
|
442
389
|
}),
|
|
@@ -491,7 +438,6 @@ describe("JantComposeFullscreen", () => {
|
|
|
491
438
|
],
|
|
492
439
|
},
|
|
493
440
|
title: "",
|
|
494
|
-
showTitle: false,
|
|
495
441
|
labels,
|
|
496
442
|
},
|
|
497
443
|
}),
|
|
@@ -249,7 +249,11 @@ export function convertComposeFormat(
|
|
|
249
249
|
if (prepend.length) content.unshift(...prepend);
|
|
250
250
|
|
|
251
251
|
out.bodyJson = content.length ? { type: "doc", content } : null;
|
|
252
|
-
|
|
252
|
+
// A title carried in from another format has nowhere to show unless the note's
|
|
253
|
+
// title field is open, so opening it is the only way not to drop it. The
|
|
254
|
+
// toggle is otherwise left alone, so note → link → note keeps the user's
|
|
255
|
+
// choice instead of resetting it.
|
|
256
|
+
if (to === "note" && out.title.trim().length > 0) out.showTitle = true;
|
|
253
257
|
|
|
254
258
|
return out;
|
|
255
259
|
}
|
|
@@ -11,6 +11,18 @@ import type { TableControlLabels } from "../tiptap/table-control-labels.js";
|
|
|
11
11
|
|
|
12
12
|
export type ComposeFormat = "note" | "link" | "quote";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* What one composer row says about itself. An editor owns its fields, so it
|
|
16
|
+
* owns these two answers too — the dialog reduces them over the rows it has
|
|
17
|
+
* instead of reading editors back out of the DOM while it renders.
|
|
18
|
+
*/
|
|
19
|
+
export interface ComposeRowStatus {
|
|
20
|
+
/** Anything worth keeping: the check behind "discard this?" and autosave. */
|
|
21
|
+
hasContent: boolean;
|
|
22
|
+
/** Enough to publish this row: its format's requirements met, fields valid. */
|
|
23
|
+
publishable: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
export interface ComposeAttachment {
|
|
15
27
|
clientId: string;
|
|
16
28
|
file: File;
|
|
@@ -25,6 +37,12 @@ export interface ComposeAttachment {
|
|
|
25
37
|
status: "pending" | "processing" | "uploading" | "done" | "error";
|
|
26
38
|
progress: number | null;
|
|
27
39
|
mediaId: string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Server URL of the uploaded file, set when the upload completes. Unlike
|
|
42
|
+
* `previewUrl` (a blob URL revoked on editor reset) it stays valid, so
|
|
43
|
+
* draft restore can rehydrate the attachment after the dialog closed.
|
|
44
|
+
*/
|
|
45
|
+
remoteUrl: string | null;
|
|
28
46
|
alt: string;
|
|
29
47
|
error: string | null;
|
|
30
48
|
/** Text content preview for text files (first ~100 chars) */
|
|
@@ -64,6 +82,23 @@ export interface DraftItem {
|
|
|
64
82
|
}[];
|
|
65
83
|
}
|
|
66
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Snapshot of a media attachment whose upload completed. Stored in the local
|
|
87
|
+
* draft (and passed by the bridge on failure restore) so uploaded images
|
|
88
|
+
* survive a failed publish or a page reload — the bytes are already on the
|
|
89
|
+
* server, only the reference needs restoring.
|
|
90
|
+
*/
|
|
91
|
+
export interface LocalDraftMedia {
|
|
92
|
+
clientId: string;
|
|
93
|
+
mediaId: string;
|
|
94
|
+
url: string;
|
|
95
|
+
mimeType: string;
|
|
96
|
+
name?: string;
|
|
97
|
+
alt?: string;
|
|
98
|
+
summary?: string | null;
|
|
99
|
+
chars?: number | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
67
102
|
export interface LocalDraft {
|
|
68
103
|
format: ComposeFormat;
|
|
69
104
|
title: string;
|
|
@@ -87,9 +122,15 @@ export interface LocalDraft {
|
|
|
87
122
|
summary: string;
|
|
88
123
|
}>;
|
|
89
124
|
attachmentOrder?: string[];
|
|
125
|
+
mediaAttachments?: LocalDraftMedia[];
|
|
90
126
|
/** Present when the draft is a multi-post thread */
|
|
91
127
|
threadItems?: Array<{
|
|
92
128
|
format: ComposeFormat;
|
|
129
|
+
/** Per-post publish date; empty means "follows the thread". */
|
|
130
|
+
publishedAtInput?: string;
|
|
131
|
+
publishedAtTimeMinutes?: number | null;
|
|
132
|
+
/** Per-post permalink; empty means the server assigns a random id. */
|
|
133
|
+
slug?: string;
|
|
93
134
|
title: string;
|
|
94
135
|
bodyJson: JSONContent | null;
|
|
95
136
|
url: string;
|
|
@@ -102,6 +143,7 @@ export interface LocalDraft {
|
|
|
102
143
|
summary: string;
|
|
103
144
|
}>;
|
|
104
145
|
attachmentOrder?: string[];
|
|
146
|
+
mediaAttachments?: LocalDraftMedia[];
|
|
105
147
|
}>;
|
|
106
148
|
savedAt: number;
|
|
107
149
|
}
|
|
@@ -229,9 +271,9 @@ export interface ComposeLabels {
|
|
|
229
271
|
threadLimitReached: string;
|
|
230
272
|
showMore: string;
|
|
231
273
|
showLess: string;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
274
|
+
closeCompose: string;
|
|
275
|
+
editing: string;
|
|
276
|
+
composeDialogLabel: string;
|
|
235
277
|
slashHint: string;
|
|
236
278
|
tableControls: TableControlLabels;
|
|
237
279
|
collectionFormLabels: CollectionFormLabels;
|
|
@@ -301,6 +343,8 @@ export interface ComposeSubmitDetail {
|
|
|
301
343
|
collectionIds: string[];
|
|
302
344
|
attachments: ComposeSubmitAttachment[];
|
|
303
345
|
editPostId?: string;
|
|
346
|
+
/** Identifies an existing server draft loaded through the drafts workflow. */
|
|
347
|
+
draftSourceId?: string;
|
|
304
348
|
replyToId?: string;
|
|
305
349
|
quietReply?: boolean;
|
|
306
350
|
replyThreadRootId?: string;
|
|
@@ -13,6 +13,7 @@ import { classMap } from "lit/directives/class-map.js";
|
|
|
13
13
|
import { unsafeSVG } from "lit/directives/unsafe-svg.js";
|
|
14
14
|
import { NAVIGATION_SETTINGS_PATH } from "../../lib/settings-paths.js";
|
|
15
15
|
import { openNewCompose } from "../compose-launch.js";
|
|
16
|
+
import { publicPath } from "../runtime-paths.js";
|
|
16
17
|
import { getBestFieldSearchRank, normalizeSearch } from "../search-rank.js";
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
@@ -23,6 +24,7 @@ interface PaletteItem {
|
|
|
23
24
|
title: string;
|
|
24
25
|
path: string;
|
|
25
26
|
type: "post" | "collection" | "system";
|
|
27
|
+
status?: "draft" | "published";
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
interface CommandItem {
|
|
@@ -351,7 +353,7 @@ export class JantCommandPalette extends LitElement {
|
|
|
351
353
|
const q = normalizeSearch(this._query);
|
|
352
354
|
const navItems: DisplayItem[] = this.#navigateItems.map((item) => ({
|
|
353
355
|
label: item.title,
|
|
354
|
-
secondary: item.path,
|
|
356
|
+
secondary: item.status === "draft" ? `Draft · ${item.path}` : item.path,
|
|
355
357
|
icon: ICONS[item.type],
|
|
356
358
|
}));
|
|
357
359
|
|
|
@@ -378,7 +380,7 @@ export class JantCommandPalette extends LitElement {
|
|
|
378
380
|
#executeSearch(query: string) {
|
|
379
381
|
saveHistory(SEARCH_HISTORY_KEY, query);
|
|
380
382
|
this.close();
|
|
381
|
-
window.location.href = `/search?q=${encodeURIComponent(query)}
|
|
383
|
+
window.location.href = publicPath(`/search?q=${encodeURIComponent(query)}`);
|
|
382
384
|
}
|
|
383
385
|
|
|
384
386
|
#executeItem(index: number) {
|
|
@@ -407,7 +409,7 @@ export class JantCommandPalette extends LitElement {
|
|
|
407
409
|
if (displayItem?.pathTarget) {
|
|
408
410
|
saveHistory(NAV_HISTORY_KEY, displayItem.pathTarget);
|
|
409
411
|
this.close();
|
|
410
|
-
window.location.href = displayItem.pathTarget;
|
|
412
|
+
window.location.href = publicPath(displayItem.pathTarget);
|
|
411
413
|
return;
|
|
412
414
|
}
|
|
413
415
|
|
|
@@ -429,11 +431,15 @@ export class JantCommandPalette extends LitElement {
|
|
|
429
431
|
saveHistory(NAV_HISTORY_KEY, item.path);
|
|
430
432
|
this.close();
|
|
431
433
|
if (item.type === "system") {
|
|
432
|
-
window.location.href = item.path;
|
|
434
|
+
window.location.href = publicPath(item.path);
|
|
433
435
|
} else if (item.type === "collection") {
|
|
434
|
-
window.location.href = `/collections/${item.path}
|
|
436
|
+
window.location.href = publicPath(`/collections/${item.path}`);
|
|
435
437
|
} else {
|
|
436
|
-
window.location.href =
|
|
438
|
+
window.location.href = publicPath(
|
|
439
|
+
item.status === "draft"
|
|
440
|
+
? `/preview/${item.path}?edit=1`
|
|
441
|
+
: `/${item.path}`,
|
|
442
|
+
);
|
|
437
443
|
}
|
|
438
444
|
}
|
|
439
445
|
}
|