@jant/core 0.6.11 → 0.6.12
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 +45 -20
- package/dist/app-B5P1b4TF.js +6 -0
- package/dist/{app-CpmficmQ.js → app-D-zAhayc.js} +331 -55
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-BUxR-E8O.css +2 -0
- package/dist/client/_assets/{client-Dd9U383b.js → client-S1cdvulk.js} +1 -1
- package/dist/client/_assets/{client-auth-DkpSdIDz.js → client-auth-YW92ZH7u.js} +116 -55
- package/dist/{export-Ba7NJImL.js → export-Ckwh4BiE.js} +4 -4
- package/dist/{github-sync-BD4w2m8-.js → github-sync-BMmK3JZ0.js} +2 -2
- package/dist/{github-sync-Cb4_6_i7.js → github-sync-Dl9wQMar.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/node.js +4 -4
- package/package.json +1 -1
- package/src/__tests__/import-site-command.test.ts +75 -0
- package/src/__tests__/vite-dev-plugins.test.ts +34 -0
- package/src/client/__tests__/compose-launch.test.ts +50 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +74 -18
- package/src/client/components/__tests__/jant-command-palette.test.ts +63 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +33 -0
- package/src/client/components/__tests__/jant-compose-editor-rehost-notice.test.ts +20 -1
- package/src/client/components/__tests__/jant-nav-manager.test.ts +77 -1
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +6 -0
- package/src/client/components/__tests__/jant-settings-general.test.ts +71 -0
- package/src/client/components/jant-command-palette.ts +67 -2
- package/src/client/components/jant-compose-dialog.ts +8 -2
- package/src/client/components/jant-nav-manager.ts +142 -2
- package/src/client/components/jant-settings-general.ts +56 -0
- package/src/client/components/nav-manager-types.ts +14 -0
- package/src/client/components/settings-types.ts +30 -0
- package/src/client/compose-launch.ts +30 -0
- package/src/client/compose-shortcuts.ts +46 -3
- package/src/client/tiptap/__tests__/rehost-images.test.ts +66 -13
- package/src/client/tiptap/rehost-images.ts +5 -1
- package/src/i18n/__tests__/fallback.test.ts +24 -0
- package/src/i18n/locales/public/zh-Hans.po +2 -2
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +2 -2
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +54 -0
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +64 -10
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +56 -2
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +96 -10
- package/src/lib/feed.ts +63 -3
- package/src/lib/post-display.ts +14 -8
- package/src/preset.css +60 -18
- package/src/routes/dash/settings.tsx +41 -2
- package/src/routes/pages/__tests__/draft-about-edit.test.ts +65 -0
- package/src/routes/pages/page.tsx +44 -12
- package/src/services/__tests__/about-page.test.ts +116 -0
- package/src/services/__tests__/navigation.test.ts +190 -1
- package/src/services/__tests__/post-timeline.test.ts +119 -2
- package/src/services/about-page.ts +143 -0
- package/src/services/export-theme/layouts/_default/rss.xml +2 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +25 -1
- package/src/services/export-theme/styles/main.css +46 -14
- package/src/services/index.ts +27 -17
- package/src/services/navigation.ts +155 -1
- package/src/services/post.ts +24 -7
- package/src/styles/components.css +20 -0
- package/src/styles/tokens.css +26 -8
- package/src/styles/ui.css +149 -7
- package/src/types/views.ts +13 -0
- package/src/ui/dash/appearance/NavigationContent.tsx +57 -0
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
- package/src/ui/dash/settings/GeneralContent.tsx +44 -0
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
- package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
- package/dist/app-DqKkZenB.js +0 -6
- package/dist/client/_assets/client-BhHHVvSY.css +0 -2
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
SettingsCjkFont,
|
|
8
8
|
SettingsDashboardLanguage,
|
|
9
9
|
SettingsSaveDetail,
|
|
10
|
+
SettingsAboutPageStatus,
|
|
10
11
|
} from "../settings-types.js";
|
|
11
12
|
import { MAX_SITE_NAME_LENGTH } from "../../../types.js";
|
|
12
13
|
import "../jant-settings-general.js";
|
|
@@ -76,6 +77,12 @@ const labels: SettingsLabels = {
|
|
|
76
77
|
uploadError: "Upload failed.",
|
|
77
78
|
general: "General",
|
|
78
79
|
site: "Site",
|
|
80
|
+
aboutPage: "About page",
|
|
81
|
+
aboutPagePrompt: "Want to write a fuller introduction?",
|
|
82
|
+
aboutPageConflict:
|
|
83
|
+
"/about is already used. Rename that item before creating an About page.",
|
|
84
|
+
createAboutPage: "Create About page",
|
|
85
|
+
editAboutPage: "Edit About page",
|
|
79
86
|
languageAndTime: "Language & Time",
|
|
80
87
|
home: "Home",
|
|
81
88
|
search: "Search",
|
|
@@ -167,6 +174,7 @@ function findCheckboxByLabel(
|
|
|
167
174
|
async function createElement(
|
|
168
175
|
opts: {
|
|
169
176
|
demoMode?: boolean;
|
|
177
|
+
aboutPage?: SettingsAboutPageStatus;
|
|
170
178
|
} = {},
|
|
171
179
|
): Promise<JantSettingsGeneral> {
|
|
172
180
|
const el = document.createElement(
|
|
@@ -182,6 +190,14 @@ async function createElement(
|
|
|
182
190
|
el.latestFeedUrl = "/latest/feed";
|
|
183
191
|
el.featuredFeedUrl = "/featured/feed";
|
|
184
192
|
el.archiveFeedUrl = "/archive/feed";
|
|
193
|
+
el.aboutPage =
|
|
194
|
+
opts.aboutPage ??
|
|
195
|
+
({
|
|
196
|
+
state: "missing",
|
|
197
|
+
path: "/about",
|
|
198
|
+
} satisfies SettingsAboutPageStatus);
|
|
199
|
+
el.aboutEditUrl = "/about?edit=1";
|
|
200
|
+
el.aboutCreateUrl = "/settings/general/about-page";
|
|
185
201
|
el.demoMode = opts.demoMode ?? false;
|
|
186
202
|
document.body.appendChild(el);
|
|
187
203
|
await el.updateComplete;
|
|
@@ -209,6 +225,61 @@ describe("JantSettingsGeneral", () => {
|
|
|
209
225
|
labels.home,
|
|
210
226
|
labels.search,
|
|
211
227
|
]);
|
|
228
|
+
|
|
229
|
+
const siteSection = requireElement(
|
|
230
|
+
findSectionByHeading(el, labels.site),
|
|
231
|
+
"expected site section",
|
|
232
|
+
);
|
|
233
|
+
expect(siteSection.querySelector("[data-about-page-row]")).not.toBeNull();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("renders a create About form from the missing About prompt", async () => {
|
|
237
|
+
const el = await createElement();
|
|
238
|
+
const aboutRow = requireElement(
|
|
239
|
+
el.querySelector<HTMLElement>("[data-about-page-row]"),
|
|
240
|
+
"expected About page row",
|
|
241
|
+
);
|
|
242
|
+
const form = requireElement(
|
|
243
|
+
aboutRow.querySelector<HTMLFormElement>("form"),
|
|
244
|
+
"expected create About page form",
|
|
245
|
+
);
|
|
246
|
+
const button = requireElement(
|
|
247
|
+
form.querySelector<HTMLButtonElement>("button"),
|
|
248
|
+
"expected create About page button",
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
expect(aboutRow.textContent).toContain(labels.aboutPagePrompt);
|
|
252
|
+
expect(form.method).toBe("post");
|
|
253
|
+
expect(form.action).toContain("/settings/general/about-page");
|
|
254
|
+
expect(button.textContent).toContain(labels.createAboutPage);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("renders an edit link when the About page exists", async () => {
|
|
258
|
+
const el = await createElement({
|
|
259
|
+
aboutPage: {
|
|
260
|
+
state: "ready",
|
|
261
|
+
path: "/about",
|
|
262
|
+
post: {
|
|
263
|
+
id: "pst_about000000000000000000000",
|
|
264
|
+
title: "About",
|
|
265
|
+
status: "published",
|
|
266
|
+
visibility: "latest_hidden",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const aboutRow = requireElement(
|
|
272
|
+
el.querySelector<HTMLElement>("[data-about-page-row]"),
|
|
273
|
+
"expected About page row",
|
|
274
|
+
);
|
|
275
|
+
const editLink = requireElement(
|
|
276
|
+
aboutRow.querySelector<HTMLAnchorElement>("a"),
|
|
277
|
+
"expected About edit link",
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
expect(aboutRow.textContent).toContain(labels.aboutPagePrompt);
|
|
281
|
+
expect(editLink.textContent).toContain(labels.editAboutPage);
|
|
282
|
+
expect(editLink.href).toContain("/about?edit=1");
|
|
212
283
|
});
|
|
213
284
|
|
|
214
285
|
it("renders form fields with initial values", async () => {
|
|
@@ -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
|
|
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) {
|
|
@@ -5432,7 +5432,11 @@ export class JantComposeDialog extends LitElement {
|
|
|
5432
5432
|
|
|
5433
5433
|
return html`
|
|
5434
5434
|
<div
|
|
5435
|
-
class
|
|
5435
|
+
class=${classMap({
|
|
5436
|
+
"compose-thread-layout": true,
|
|
5437
|
+
"compose-thread-compose-layout": true,
|
|
5438
|
+
"compose-reply-compose-layout": isReply,
|
|
5439
|
+
})}
|
|
5436
5440
|
@jant:compose-content-changed=${() => this._scheduleDraftSave()}
|
|
5437
5441
|
>
|
|
5438
5442
|
${isReply ? this._renderReplyContext() : nothing}
|
|
@@ -5502,7 +5506,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
5502
5506
|
? this._renderThreadComposeLayout()
|
|
5503
5507
|
: isReply
|
|
5504
5508
|
? html`
|
|
5505
|
-
<div
|
|
5509
|
+
<div
|
|
5510
|
+
class="compose-thread-layout compose-reply-compose-layout"
|
|
5511
|
+
>
|
|
5506
5512
|
${this._renderReplyContext()}
|
|
5507
5513
|
<div
|
|
5508
5514
|
class="compose-editor-row"
|
|
@@ -33,6 +33,7 @@ import type {
|
|
|
33
33
|
NavManagerLabels,
|
|
34
34
|
NavManagerUpdateDetail,
|
|
35
35
|
NavManagerDeleteDetail,
|
|
36
|
+
NavManagerSuggestedLink,
|
|
36
37
|
SystemNavConfig,
|
|
37
38
|
} from "./nav-manager-types.js";
|
|
38
39
|
|
|
@@ -42,6 +43,7 @@ export class JantNavManager extends LitElement {
|
|
|
42
43
|
labels: { type: Object },
|
|
43
44
|
systemNavItems: { type: Array, attribute: "system-nav-items" },
|
|
44
45
|
collections: { type: Array },
|
|
46
|
+
suggestedLinks: { type: Array, attribute: "suggested-links" },
|
|
45
47
|
siteName: { type: String, attribute: "site-name" },
|
|
46
48
|
|
|
47
49
|
_items: { state: true },
|
|
@@ -56,12 +58,14 @@ export class JantNavManager extends LitElement {
|
|
|
56
58
|
_showPreviewMore: { state: true },
|
|
57
59
|
_addingCollectionId: { state: true },
|
|
58
60
|
_showCollectionPicker: { state: true },
|
|
61
|
+
_addingSuggestedKey: { state: true },
|
|
59
62
|
};
|
|
60
63
|
|
|
61
64
|
declare items: NavManagerItem[];
|
|
62
65
|
declare labels: NavManagerLabels;
|
|
63
66
|
declare systemNavItems: SystemNavConfig[];
|
|
64
67
|
declare collections: NavManagerCollection[];
|
|
68
|
+
declare suggestedLinks: NavManagerSuggestedLink[];
|
|
65
69
|
declare siteName: string;
|
|
66
70
|
|
|
67
71
|
declare _items: NavManagerItem[];
|
|
@@ -78,6 +82,8 @@ export class JantNavManager extends LitElement {
|
|
|
78
82
|
/** ID of the collection currently being added (for loading state) */
|
|
79
83
|
declare _addingCollectionId: string | null;
|
|
80
84
|
declare _showCollectionPicker: boolean;
|
|
85
|
+
/** Key of the suggested link currently being added */
|
|
86
|
+
declare _addingSuggestedKey: string | null;
|
|
81
87
|
|
|
82
88
|
#sortableHeader: { destroy(): void } | null = null;
|
|
83
89
|
#sortableMore: { destroy(): void } | null = null;
|
|
@@ -124,6 +130,7 @@ export class JantNavManager extends LitElement {
|
|
|
124
130
|
this.labels = {} as NavManagerLabels;
|
|
125
131
|
this.systemNavItems = [];
|
|
126
132
|
this.collections = [];
|
|
133
|
+
this.suggestedLinks = [];
|
|
127
134
|
this.siteName = "";
|
|
128
135
|
|
|
129
136
|
this._items = [];
|
|
@@ -138,6 +145,7 @@ export class JantNavManager extends LitElement {
|
|
|
138
145
|
this._showPreviewMore = false;
|
|
139
146
|
this._addingCollectionId = null;
|
|
140
147
|
this._showCollectionPicker = false;
|
|
148
|
+
this._addingSuggestedKey = null;
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
protected update(changedProperties: PropertyValueMap<JantNavManager>): void {
|
|
@@ -448,6 +456,99 @@ export class JantNavManager extends LitElement {
|
|
|
448
456
|
}
|
|
449
457
|
}
|
|
450
458
|
|
|
459
|
+
// ===========================================================================
|
|
460
|
+
// Suggested link handlers
|
|
461
|
+
// ===========================================================================
|
|
462
|
+
|
|
463
|
+
#getComparableInternalPath(url: string): string | null {
|
|
464
|
+
if (
|
|
465
|
+
url.startsWith("http://") ||
|
|
466
|
+
url.startsWith("https://") ||
|
|
467
|
+
url.startsWith("//") ||
|
|
468
|
+
url.startsWith("mailto:") ||
|
|
469
|
+
url.startsWith("tel:") ||
|
|
470
|
+
url.startsWith("#")
|
|
471
|
+
) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
const pathname = new URL(url, "https://jant.invalid").pathname;
|
|
477
|
+
const normalized = pathname
|
|
478
|
+
.trim()
|
|
479
|
+
.toLowerCase()
|
|
480
|
+
.replace(/^\/+|\/+$/g, "")
|
|
481
|
+
.replace(/\/+/g, "/");
|
|
482
|
+
return normalized ? `/${normalized}` : "/";
|
|
483
|
+
} catch {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
#isSuggestedLinkAdded(link: NavManagerSuggestedLink): boolean {
|
|
489
|
+
const path = this.#getComparableInternalPath(link.url);
|
|
490
|
+
const hasMatchingPath =
|
|
491
|
+
path !== null &&
|
|
492
|
+
this._items.some(
|
|
493
|
+
(item) => this.#getComparableInternalPath(item.url) === path,
|
|
494
|
+
);
|
|
495
|
+
if (hasMatchingPath) return true;
|
|
496
|
+
|
|
497
|
+
return Boolean(
|
|
498
|
+
link.collectionId &&
|
|
499
|
+
this._items.some(
|
|
500
|
+
(item) =>
|
|
501
|
+
item.type === "collection" && item.collectionId === link.collectionId,
|
|
502
|
+
),
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
get #availableSuggestedLinks(): NavManagerSuggestedLink[] {
|
|
507
|
+
return (this.suggestedLinks ?? []).filter(
|
|
508
|
+
(link) => !this.#isSuggestedLinkAdded(link),
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async #handleAddSuggestedLink(link: NavManagerSuggestedLink) {
|
|
513
|
+
if (this._addingSuggestedKey) return;
|
|
514
|
+
|
|
515
|
+
this._addingSuggestedKey = link.key;
|
|
516
|
+
try {
|
|
517
|
+
const body =
|
|
518
|
+
link.navItemType === "collection" && link.collectionId
|
|
519
|
+
? {
|
|
520
|
+
type: "collection",
|
|
521
|
+
collectionId: link.collectionId,
|
|
522
|
+
placement: "header",
|
|
523
|
+
}
|
|
524
|
+
: {
|
|
525
|
+
type: "link",
|
|
526
|
+
label: link.label,
|
|
527
|
+
url: link.url,
|
|
528
|
+
placement: "header",
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const res = await fetch("/api/nav-items", {
|
|
532
|
+
method: "POST",
|
|
533
|
+
headers: {
|
|
534
|
+
"Content-Type": "application/json",
|
|
535
|
+
Accept: "application/json",
|
|
536
|
+
},
|
|
537
|
+
body: JSON.stringify(body),
|
|
538
|
+
});
|
|
539
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
540
|
+
|
|
541
|
+
const created: NavManagerItem = await res.json();
|
|
542
|
+
this.#destroySortables();
|
|
543
|
+
this._items = [...this._items, created];
|
|
544
|
+
showToast(this.labels.suggestedLinkAdded);
|
|
545
|
+
} catch {
|
|
546
|
+
showToast(this.labels.saveFailed, "error");
|
|
547
|
+
} finally {
|
|
548
|
+
this._addingSuggestedKey = null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
451
552
|
// ===========================================================================
|
|
452
553
|
// System toggle handlers
|
|
453
554
|
// ===========================================================================
|
|
@@ -834,6 +935,45 @@ export class JantNavManager extends LitElement {
|
|
|
834
935
|
`;
|
|
835
936
|
}
|
|
836
937
|
|
|
938
|
+
#renderSuggestedLinksSection() {
|
|
939
|
+
const available = this.#availableSuggestedLinks;
|
|
940
|
+
if (available.length === 0) return nothing;
|
|
941
|
+
|
|
942
|
+
return html`
|
|
943
|
+
<section class="mt-8">
|
|
944
|
+
<h2 class="text-lg font-semibold mb-1">
|
|
945
|
+
${this.labels.suggestedLinks}
|
|
946
|
+
</h2>
|
|
947
|
+
<p class="text-sm text-muted-foreground mb-3">
|
|
948
|
+
${this.labels.suggestedLinksDescription}
|
|
949
|
+
</p>
|
|
950
|
+
<div class="nav-suggestions-list">
|
|
951
|
+
${available.map((link) => {
|
|
952
|
+
const adding = this._addingSuggestedKey === link.key;
|
|
953
|
+
return html`
|
|
954
|
+
<div class="nav-suggestion-item">
|
|
955
|
+
<div class="nav-suggestion-info">
|
|
956
|
+
<span class="nav-suggestion-title">${link.label}</span>
|
|
957
|
+
<span class="nav-suggestion-meta">
|
|
958
|
+
${link.url} · ${link.targetLabel}
|
|
959
|
+
</span>
|
|
960
|
+
</div>
|
|
961
|
+
<button
|
|
962
|
+
type="button"
|
|
963
|
+
class="btn-sm"
|
|
964
|
+
?disabled=${adding || this._addingSuggestedKey !== null}
|
|
965
|
+
@click=${() => this.#handleAddSuggestedLink(link)}
|
|
966
|
+
>
|
|
967
|
+
${this.labels.addSuggestedLink}
|
|
968
|
+
</button>
|
|
969
|
+
</div>
|
|
970
|
+
`;
|
|
971
|
+
})}
|
|
972
|
+
</div>
|
|
973
|
+
</section>
|
|
974
|
+
`;
|
|
975
|
+
}
|
|
976
|
+
|
|
837
977
|
#renderAddLinkSection() {
|
|
838
978
|
return html`
|
|
839
979
|
<section class="mt-8">
|
|
@@ -1175,8 +1315,8 @@ export class JantNavManager extends LitElement {
|
|
|
1175
1315
|
</div>
|
|
1176
1316
|
</section>
|
|
1177
1317
|
|
|
1178
|
-
${this.#
|
|
1179
|
-
${this.#renderSystemToggles()}
|
|
1318
|
+
${this.#renderSuggestedLinksSection()} ${this.#renderAddLinkSection()}
|
|
1319
|
+
${this.#renderAddCollectionSection()} ${this.#renderSystemToggles()}
|
|
1180
1320
|
`;
|
|
1181
1321
|
}
|
|
1182
1322
|
}
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
SettingsTimezone,
|
|
22
22
|
SettingsCjkFont,
|
|
23
23
|
SettingsDashboardLanguage,
|
|
24
|
+
SettingsAboutPageStatus,
|
|
24
25
|
} from "./settings-types.js";
|
|
25
26
|
import { showToast } from "../toast.js";
|
|
26
27
|
import {
|
|
@@ -44,6 +45,12 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
44
45
|
latestFeedUrl: { type: String, attribute: "latest-feed-url" },
|
|
45
46
|
featuredFeedUrl: { type: String, attribute: "featured-feed-url" },
|
|
46
47
|
archiveFeedUrl: { type: String, attribute: "archive-feed-url" },
|
|
48
|
+
aboutPage: { type: Object, attribute: "about-page" },
|
|
49
|
+
aboutEditUrl: { type: String, attribute: "about-edit-url" },
|
|
50
|
+
aboutCreateUrl: {
|
|
51
|
+
type: String,
|
|
52
|
+
attribute: "about-create-url",
|
|
53
|
+
},
|
|
47
54
|
|
|
48
55
|
// Site group
|
|
49
56
|
_siteName: { state: true },
|
|
@@ -92,6 +99,9 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
92
99
|
declare latestFeedUrl: string;
|
|
93
100
|
declare featuredFeedUrl: string;
|
|
94
101
|
declare archiveFeedUrl: string;
|
|
102
|
+
declare aboutPage: SettingsAboutPageStatus;
|
|
103
|
+
declare aboutEditUrl: string;
|
|
104
|
+
declare aboutCreateUrl: string;
|
|
95
105
|
|
|
96
106
|
// Site
|
|
97
107
|
declare _siteName: string;
|
|
@@ -162,6 +172,12 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
162
172
|
this.latestFeedUrl = "/latest/feed";
|
|
163
173
|
this.featuredFeedUrl = "/featured/feed";
|
|
164
174
|
this.archiveFeedUrl = "/archive/feed";
|
|
175
|
+
this.aboutPage = {
|
|
176
|
+
state: "missing",
|
|
177
|
+
path: "/about",
|
|
178
|
+
};
|
|
179
|
+
this.aboutEditUrl = "/about?edit=1";
|
|
180
|
+
this.aboutCreateUrl = "/settings/general/about-page";
|
|
165
181
|
|
|
166
182
|
this._siteName = "";
|
|
167
183
|
this._siteDescription = "";
|
|
@@ -757,6 +773,45 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
757
773
|
`;
|
|
758
774
|
}
|
|
759
775
|
|
|
776
|
+
private _renderAboutPageRow() {
|
|
777
|
+
const status = this.aboutPage;
|
|
778
|
+
|
|
779
|
+
return html`
|
|
780
|
+
<div class="mt-2 text-sm text-muted-foreground" data-about-page-row>
|
|
781
|
+
${this.labels.aboutPagePrompt}
|
|
782
|
+
${status.state === "ready"
|
|
783
|
+
? html`
|
|
784
|
+
<a
|
|
785
|
+
class="font-medium text-foreground underline-offset-4 hover:underline"
|
|
786
|
+
href=${this.aboutEditUrl}
|
|
787
|
+
>
|
|
788
|
+
${this.labels.editAboutPage}
|
|
789
|
+
</a>
|
|
790
|
+
`
|
|
791
|
+
: status.state === "missing"
|
|
792
|
+
? html`
|
|
793
|
+
<form
|
|
794
|
+
class="inline"
|
|
795
|
+
method="post"
|
|
796
|
+
action=${this.aboutCreateUrl}
|
|
797
|
+
>
|
|
798
|
+
<button
|
|
799
|
+
type="submit"
|
|
800
|
+
class="inline cursor-pointer border-0 bg-transparent p-0 font-medium text-foreground underline-offset-4 hover:underline"
|
|
801
|
+
>
|
|
802
|
+
${this.labels.createAboutPage}
|
|
803
|
+
</button>
|
|
804
|
+
</form>
|
|
805
|
+
`
|
|
806
|
+
: html`
|
|
807
|
+
<span class="text-destructive"
|
|
808
|
+
>${this.labels.aboutPageConflict}</span
|
|
809
|
+
>
|
|
810
|
+
`}
|
|
811
|
+
</div>
|
|
812
|
+
`;
|
|
813
|
+
}
|
|
814
|
+
|
|
760
815
|
private _renderGeneralForm() {
|
|
761
816
|
return html`
|
|
762
817
|
<div class="flex flex-col gap-8">
|
|
@@ -796,6 +851,7 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
796
851
|
<p class="text-sm text-muted-foreground mt-1">
|
|
797
852
|
${this.labels.aboutBlogHelp}
|
|
798
853
|
</p>
|
|
854
|
+
${this._renderAboutPageRow()}
|
|
799
855
|
</div>
|
|
800
856
|
|
|
801
857
|
<div class="field">
|
|
@@ -30,6 +30,16 @@ export interface NavManagerCollection {
|
|
|
30
30
|
group?: string | null;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export interface NavManagerSuggestedLink {
|
|
34
|
+
key: string;
|
|
35
|
+
label: string;
|
|
36
|
+
url: string;
|
|
37
|
+
targetType: "page" | "collection" | "archive";
|
|
38
|
+
targetLabel: string;
|
|
39
|
+
navItemType: "link" | "collection";
|
|
40
|
+
collectionId?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
export interface NavManagerLabels {
|
|
34
44
|
preview: string;
|
|
35
45
|
navigationItems: string;
|
|
@@ -54,6 +64,10 @@ export interface NavManagerLabels {
|
|
|
54
64
|
addLinkDescription: string;
|
|
55
65
|
urlPlaceholder: string;
|
|
56
66
|
labelAndUrlRequired: string;
|
|
67
|
+
suggestedLinks: string;
|
|
68
|
+
suggestedLinksDescription: string;
|
|
69
|
+
addSuggestedLink: string;
|
|
70
|
+
suggestedLinkAdded: string;
|
|
57
71
|
collection: string;
|
|
58
72
|
addCollection: string;
|
|
59
73
|
addCollectionToNavigation: string;
|
|
@@ -18,6 +18,11 @@ export interface SettingsLabels {
|
|
|
18
18
|
// General
|
|
19
19
|
general: string;
|
|
20
20
|
site: string;
|
|
21
|
+
aboutPage: string;
|
|
22
|
+
aboutPagePrompt: string;
|
|
23
|
+
aboutPageConflict: string;
|
|
24
|
+
createAboutPage: string;
|
|
25
|
+
editAboutPage: string;
|
|
21
26
|
languageAndTime: string;
|
|
22
27
|
home: string;
|
|
23
28
|
search: string;
|
|
@@ -100,6 +105,31 @@ export interface SettingsInitialData {
|
|
|
100
105
|
noindex: boolean;
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
export type SettingsAboutPageStatus =
|
|
109
|
+
| {
|
|
110
|
+
state: "missing";
|
|
111
|
+
path: "/about";
|
|
112
|
+
}
|
|
113
|
+
| {
|
|
114
|
+
state: "ready";
|
|
115
|
+
path: "/about";
|
|
116
|
+
post: {
|
|
117
|
+
id: string;
|
|
118
|
+
title: string | null;
|
|
119
|
+
status: "draft" | "published";
|
|
120
|
+
visibility: "public" | "latest_hidden" | "private";
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
| {
|
|
124
|
+
state: "conflict";
|
|
125
|
+
path: "/about";
|
|
126
|
+
conflict: {
|
|
127
|
+
targetType: "collection" | "redirect" | "archive" | "post";
|
|
128
|
+
id: string | null;
|
|
129
|
+
title: string | null;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
|
|
103
133
|
/** Event detail dispatched when a settings form is saved */
|
|
104
134
|
export interface SettingsSaveDetail {
|
|
105
135
|
endpoint: string;
|
|
@@ -57,6 +57,36 @@ export function getCurrentDetailPostArticle(
|
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function getLastThreadDetailArticle(
|
|
61
|
+
root: globalThis.Document | globalThis.Element,
|
|
62
|
+
): HTMLElement | null {
|
|
63
|
+
const threadGroup = root.querySelector<HTMLElement>(
|
|
64
|
+
".thread-group-detail[data-page='post']",
|
|
65
|
+
);
|
|
66
|
+
if (!threadGroup) return null;
|
|
67
|
+
|
|
68
|
+
const articles = threadGroup.querySelectorAll<HTMLElement>(
|
|
69
|
+
".thread-detail-item article[data-post]",
|
|
70
|
+
);
|
|
71
|
+
return articles.length > 0 ? articles.item(articles.length - 1) : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getReplyTargetArticle(
|
|
75
|
+
root: globalThis.Document | globalThis.Element = document,
|
|
76
|
+
): HTMLElement | null {
|
|
77
|
+
const lastThreadPost = getLastThreadDetailArticle(root);
|
|
78
|
+
if (lastThreadPost) return lastThreadPost;
|
|
79
|
+
|
|
80
|
+
if (root === document) {
|
|
81
|
+
const hoveredPost = document.querySelector<HTMLElement>(
|
|
82
|
+
"[data-page='post'] article[data-post]:hover",
|
|
83
|
+
);
|
|
84
|
+
if (hoveredPost) return hoveredPost;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return getCurrentDetailPostArticle(root);
|
|
88
|
+
}
|
|
89
|
+
|
|
60
90
|
export function getReplyRefreshTarget(
|
|
61
91
|
article: HTMLElement,
|
|
62
92
|
): ReplyRefreshTarget | null {
|