@jant/core 0.6.12 → 0.6.14
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 +2 -10
- package/bin/commands/migrate-text-attachments.js +1 -3
- package/bin/commands/site/export.js +1 -3
- package/bin/commands/telegram/register-webhooks.js +9 -3
- package/bin/lib/site-snapshot.js +1 -5
- package/dist/{app-D-zAhayc.js → app-2i7-WQHg.js} +426 -450
- package/dist/app-DZDlmh7C.js +6 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-S1cdvulk.js → client-BvID5ucz.js} +1 -1
- package/dist/client/_assets/client-Cevfpm8H.css +2 -0
- package/dist/client/_assets/{client-auth-YW92ZH7u.js → client-auth-v3yvhbgZ.js} +596 -590
- package/dist/{export-Ckwh4BiE.js → export-DWn149b7.js} +24 -19
- package/dist/{github-sync-BMmK3JZ0.js → github-sync-C0zyF6XS.js} +2 -2
- package/dist/{github-sync-Dl9wQMar.js → github-sync-Ckk0sJxK.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/node.js +4 -4
- package/package.json +1 -1
- package/src/__tests__/export-service.test.ts +1 -2
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +0 -1
- package/src/__tests__/import-site-command.test.ts +1 -2
- package/src/client/__tests__/site-header-nav.test.ts +36 -2
- package/src/client/components/__tests__/compose-title-from-heading.test.ts +57 -0
- package/src/client/components/__tests__/jant-command-palette.test.ts +137 -10
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +3 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
- package/src/client/components/__tests__/jant-nav-manager.test.ts +107 -22
- package/src/client/components/__tests__/jant-settings-general.test.ts +1 -0
- package/src/client/components/compose-title-from-heading.ts +66 -0
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/inline-image-issues.ts +17 -0
- package/src/client/components/jant-command-palette.ts +100 -70
- package/src/client/components/jant-compose-dialog.ts +3 -1
- package/src/client/components/jant-compose-editor.ts +78 -1
- package/src/client/components/jant-compose-fullscreen.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +245 -155
- package/src/client/components/nav-manager-types.ts +0 -10
- package/src/client/site-header-nav.d.ts +1 -0
- package/src/client/site-header-nav.js +105 -47
- package/src/client/tiptap/__tests__/block-insertion.test.ts +35 -0
- package/src/client/tiptap/__tests__/footnotes.test.ts +284 -0
- package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +148 -1
- package/src/client/tiptap/bubble-menu.ts +3 -3
- package/src/client/tiptap/create-editor.ts +7 -0
- package/src/client/tiptap/extensions.ts +7 -2
- package/src/client/tiptap/footnotes.ts +156 -0
- package/src/client/tiptap/image-input-rules.ts +48 -0
- package/src/client/tiptap/image-node.ts +189 -7
- package/src/client/tiptap/markdown-clipboard.ts +7 -2
- package/src/client-auth.ts +0 -1
- package/src/i18n/locales/public/en.po +20 -4
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +20 -4
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +20 -4
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +23 -20
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +23 -20
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +23 -20
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +4 -0
- package/src/lib/__tests__/navigation.test.ts +4 -37
- package/src/lib/__tests__/resolve-config.test.ts +6 -1
- package/src/lib/__tests__/view.test.ts +6 -30
- package/src/lib/github-sync-site-config.ts +0 -2
- package/src/lib/hugo-markdown.ts +1 -1
- package/src/lib/markdown-manager.ts +14 -5
- package/src/lib/navigation.ts +5 -22
- package/src/lib/resolve-config.ts +9 -2
- package/src/lib/site-header-fragment.tsx +37 -0
- package/src/lib/view.ts +2 -7
- package/src/node/__tests__/cli-site-snapshot.test.ts +45 -45
- package/src/routes/api/__tests__/nav-items.test.ts +26 -0
- package/src/routes/api/export.ts +0 -2
- package/src/routes/api/internal/__tests__/uploads.test.ts +9 -13
- package/src/routes/api/internal/sites.ts +5 -5
- package/src/routes/api/nav-items.ts +25 -4
- package/src/routes/dash/settings.tsx +8 -2
- package/src/routes/feed/__tests__/feed.test.ts +42 -0
- package/src/routes/feed/__tests__/sitemap.test.ts +2 -13
- package/src/routes/feed/feed.ts +1 -0
- package/src/routes/feed/sitemap.ts +2 -6
- package/src/routes/pages/archive.tsx +1 -0
- package/src/routes/pages/collection.tsx +1 -0
- package/src/routes/pages/featured.tsx +0 -4
- package/src/routes/pages/home.tsx +4 -51
- package/src/routes/pages/latest.tsx +2 -51
- package/src/services/__tests__/github-app-installations.test.ts +10 -10
- package/src/services/__tests__/navigation.test.ts +41 -0
- package/src/services/__tests__/post-timeline.test.ts +56 -0
- package/src/services/__tests__/post.test.ts +33 -0
- package/src/services/__tests__/settings.test.ts +5 -20
- package/src/services/export-theme/assets/client-site.js +10 -10
- package/src/services/export-theme/layouts/_default/rss.xml +5 -15
- package/src/services/export-theme/layouts/index.html +26 -39
- package/src/services/export.ts +5 -10
- package/src/services/github-app-installations.ts +1 -3
- package/src/services/navigation.ts +44 -13
- package/src/services/post.ts +80 -57
- package/src/services/settings.ts +0 -10
- package/src/services/site-admin.ts +0 -2
- package/src/services/upload-session.ts +4 -18
- package/src/styles/tokens.css +2 -2
- package/src/styles/ui.css +316 -34
- package/src/types/bindings.ts +0 -1
- package/src/types/config.ts +3 -7
- package/src/ui/compose/ComposeDialog.tsx +28 -0
- package/src/ui/dash/appearance/NavigationContent.tsx +1 -1
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
- package/src/ui/feed/__tests__/timeline-cards.test.ts +8 -3
- package/src/ui/layouts/BaseLayout.tsx +1 -1
- package/src/ui/layouts/SiteLayout.tsx +266 -227
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +48 -0
- package/dist/app-B5P1b4TF.js +0 -6
- package/dist/client/_assets/client-BUxR-E8O.css +0 -2
- package/src/client/nav-manager-bridge.ts +0 -54
|
@@ -104,6 +104,46 @@ const suggestedLinks: NavManagerSuggestedLink[] = [
|
|
|
104
104
|
},
|
|
105
105
|
];
|
|
106
106
|
|
|
107
|
+
function renderHeaderFragment(label: string): string {
|
|
108
|
+
return `
|
|
109
|
+
<header class="site-header" data-site-header-fragment="header">
|
|
110
|
+
<div class="site-header-inner">
|
|
111
|
+
<div class="site-header-top">
|
|
112
|
+
<a href="/" class="site-logo">Test Site</a>
|
|
113
|
+
<nav class="site-header-nav" aria-label="Primary">
|
|
114
|
+
<a href="/now" class="site-header-link">${label}</a>
|
|
115
|
+
</nav>
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
class="site-header-hamburger"
|
|
119
|
+
aria-controls="site-nav-drawer"
|
|
120
|
+
aria-expanded="false"
|
|
121
|
+
></button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</header>
|
|
125
|
+
<div
|
|
126
|
+
class="site-nav-drawer-backdrop"
|
|
127
|
+
data-site-header-fragment="drawer-backdrop"
|
|
128
|
+
aria-hidden="true"
|
|
129
|
+
></div>
|
|
130
|
+
<div
|
|
131
|
+
id="site-nav-drawer"
|
|
132
|
+
class="site-nav-drawer"
|
|
133
|
+
data-site-header-fragment="drawer"
|
|
134
|
+
aria-hidden="true"
|
|
135
|
+
inert
|
|
136
|
+
>
|
|
137
|
+
<button class="site-nav-drawer-close" type="button"></button>
|
|
138
|
+
<a href="/now" class="site-nav-drawer-link">${label}</a>
|
|
139
|
+
</div>
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function installCurrentHeaderFragment(label = "Old"): void {
|
|
144
|
+
document.body.insertAdjacentHTML("afterbegin", renderHeaderFragment(label));
|
|
145
|
+
}
|
|
146
|
+
|
|
107
147
|
function requireElement<T>(value: T | null, message: string): T {
|
|
108
148
|
if (!value) {
|
|
109
149
|
throw new Error(message);
|
|
@@ -148,6 +188,12 @@ function getSortableOptions(
|
|
|
148
188
|
return options as Record<string, ((event: unknown) => void) | undefined>;
|
|
149
189
|
}
|
|
150
190
|
|
|
191
|
+
async function flushAsyncWork(): Promise<void> {
|
|
192
|
+
await Promise.resolve();
|
|
193
|
+
await Promise.resolve();
|
|
194
|
+
await Promise.resolve();
|
|
195
|
+
}
|
|
196
|
+
|
|
151
197
|
async function createElement(): Promise<JantNavManager> {
|
|
152
198
|
const el = document.createElement("jant-nav-manager") as JantNavManager;
|
|
153
199
|
el.labels = labels;
|
|
@@ -170,6 +216,7 @@ describe("JantNavManager", () => {
|
|
|
170
216
|
vi.fn().mockResolvedValue({
|
|
171
217
|
ok: true,
|
|
172
218
|
status: 200,
|
|
219
|
+
json: async () => ({ ...items[1] }),
|
|
173
220
|
}),
|
|
174
221
|
);
|
|
175
222
|
});
|
|
@@ -209,8 +256,7 @@ describe("JantNavManager", () => {
|
|
|
209
256
|
});
|
|
210
257
|
|
|
211
258
|
await el.updateComplete;
|
|
212
|
-
await
|
|
213
|
-
await Promise.resolve();
|
|
259
|
+
await flushAsyncWork();
|
|
214
260
|
|
|
215
261
|
expect(getListIds(headerList)).toEqual(["nav-1"]);
|
|
216
262
|
expect(getListIds(moreList)).toEqual(["nav-2", "nav-3"]);
|
|
@@ -234,6 +280,9 @@ describe("JantNavManager", () => {
|
|
|
234
280
|
"/api/nav-items/nav-2/move",
|
|
235
281
|
expect.objectContaining({
|
|
236
282
|
method: "PUT",
|
|
283
|
+
headers: expect.objectContaining({
|
|
284
|
+
"X-Jant-Site-Header": "include",
|
|
285
|
+
}),
|
|
237
286
|
body: JSON.stringify({
|
|
238
287
|
after: null,
|
|
239
288
|
before: "nav-3",
|
|
@@ -293,22 +342,28 @@ describe("JantNavManager", () => {
|
|
|
293
342
|
url: "/now",
|
|
294
343
|
placement: "header",
|
|
295
344
|
};
|
|
345
|
+
installCurrentHeaderFragment("Old");
|
|
296
346
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
297
347
|
ok: true,
|
|
298
348
|
status: 201,
|
|
299
|
-
json: async () =>
|
|
349
|
+
json: async () => ({
|
|
350
|
+
...created,
|
|
351
|
+
headerHtml: renderHeaderFragment("Now"),
|
|
352
|
+
}),
|
|
300
353
|
});
|
|
301
354
|
vi.stubGlobal("fetch", fetchMock);
|
|
302
355
|
|
|
303
356
|
addButton.click();
|
|
304
|
-
await
|
|
305
|
-
await Promise.resolve();
|
|
357
|
+
await flushAsyncWork();
|
|
306
358
|
await el.updateComplete;
|
|
307
359
|
|
|
308
360
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
309
361
|
"/api/nav-items",
|
|
310
362
|
expect.objectContaining({
|
|
311
363
|
method: "POST",
|
|
364
|
+
headers: expect.objectContaining({
|
|
365
|
+
"X-Jant-Site-Header": "include",
|
|
366
|
+
}),
|
|
312
367
|
body: JSON.stringify({
|
|
313
368
|
type: "collection",
|
|
314
369
|
collectionId: "col_now",
|
|
@@ -325,12 +380,25 @@ describe("JantNavManager", () => {
|
|
|
325
380
|
),
|
|
326
381
|
).toContain("nav-now");
|
|
327
382
|
expect(el.querySelector(".nav-suggestion-item")).toBeNull();
|
|
383
|
+
expect(
|
|
384
|
+
document
|
|
385
|
+
.querySelector<HTMLElement>('[data-site-header-fragment="header"]')
|
|
386
|
+
?.textContent?.trim(),
|
|
387
|
+
).toContain("Now");
|
|
328
388
|
});
|
|
329
389
|
|
|
330
|
-
it("confirms before
|
|
390
|
+
it("confirms before deleting a navigation item", async () => {
|
|
331
391
|
const el = await createElement();
|
|
332
|
-
const
|
|
333
|
-
|
|
392
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
393
|
+
ok: true,
|
|
394
|
+
status: 200,
|
|
395
|
+
json: async () => ({
|
|
396
|
+
success: true,
|
|
397
|
+
headerHtml: renderHeaderFragment("Links"),
|
|
398
|
+
}),
|
|
399
|
+
});
|
|
400
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
401
|
+
installCurrentHeaderFragment("About");
|
|
334
402
|
|
|
335
403
|
(
|
|
336
404
|
el as unknown as { _editingId: string | null; requestUpdate: () => void }
|
|
@@ -343,7 +411,7 @@ describe("JantNavManager", () => {
|
|
|
343
411
|
"expected nav delete button",
|
|
344
412
|
);
|
|
345
413
|
deleteButton.click();
|
|
346
|
-
await
|
|
414
|
+
await flushAsyncWork();
|
|
347
415
|
|
|
348
416
|
const host = requireElement(
|
|
349
417
|
document.querySelector<HTMLElement>("jant-confirm-dialog"),
|
|
@@ -356,19 +424,37 @@ describe("JantNavManager", () => {
|
|
|
356
424
|
"expected confirm button",
|
|
357
425
|
);
|
|
358
426
|
confirmButton.click();
|
|
359
|
-
await
|
|
360
|
-
await
|
|
427
|
+
await flushAsyncWork();
|
|
428
|
+
await el.updateComplete;
|
|
361
429
|
|
|
362
|
-
expect(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
430
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
431
|
+
"/api/nav-items/nav-1",
|
|
432
|
+
expect.objectContaining({
|
|
433
|
+
method: "DELETE",
|
|
434
|
+
headers: expect.objectContaining({
|
|
435
|
+
"X-Jant-Site-Header": "include",
|
|
436
|
+
}),
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
expect(
|
|
440
|
+
getListIds(
|
|
441
|
+
requireElement(
|
|
442
|
+
el.querySelector<HTMLElement>("#nav-items-header"),
|
|
443
|
+
"expected header nav list",
|
|
444
|
+
),
|
|
445
|
+
),
|
|
446
|
+
).toEqual(["nav-2"]);
|
|
447
|
+
expect(
|
|
448
|
+
document
|
|
449
|
+
.querySelector<HTMLElement>('[data-site-header-fragment="header"]')
|
|
450
|
+
?.textContent?.trim(),
|
|
451
|
+
).toContain("Links");
|
|
366
452
|
});
|
|
367
453
|
|
|
368
|
-
it("does not
|
|
454
|
+
it("does not delete when confirmation is canceled", async () => {
|
|
369
455
|
const el = await createElement();
|
|
370
|
-
const
|
|
371
|
-
|
|
456
|
+
const fetchMock = vi.fn();
|
|
457
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
372
458
|
|
|
373
459
|
(
|
|
374
460
|
el as unknown as { _editingId: string | null; requestUpdate: () => void }
|
|
@@ -381,7 +467,7 @@ describe("JantNavManager", () => {
|
|
|
381
467
|
"expected nav delete button",
|
|
382
468
|
);
|
|
383
469
|
deleteButton.click();
|
|
384
|
-
await
|
|
470
|
+
await flushAsyncWork();
|
|
385
471
|
|
|
386
472
|
const host = requireElement(
|
|
387
473
|
document.querySelector<HTMLElement>("jant-confirm-dialog"),
|
|
@@ -394,9 +480,8 @@ describe("JantNavManager", () => {
|
|
|
394
480
|
"expected cancel button",
|
|
395
481
|
);
|
|
396
482
|
cancelButton.click();
|
|
397
|
-
await
|
|
398
|
-
await Promise.resolve();
|
|
483
|
+
await flushAsyncWork();
|
|
399
484
|
|
|
400
|
-
expect(
|
|
485
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
401
486
|
});
|
|
402
487
|
});
|
|
@@ -657,6 +657,7 @@ describe("JantSettingsGeneral", () => {
|
|
|
657
657
|
const d = detail as unknown as SettingsSaveDetail;
|
|
658
658
|
expect(d.endpoint).toBe("/settings/general/home");
|
|
659
659
|
expect(d.section).toBe("home");
|
|
660
|
+
expect(d.data).not.toHaveProperty("homeDefaultView");
|
|
660
661
|
expect(d.data.showJantBrandingOnHome).toBe(true);
|
|
661
662
|
expect(siteSaveBtn?.disabled).toBe(true);
|
|
662
663
|
});
|
|
@@ -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
|
+
}
|
|
@@ -30,6 +30,19 @@ interface CommandItem {
|
|
|
30
30
|
action: () => void;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
interface DisplayItem {
|
|
34
|
+
label: string;
|
|
35
|
+
secondary?: string;
|
|
36
|
+
icon: string;
|
|
37
|
+
/** For search-mode items: the query to execute */
|
|
38
|
+
searchQuery?: string;
|
|
39
|
+
/** For path-mode items: the local path to open */
|
|
40
|
+
pathTarget?: string;
|
|
41
|
+
/** Keep cross-mode actions visible below the scrollable result list. */
|
|
42
|
+
placement?: "results" | "footer";
|
|
43
|
+
shortcut?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
// ---------------------------------------------------------------------------
|
|
34
47
|
// SVG icon paths (reuse existing Lucide-style icons from the codebase)
|
|
35
48
|
// ---------------------------------------------------------------------------
|
|
@@ -280,15 +293,7 @@ export class JantCommandPalette extends LitElement {
|
|
|
280
293
|
return COMMANDS.filter((c) => !q || c.label.toLowerCase().includes(q));
|
|
281
294
|
}
|
|
282
295
|
|
|
283
|
-
get #displayItems():
|
|
284
|
-
label: string;
|
|
285
|
-
secondary?: string;
|
|
286
|
-
icon: string;
|
|
287
|
-
/** For search-mode items: the query to execute */
|
|
288
|
-
searchQuery?: string;
|
|
289
|
-
/** For path-mode items: the local path to open */
|
|
290
|
-
pathTarget?: string;
|
|
291
|
-
}> {
|
|
296
|
+
get #displayItems(): DisplayItem[] {
|
|
292
297
|
const mode = this.#mode;
|
|
293
298
|
|
|
294
299
|
if (mode === "command") {
|
|
@@ -320,13 +325,7 @@ export class JantCommandPalette extends LitElement {
|
|
|
320
325
|
if (mode === "path") {
|
|
321
326
|
const target = getPathCommandTarget(this._query);
|
|
322
327
|
const searchQuery = getPathCommandSearchQuery(this._query);
|
|
323
|
-
const items:
|
|
324
|
-
label: string;
|
|
325
|
-
secondary?: string;
|
|
326
|
-
icon: string;
|
|
327
|
-
searchQuery?: string;
|
|
328
|
-
pathTarget?: string;
|
|
329
|
-
}> = [
|
|
328
|
+
const items: DisplayItem[] = [
|
|
330
329
|
{
|
|
331
330
|
label: `Go to ${target}`,
|
|
332
331
|
icon: ICONS.path,
|
|
@@ -346,25 +345,25 @@ export class JantCommandPalette extends LitElement {
|
|
|
346
345
|
}
|
|
347
346
|
|
|
348
347
|
// Navigate mode — show all items when no query (autocomplete)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
searchQuery?: string;
|
|
354
|
-
pathTarget?: string;
|
|
355
|
-
}> = this.#navigateItems.map((item) => ({
|
|
348
|
+
// Rank direct destinations and let the scrollable result list handle
|
|
349
|
+
// overflow without hiding valid matches.
|
|
350
|
+
const q = normalizeSearch(this._query);
|
|
351
|
+
const navItems: DisplayItem[] = this.#navigateItems.map((item) => ({
|
|
356
352
|
label: item.title,
|
|
357
|
-
secondary: item.
|
|
353
|
+
secondary: item.path,
|
|
358
354
|
icon: ICONS[item.type],
|
|
359
355
|
}));
|
|
360
356
|
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
357
|
+
// Full-text search is a persistent action, not another navigation result.
|
|
358
|
+
// Keep it outside the scrollable list while retaining it as the final
|
|
359
|
+
// keyboard option.
|
|
360
|
+
if (q) {
|
|
364
361
|
navItems.push({
|
|
365
|
-
label: `Search for "${this._query.trim()}"`,
|
|
362
|
+
label: `Search all content for "${this._query.trim()}"`,
|
|
366
363
|
icon: ICONS.search,
|
|
367
364
|
searchQuery: this._query.trim(),
|
|
365
|
+
placement: "footer",
|
|
366
|
+
...(navItems.length > 0 ? { shortcut: "⌘/Ctrl ↵" } : {}),
|
|
368
367
|
});
|
|
369
368
|
}
|
|
370
369
|
|
|
@@ -375,6 +374,12 @@ export class JantCommandPalette extends LitElement {
|
|
|
375
374
|
// Actions
|
|
376
375
|
// -----------------------------------------------------------------------
|
|
377
376
|
|
|
377
|
+
#executeSearch(query: string) {
|
|
378
|
+
saveHistory(SEARCH_HISTORY_KEY, query);
|
|
379
|
+
this.close();
|
|
380
|
+
window.location.href = `/search?q=${encodeURIComponent(query)}`;
|
|
381
|
+
}
|
|
382
|
+
|
|
378
383
|
#executeItem(index: number) {
|
|
379
384
|
const mode = this.#mode;
|
|
380
385
|
|
|
@@ -391,9 +396,7 @@ export class JantCommandPalette extends LitElement {
|
|
|
391
396
|
const displayItem = this.#displayItems[index];
|
|
392
397
|
const q = displayItem?.searchQuery;
|
|
393
398
|
if (q) {
|
|
394
|
-
|
|
395
|
-
this.close();
|
|
396
|
-
window.location.href = `/search?q=${encodeURIComponent(q)}`;
|
|
399
|
+
this.#executeSearch(q);
|
|
397
400
|
}
|
|
398
401
|
return;
|
|
399
402
|
}
|
|
@@ -408,10 +411,7 @@ export class JantCommandPalette extends LitElement {
|
|
|
408
411
|
}
|
|
409
412
|
|
|
410
413
|
if (displayItem?.searchQuery) {
|
|
411
|
-
|
|
412
|
-
saveHistory(SEARCH_HISTORY_KEY, q);
|
|
413
|
-
this.close();
|
|
414
|
-
window.location.href = `/search?q=${encodeURIComponent(q)}`;
|
|
414
|
+
this.#executeSearch(displayItem.searchQuery);
|
|
415
415
|
}
|
|
416
416
|
return;
|
|
417
417
|
}
|
|
@@ -419,10 +419,7 @@ export class JantCommandPalette extends LitElement {
|
|
|
419
419
|
// Navigate mode — check if the selected item is a search fallback
|
|
420
420
|
const displayItem = this.#displayItems[index];
|
|
421
421
|
if (displayItem?.searchQuery) {
|
|
422
|
-
|
|
423
|
-
saveHistory(SEARCH_HISTORY_KEY, q);
|
|
424
|
-
this.close();
|
|
425
|
-
window.location.href = `/search?q=${encodeURIComponent(q)}`;
|
|
422
|
+
this.#executeSearch(displayItem.searchQuery);
|
|
426
423
|
return;
|
|
427
424
|
}
|
|
428
425
|
|
|
@@ -475,6 +472,19 @@ export class JantCommandPalette extends LitElement {
|
|
|
475
472
|
return;
|
|
476
473
|
}
|
|
477
474
|
|
|
475
|
+
if (
|
|
476
|
+
event.key === "Enter" &&
|
|
477
|
+
(event.metaKey || event.ctrlKey) &&
|
|
478
|
+
this.#mode === "navigate"
|
|
479
|
+
) {
|
|
480
|
+
const query = this._query.trim();
|
|
481
|
+
if (query) {
|
|
482
|
+
event.preventDefault();
|
|
483
|
+
this.#executeSearch(query);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
478
488
|
if (event.key === "Enter") {
|
|
479
489
|
event.preventDefault();
|
|
480
490
|
this.#executeItem(this._selectedIndex);
|
|
@@ -521,10 +531,49 @@ export class JantCommandPalette extends LitElement {
|
|
|
521
531
|
// Render
|
|
522
532
|
// -----------------------------------------------------------------------
|
|
523
533
|
|
|
534
|
+
#renderItem(item: DisplayItem, index: number, footer = false) {
|
|
535
|
+
return html`
|
|
536
|
+
<div
|
|
537
|
+
id="command-palette-item-${index}"
|
|
538
|
+
class=${classMap({
|
|
539
|
+
"command-palette-result": true,
|
|
540
|
+
"command-palette-result-selected": index === this._selectedIndex,
|
|
541
|
+
"command-palette-search-action": footer,
|
|
542
|
+
})}
|
|
543
|
+
role="option"
|
|
544
|
+
aria-selected=${index === this._selectedIndex}
|
|
545
|
+
@click=${this.#handleItemClick(index)}
|
|
546
|
+
>
|
|
547
|
+
<span class="command-palette-result-icon">${unsafeSVG(item.icon)}</span>
|
|
548
|
+
<span class="command-palette-result-body">
|
|
549
|
+
<span class="command-palette-result-title">${item.label}</span>
|
|
550
|
+
${item.secondary
|
|
551
|
+
? html`<span class="command-palette-result-path"
|
|
552
|
+
>${item.secondary}</span
|
|
553
|
+
>`
|
|
554
|
+
: nothing}
|
|
555
|
+
</span>
|
|
556
|
+
${item.shortcut
|
|
557
|
+
? html`<kbd
|
|
558
|
+
class="command-palette-result-shortcut"
|
|
559
|
+
aria-label="Command or Control plus Enter"
|
|
560
|
+
>${item.shortcut}</kbd
|
|
561
|
+
>`
|
|
562
|
+
: nothing}
|
|
563
|
+
</div>
|
|
564
|
+
`;
|
|
565
|
+
}
|
|
566
|
+
|
|
524
567
|
render() {
|
|
525
568
|
if (!this._open) return nothing;
|
|
526
569
|
|
|
527
570
|
const items = this.#displayItems;
|
|
571
|
+
const resultItems = items
|
|
572
|
+
.map((item, index) => ({ item, index }))
|
|
573
|
+
.filter(({ item }) => item.placement !== "footer");
|
|
574
|
+
const footerItems = items
|
|
575
|
+
.map((item, index) => ({ item, index }))
|
|
576
|
+
.filter(({ item }) => item.placement === "footer");
|
|
528
577
|
|
|
529
578
|
return html`
|
|
530
579
|
<dialog
|
|
@@ -563,41 +612,22 @@ export class JantCommandPalette extends LitElement {
|
|
|
563
612
|
|
|
564
613
|
${items.length > 0
|
|
565
614
|
? html`
|
|
566
|
-
<
|
|
615
|
+
<div
|
|
567
616
|
id="command-palette-results"
|
|
568
|
-
class="command-palette-results"
|
|
617
|
+
class="command-palette-results-container"
|
|
569
618
|
role="listbox"
|
|
570
619
|
>
|
|
571
|
-
${
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
role="option"
|
|
581
|
-
aria-selected=${i === this._selectedIndex}
|
|
582
|
-
@click=${this.#handleItemClick(i)}
|
|
583
|
-
>
|
|
584
|
-
<span class="command-palette-result-icon"
|
|
585
|
-
>${unsafeSVG(item.icon)}</span
|
|
586
|
-
>
|
|
587
|
-
<span class="command-palette-result-body">
|
|
588
|
-
<span class="command-palette-result-title"
|
|
589
|
-
>${item.label}</span
|
|
590
|
-
>
|
|
591
|
-
${item.secondary
|
|
592
|
-
? html`<span class="command-palette-result-path"
|
|
593
|
-
>${item.secondary}</span
|
|
594
|
-
>`
|
|
595
|
-
: nothing}
|
|
596
|
-
</span>
|
|
597
|
-
</li>
|
|
598
|
-
`,
|
|
620
|
+
${resultItems.length > 0
|
|
621
|
+
? html`<div class="command-palette-results">
|
|
622
|
+
${resultItems.map(({ item, index }) =>
|
|
623
|
+
this.#renderItem(item, index),
|
|
624
|
+
)}
|
|
625
|
+
</div>`
|
|
626
|
+
: nothing}
|
|
627
|
+
${footerItems.map(({ item, index }) =>
|
|
628
|
+
this.#renderItem(item, index, true),
|
|
599
629
|
)}
|
|
600
|
-
</
|
|
630
|
+
</div>
|
|
601
631
|
`
|
|
602
632
|
: this._query.trim() && !this._loading
|
|
603
633
|
? html`<div class="command-palette-empty">No results</div>`
|
|
@@ -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) => [
|
|
@@ -4031,7 +4033,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
4031
4033
|
<span class="compose-alt-title">${this.labels.attachedText}</span>
|
|
4032
4034
|
<button
|
|
4033
4035
|
type="button"
|
|
4034
|
-
class="compose-
|
|
4036
|
+
class="compose-attached-done"
|
|
4035
4037
|
@click=${() => this._doneAttachedPanel()}
|
|
4036
4038
|
>
|
|
4037
4039
|
${this.labels.done}
|