@jant/core 0.6.11 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/import-site.js +47 -30
- 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-CWJFPjuR.js +6 -0
- package/dist/{app-CpmficmQ.js → app-HnzmsaqU.js} +721 -473
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-Dd9U383b.js → client-3_OaxhTs.js} +1 -1
- package/dist/client/_assets/client-DCrcHVJl.css +2 -0
- package/dist/client/_assets/{client-auth-DkpSdIDz.js → client-auth-TsYq90xm.js} +625 -561
- package/dist/{export-Ba7NJImL.js → export-Dfbq9aot.js} +12 -15
- package/dist/{github-sync-Cb4_6_i7.js → github-sync--PSoE-Ne.js} +1 -1
- package/dist/{github-sync-BD4w2m8-.js → github-sync-BGTLx8Mf.js} +2 -2
- 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 +76 -2
- 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/__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 +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-compose-editor.test.ts +94 -2
- package/src/client/components/__tests__/jant-nav-manager.test.ts +181 -20
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +6 -0
- package/src/client/components/__tests__/jant-settings-general.test.ts +72 -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 +67 -2
- package/src/client/components/jant-compose-dialog.ts +10 -2
- 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 +382 -152
- package/src/client/components/jant-settings-general.ts +56 -0
- package/src/client/components/nav-manager-types.ts +14 -10
- 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/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__/image-input-rules.test.ts +117 -0
- package/src/client/tiptap/__tests__/rehost-images.test.ts +66 -13
- package/src/client/tiptap/create-editor.ts +7 -0
- package/src/client/tiptap/extensions.ts +7 -2
- package/src/client/tiptap/image-input-rules.ts +48 -0
- package/src/client/tiptap/image-node.ts +189 -7
- package/src/client/tiptap/rehost-images.ts +5 -1
- package/src/client-auth.ts +0 -1
- package/src/i18n/__tests__/fallback.test.ts +24 -0
- 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 +22 -6
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +22 -6
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +77 -20
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +85 -28
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +79 -22
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +96 -10
- 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/feed.ts +63 -3
- package/src/lib/github-sync-site-config.ts +0 -2
- package/src/lib/hugo-markdown.ts +1 -1
- package/src/lib/navigation.ts +5 -22
- package/src/lib/post-display.ts +14 -8
- 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/preset.css +60 -18
- 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 +48 -3
- 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/__tests__/draft-about-edit.test.ts +65 -0
- 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/routes/pages/page.tsx +44 -12
- package/src/services/__tests__/about-page.test.ts +116 -0
- package/src/services/__tests__/github-app-installations.test.ts +10 -10
- package/src/services/__tests__/navigation.test.ts +231 -1
- package/src/services/__tests__/post-timeline.test.ts +175 -2
- package/src/services/__tests__/post.test.ts +33 -0
- package/src/services/__tests__/settings.test.ts +5 -20
- package/src/services/about-page.ts +143 -0
- package/src/services/export-theme/assets/client-site.js +10 -10
- package/src/services/export-theme/layouts/_default/rss.xml +7 -16
- package/src/services/export-theme/layouts/index.html +26 -39
- 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/export.ts +5 -10
- package/src/services/github-app-installations.ts +1 -3
- package/src/services/index.ts +27 -17
- package/src/services/navigation.ts +186 -1
- package/src/services/post.ts +103 -63
- 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/components.css +20 -0
- package/src/styles/tokens.css +28 -10
- package/src/styles/ui.css +349 -13
- package/src/types/bindings.ts +0 -1
- package/src/types/config.ts +3 -7
- package/src/types/views.ts +13 -0
- package/src/ui/compose/ComposeDialog.tsx +28 -0
- package/src/ui/dash/appearance/NavigationContent.tsx +58 -1
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/GeneralContent.tsx +44 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
- package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
- package/src/ui/layouts/SiteLayout.tsx +249 -216
- package/dist/app-DqKkZenB.js +0 -6
- package/dist/client/_assets/client-BhHHVvSY.css +0 -2
- package/src/client/nav-manager-bridge.ts +0 -54
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - SortableJS drag-and-drop reorder with immediate preview update
|
|
8
8
|
* - Add link forms
|
|
9
9
|
* - System nav item toggles with immediate list/preview update
|
|
10
|
-
* -
|
|
10
|
+
* - Applies server-rendered site header fragments after saved changes
|
|
11
11
|
*
|
|
12
12
|
* Light DOM only — BaseCoat and Tailwind classes apply directly.
|
|
13
13
|
*/
|
|
@@ -27,21 +27,31 @@ import {
|
|
|
27
27
|
import { showConfirmDialog } from "../confirm.js";
|
|
28
28
|
import { showToast } from "../toast.js";
|
|
29
29
|
import { publicPath } from "../runtime-paths.js";
|
|
30
|
+
import { destroySiteHeaderNav, initSiteHeaderNav } from "../site-header-nav.js";
|
|
30
31
|
import type {
|
|
31
32
|
NavManagerCollection,
|
|
32
33
|
NavManagerItem,
|
|
33
34
|
NavManagerLabels,
|
|
34
|
-
|
|
35
|
-
NavManagerDeleteDetail,
|
|
35
|
+
NavManagerSuggestedLink,
|
|
36
36
|
SystemNavConfig,
|
|
37
37
|
} from "./nav-manager-types.js";
|
|
38
38
|
|
|
39
|
+
const SITE_HEADER_REQUEST_HEADER = "X-Jant-Site-Header";
|
|
40
|
+
const INCLUDE_SITE_HEADER_RESPONSE = "include";
|
|
41
|
+
|
|
42
|
+
type NavManagerMutationResponse<T extends object> = T & {
|
|
43
|
+
headerHtml?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type SortableEndEvent = Parameters<NonNullable<SortableOptions["onEnd"]>>[0];
|
|
47
|
+
|
|
39
48
|
export class JantNavManager extends LitElement {
|
|
40
49
|
static properties = {
|
|
41
50
|
items: { type: Array },
|
|
42
51
|
labels: { type: Object },
|
|
43
52
|
systemNavItems: { type: Array, attribute: "system-nav-items" },
|
|
44
53
|
collections: { type: Array },
|
|
54
|
+
suggestedLinks: { type: Array, attribute: "suggested-links" },
|
|
45
55
|
siteName: { type: String, attribute: "site-name" },
|
|
46
56
|
|
|
47
57
|
_items: { state: true },
|
|
@@ -56,12 +66,14 @@ export class JantNavManager extends LitElement {
|
|
|
56
66
|
_showPreviewMore: { state: true },
|
|
57
67
|
_addingCollectionId: { state: true },
|
|
58
68
|
_showCollectionPicker: { state: true },
|
|
69
|
+
_addingSuggestedKey: { state: true },
|
|
59
70
|
};
|
|
60
71
|
|
|
61
72
|
declare items: NavManagerItem[];
|
|
62
73
|
declare labels: NavManagerLabels;
|
|
63
74
|
declare systemNavItems: SystemNavConfig[];
|
|
64
75
|
declare collections: NavManagerCollection[];
|
|
76
|
+
declare suggestedLinks: NavManagerSuggestedLink[];
|
|
65
77
|
declare siteName: string;
|
|
66
78
|
|
|
67
79
|
declare _items: NavManagerItem[];
|
|
@@ -78,6 +90,8 @@ export class JantNavManager extends LitElement {
|
|
|
78
90
|
/** ID of the collection currently being added (for loading state) */
|
|
79
91
|
declare _addingCollectionId: string | null;
|
|
80
92
|
declare _showCollectionPicker: boolean;
|
|
93
|
+
/** Key of the suggested link currently being added */
|
|
94
|
+
declare _addingSuggestedKey: string | null;
|
|
81
95
|
|
|
82
96
|
#sortableHeader: { destroy(): void } | null = null;
|
|
83
97
|
#sortableMore: { destroy(): void } | null = null;
|
|
@@ -124,6 +138,7 @@ export class JantNavManager extends LitElement {
|
|
|
124
138
|
this.labels = {} as NavManagerLabels;
|
|
125
139
|
this.systemNavItems = [];
|
|
126
140
|
this.collections = [];
|
|
141
|
+
this.suggestedLinks = [];
|
|
127
142
|
this.siteName = "";
|
|
128
143
|
|
|
129
144
|
this._items = [];
|
|
@@ -138,6 +153,7 @@ export class JantNavManager extends LitElement {
|
|
|
138
153
|
this._showPreviewMore = false;
|
|
139
154
|
this._addingCollectionId = null;
|
|
140
155
|
this._showCollectionPicker = false;
|
|
156
|
+
this._addingSuggestedKey = null;
|
|
141
157
|
}
|
|
142
158
|
|
|
143
159
|
protected update(changedProperties: PropertyValueMap<JantNavManager>): void {
|
|
@@ -166,6 +182,69 @@ export class JantNavManager extends LitElement {
|
|
|
166
182
|
this.#closePreviewMore();
|
|
167
183
|
}
|
|
168
184
|
|
|
185
|
+
#jsonMutationHeaders(): Record<string, string> {
|
|
186
|
+
return {
|
|
187
|
+
"Content-Type": "application/json",
|
|
188
|
+
Accept: "application/json",
|
|
189
|
+
[SITE_HEADER_REQUEST_HEADER]: INCLUDE_SITE_HEADER_RESPONSE,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#deleteMutationHeaders(): Record<string, string> {
|
|
194
|
+
return {
|
|
195
|
+
Accept: "application/json",
|
|
196
|
+
[SITE_HEADER_REQUEST_HEADER]: INCLUDE_SITE_HEADER_RESPONSE,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#stripHeaderHtml<T extends object>(
|
|
201
|
+
response: NavManagerMutationResponse<T>,
|
|
202
|
+
): T {
|
|
203
|
+
const body = { ...response };
|
|
204
|
+
delete (body as { headerHtml?: string }).headerHtml;
|
|
205
|
+
return body as T;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#normalizeItem(item: NavManagerItem): NavManagerItem {
|
|
209
|
+
const systemLabel =
|
|
210
|
+
item.type === "system"
|
|
211
|
+
? this.systemNavItems.find((config) => config.key === item.systemKey)
|
|
212
|
+
?.label
|
|
213
|
+
: undefined;
|
|
214
|
+
const displayLabel =
|
|
215
|
+
item.label.trim() || systemLabel || item.displayLabel || item.label;
|
|
216
|
+
|
|
217
|
+
return { ...item, displayLabel };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#applySiteHeaderHtml(headerHtml: string | undefined) {
|
|
221
|
+
if (!headerHtml) return;
|
|
222
|
+
|
|
223
|
+
const template = document.createElement("template");
|
|
224
|
+
template.innerHTML = headerHtml.trim();
|
|
225
|
+
|
|
226
|
+
const selectors = [
|
|
227
|
+
'[data-site-header-fragment="header"]',
|
|
228
|
+
'[data-site-header-fragment="drawer-backdrop"]',
|
|
229
|
+
'[data-site-header-fragment="drawer"]',
|
|
230
|
+
];
|
|
231
|
+
let replaced = false;
|
|
232
|
+
|
|
233
|
+
destroySiteHeaderNav(document);
|
|
234
|
+
for (const selector of selectors) {
|
|
235
|
+
const current = document.querySelector<HTMLElement>(selector);
|
|
236
|
+
const next = template.content.querySelector<HTMLElement>(selector);
|
|
237
|
+
if (!current || !next) continue;
|
|
238
|
+
|
|
239
|
+
current.replaceWith(next);
|
|
240
|
+
replaced = true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (replaced) {
|
|
244
|
+
initSiteHeaderNav(document);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
169
248
|
// ===========================================================================
|
|
170
249
|
// SortableJS
|
|
171
250
|
// ===========================================================================
|
|
@@ -203,119 +282,115 @@ export class JantNavManager extends LitElement {
|
|
|
203
282
|
this.#revertNextSibling = captureSortableRevertNextSibling(evt);
|
|
204
283
|
},
|
|
205
284
|
onEnd: (evt) => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
targetList.id === "nav-items-header" ? "header" : "more";
|
|
211
|
-
const headerList = this.querySelector<HTMLElement>("#nav-items-header");
|
|
212
|
-
const moreList = this.querySelector<HTMLElement>("#nav-items-more");
|
|
213
|
-
|
|
214
|
-
const movedId = evt.item?.dataset?.navId;
|
|
215
|
-
if (!movedId || !headerList || !moreList) {
|
|
216
|
-
this.#revertNextSibling = null;
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
285
|
+
void this.#handleSortableEnd(evt);
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
219
289
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (this.#revertNextSibling) {
|
|
230
|
-
sourceList.insertBefore(evt.item, this.#revertNextSibling);
|
|
231
|
-
} else if (
|
|
232
|
-
evt.oldIndex != null &&
|
|
233
|
-
evt.oldIndex < sourceList.children.length
|
|
234
|
-
) {
|
|
235
|
-
sourceList.insertBefore(
|
|
236
|
-
evt.item,
|
|
237
|
-
sourceList.children[evt.oldIndex] ?? null,
|
|
238
|
-
);
|
|
239
|
-
} else {
|
|
240
|
-
sourceList.appendChild(evt.item);
|
|
241
|
-
}
|
|
242
|
-
} else {
|
|
243
|
-
revertSortableDomMove(targetList, evt, this.#revertNextSibling);
|
|
244
|
-
}
|
|
290
|
+
async #handleSortableEnd(evt: SortableEndEvent) {
|
|
291
|
+
const targetList = evt.to;
|
|
292
|
+
const sourceList = evt.from;
|
|
293
|
+
const crossList = sourceList !== targetList;
|
|
294
|
+
const targetPlacement: "header" | "more" =
|
|
295
|
+
targetList.id === "nav-items-header" ? "header" : "more";
|
|
296
|
+
const headerList = this.querySelector<HTMLElement>("#nav-items-header");
|
|
297
|
+
const moreList = this.querySelector<HTMLElement>("#nav-items-more");
|
|
298
|
+
const previousItems = this._items.map((item) => ({ ...item }));
|
|
245
299
|
|
|
246
|
-
|
|
247
|
-
|
|
300
|
+
const movedId = evt.item?.dataset?.navId;
|
|
301
|
+
if (!movedId || !headerList || !moreList) {
|
|
302
|
+
this.#revertNextSibling = null;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
248
305
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
306
|
+
const headerIds = readSortableDataIds(headerList, "[data-nav-id]", "navId");
|
|
307
|
+
const moreIds = readSortableDataIds(moreList, "[data-nav-id]", "navId");
|
|
308
|
+
|
|
309
|
+
if (crossList) {
|
|
310
|
+
evt.item.parentNode?.removeChild(evt.item);
|
|
311
|
+
if (this.#revertNextSibling) {
|
|
312
|
+
sourceList.insertBefore(evt.item, this.#revertNextSibling);
|
|
313
|
+
} else if (
|
|
314
|
+
evt.oldIndex != null &&
|
|
315
|
+
evt.oldIndex < sourceList.children.length
|
|
316
|
+
) {
|
|
317
|
+
sourceList.insertBefore(
|
|
318
|
+
evt.item,
|
|
319
|
+
sourceList.children[evt.oldIndex] ?? null,
|
|
257
320
|
);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
321
|
+
} else {
|
|
322
|
+
sourceList.appendChild(evt.item);
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
revertSortableDomMove(targetList, evt, this.#revertNextSibling);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
this.#revertNextSibling = null;
|
|
329
|
+
this.#destroySortables();
|
|
330
|
+
|
|
331
|
+
const orderedIds = [...new Set([...headerIds, ...moreIds])];
|
|
332
|
+
const itemMap = new Map(
|
|
333
|
+
this._items.map((item) => [
|
|
334
|
+
item.id,
|
|
335
|
+
item.id === movedId
|
|
336
|
+
? { ...item, placement: targetPlacement }
|
|
337
|
+
: { ...item },
|
|
338
|
+
]),
|
|
339
|
+
);
|
|
340
|
+
const reorderedItems = orderedIds
|
|
341
|
+
.map((id) => itemMap.get(id))
|
|
342
|
+
.filter((item): item is NavManagerItem => item !== undefined);
|
|
343
|
+
const remainingItems = this._items
|
|
344
|
+
.filter((item) => !orderedIds.includes(item.id))
|
|
345
|
+
.map((item) =>
|
|
346
|
+
item.id === movedId
|
|
347
|
+
? { ...item, placement: targetPlacement }
|
|
348
|
+
: { ...item },
|
|
349
|
+
);
|
|
350
|
+
this._items = [...reorderedItems, ...remainingItems];
|
|
351
|
+
|
|
352
|
+
const targetIds = targetPlacement === "header" ? headerIds : moreIds;
|
|
353
|
+
const {
|
|
354
|
+
movedId: persistedId,
|
|
355
|
+
afterId,
|
|
356
|
+
beforeId,
|
|
357
|
+
} = getSortableMove(targetIds, evt.newIndex);
|
|
358
|
+
if (!persistedId) return;
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
if (crossList) {
|
|
362
|
+
const placementRes = await fetch(`/api/nav-items/${movedId}`, {
|
|
363
|
+
method: "PUT",
|
|
364
|
+
headers: {
|
|
365
|
+
"Content-Type": "application/json",
|
|
366
|
+
Accept: "application/json",
|
|
367
|
+
},
|
|
368
|
+
body: JSON.stringify({ placement: targetPlacement }),
|
|
369
|
+
});
|
|
370
|
+
if (!placementRes.ok) throw new Error(`HTTP ${placementRes.status}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const moveRes = await fetch(`/api/nav-items/${persistedId}/move`, {
|
|
374
|
+
method: "PUT",
|
|
375
|
+
headers: this.#jsonMutationHeaders(),
|
|
376
|
+
body: JSON.stringify({
|
|
377
|
+
after: afterId ?? null,
|
|
378
|
+
before: beforeId ?? null,
|
|
379
|
+
}),
|
|
380
|
+
});
|
|
381
|
+
if (!moveRes.ok) throw new Error(`HTTP ${moveRes.status}`);
|
|
382
|
+
|
|
383
|
+
const moved =
|
|
384
|
+
(await moveRes.json()) as NavManagerMutationResponse<NavManagerItem>;
|
|
385
|
+
this.#applySiteHeaderHtml(moved.headerHtml);
|
|
386
|
+
showToast(
|
|
387
|
+
crossList ? this.labels.placementSaved : this.labels.orderSaved,
|
|
388
|
+
);
|
|
389
|
+
} catch {
|
|
390
|
+
this.#destroySortables();
|
|
391
|
+
this._items = previousItems;
|
|
392
|
+
showToast(this.labels.saveFailed, "error");
|
|
393
|
+
}
|
|
319
394
|
}
|
|
320
395
|
|
|
321
396
|
// ===========================================================================
|
|
@@ -332,25 +407,36 @@ export class JantNavManager extends LitElement {
|
|
|
332
407
|
}
|
|
333
408
|
}
|
|
334
409
|
|
|
335
|
-
#handleUpdate(item: NavManagerItem) {
|
|
410
|
+
async #handleUpdate(item: NavManagerItem) {
|
|
336
411
|
const label = this._editLabel.trim();
|
|
337
412
|
if (!label && item.type !== "system") {
|
|
338
413
|
showToast(this.labels.labelRequired, "error");
|
|
339
414
|
return;
|
|
340
415
|
}
|
|
341
416
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
417
|
+
try {
|
|
418
|
+
const res = await fetch(`/api/nav-items/${item.id}`, {
|
|
419
|
+
method: "PUT",
|
|
420
|
+
headers: this.#jsonMutationHeaders(),
|
|
421
|
+
body: JSON.stringify({
|
|
422
|
+
label,
|
|
423
|
+
...(item.type === "link" && { url: this._editUrl.trim() }),
|
|
424
|
+
}),
|
|
425
|
+
});
|
|
426
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
347
427
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
428
|
+
const response =
|
|
429
|
+
(await res.json()) as NavManagerMutationResponse<NavManagerItem>;
|
|
430
|
+
const updated = this.#normalizeItem(this.#stripHeaderHtml(response));
|
|
431
|
+
this.#destroySortables();
|
|
432
|
+
this._items = this._items.map((current) =>
|
|
433
|
+
current.id === updated.id ? updated : current,
|
|
434
|
+
);
|
|
435
|
+
this._editingId = null;
|
|
436
|
+
this.#applySiteHeaderHtml(response.headerHtml);
|
|
437
|
+
} catch {
|
|
438
|
+
showToast(this.labels.saveFailed, "error");
|
|
439
|
+
}
|
|
354
440
|
}
|
|
355
441
|
|
|
356
442
|
async #handleDelete(item: NavManagerItem) {
|
|
@@ -368,12 +454,23 @@ export class JantNavManager extends LitElement {
|
|
|
368
454
|
});
|
|
369
455
|
if (!confirmed) return;
|
|
370
456
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
})
|
|
376
|
-
|
|
457
|
+
try {
|
|
458
|
+
const res = await fetch(`/api/nav-items/${item.id}`, {
|
|
459
|
+
method: "DELETE",
|
|
460
|
+
headers: this.#deleteMutationHeaders(),
|
|
461
|
+
});
|
|
462
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
463
|
+
|
|
464
|
+
const response = (await res.json()) as NavManagerMutationResponse<{
|
|
465
|
+
success: true;
|
|
466
|
+
}>;
|
|
467
|
+
this.#destroySortables();
|
|
468
|
+
this._items = this._items.filter((current) => current.id !== item.id);
|
|
469
|
+
this._editingId = null;
|
|
470
|
+
this.#applySiteHeaderHtml(response.headerHtml);
|
|
471
|
+
} catch {
|
|
472
|
+
showToast(this.labels.deleteFailed, "error");
|
|
473
|
+
}
|
|
377
474
|
}
|
|
378
475
|
|
|
379
476
|
// ===========================================================================
|
|
@@ -392,21 +489,21 @@ export class JantNavManager extends LitElement {
|
|
|
392
489
|
try {
|
|
393
490
|
const res = await fetch("/api/nav-items", {
|
|
394
491
|
method: "POST",
|
|
395
|
-
headers:
|
|
396
|
-
"Content-Type": "application/json",
|
|
397
|
-
Accept: "application/json",
|
|
398
|
-
},
|
|
492
|
+
headers: this.#jsonMutationHeaders(),
|
|
399
493
|
body: JSON.stringify({ type: "link", label, url, placement: "header" }),
|
|
400
494
|
});
|
|
401
495
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
402
496
|
|
|
403
|
-
const
|
|
497
|
+
const response =
|
|
498
|
+
(await res.json()) as NavManagerMutationResponse<NavManagerItem>;
|
|
499
|
+
const created = this.#normalizeItem(this.#stripHeaderHtml(response));
|
|
404
500
|
this.#destroySortables();
|
|
405
501
|
this._items = [...this._items, created];
|
|
406
502
|
this._newLinkLabel = "";
|
|
407
503
|
this._newLinkUrl = "";
|
|
408
504
|
this._showLinkForm = false;
|
|
409
505
|
document.removeEventListener("click", this.#closeLinkForm);
|
|
506
|
+
this.#applySiteHeaderHtml(response.headerHtml);
|
|
410
507
|
} catch {
|
|
411
508
|
showToast(this.labels.saveFailed, "error");
|
|
412
509
|
} finally {
|
|
@@ -425,10 +522,7 @@ export class JantNavManager extends LitElement {
|
|
|
425
522
|
try {
|
|
426
523
|
const res = await fetch("/api/nav-items", {
|
|
427
524
|
method: "POST",
|
|
428
|
-
headers:
|
|
429
|
-
"Content-Type": "application/json",
|
|
430
|
-
Accept: "application/json",
|
|
431
|
-
},
|
|
525
|
+
headers: this.#jsonMutationHeaders(),
|
|
432
526
|
body: JSON.stringify({
|
|
433
527
|
type: "collection",
|
|
434
528
|
collectionId,
|
|
@@ -437,10 +531,13 @@ export class JantNavManager extends LitElement {
|
|
|
437
531
|
});
|
|
438
532
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
439
533
|
|
|
440
|
-
const
|
|
534
|
+
const response =
|
|
535
|
+
(await res.json()) as NavManagerMutationResponse<NavManagerItem>;
|
|
536
|
+
const created = this.#normalizeItem(this.#stripHeaderHtml(response));
|
|
441
537
|
this.#destroySortables();
|
|
442
538
|
this._items = [...this._items, created];
|
|
443
539
|
this.#closeCollectionPicker();
|
|
540
|
+
this.#applySiteHeaderHtml(response.headerHtml);
|
|
444
541
|
} catch {
|
|
445
542
|
showToast(this.labels.saveFailed, "error");
|
|
446
543
|
} finally {
|
|
@@ -448,6 +545,99 @@ export class JantNavManager extends LitElement {
|
|
|
448
545
|
}
|
|
449
546
|
}
|
|
450
547
|
|
|
548
|
+
// ===========================================================================
|
|
549
|
+
// Suggested link handlers
|
|
550
|
+
// ===========================================================================
|
|
551
|
+
|
|
552
|
+
#getComparableInternalPath(url: string): string | null {
|
|
553
|
+
if (
|
|
554
|
+
url.startsWith("http://") ||
|
|
555
|
+
url.startsWith("https://") ||
|
|
556
|
+
url.startsWith("//") ||
|
|
557
|
+
url.startsWith("mailto:") ||
|
|
558
|
+
url.startsWith("tel:") ||
|
|
559
|
+
url.startsWith("#")
|
|
560
|
+
) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
const pathname = new URL(url, "https://jant.invalid").pathname;
|
|
566
|
+
const normalized = pathname
|
|
567
|
+
.trim()
|
|
568
|
+
.toLowerCase()
|
|
569
|
+
.replace(/^\/+|\/+$/g, "")
|
|
570
|
+
.replace(/\/+/g, "/");
|
|
571
|
+
return normalized ? `/${normalized}` : "/";
|
|
572
|
+
} catch {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
#isSuggestedLinkAdded(link: NavManagerSuggestedLink): boolean {
|
|
578
|
+
const path = this.#getComparableInternalPath(link.url);
|
|
579
|
+
const hasMatchingPath =
|
|
580
|
+
path !== null &&
|
|
581
|
+
this._items.some(
|
|
582
|
+
(item) => this.#getComparableInternalPath(item.url) === path,
|
|
583
|
+
);
|
|
584
|
+
if (hasMatchingPath) return true;
|
|
585
|
+
|
|
586
|
+
return Boolean(
|
|
587
|
+
link.collectionId &&
|
|
588
|
+
this._items.some(
|
|
589
|
+
(item) =>
|
|
590
|
+
item.type === "collection" && item.collectionId === link.collectionId,
|
|
591
|
+
),
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
get #availableSuggestedLinks(): NavManagerSuggestedLink[] {
|
|
596
|
+
return (this.suggestedLinks ?? []).filter(
|
|
597
|
+
(link) => !this.#isSuggestedLinkAdded(link),
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async #handleAddSuggestedLink(link: NavManagerSuggestedLink) {
|
|
602
|
+
if (this._addingSuggestedKey) return;
|
|
603
|
+
|
|
604
|
+
this._addingSuggestedKey = link.key;
|
|
605
|
+
try {
|
|
606
|
+
const body =
|
|
607
|
+
link.navItemType === "collection" && link.collectionId
|
|
608
|
+
? {
|
|
609
|
+
type: "collection",
|
|
610
|
+
collectionId: link.collectionId,
|
|
611
|
+
placement: "header",
|
|
612
|
+
}
|
|
613
|
+
: {
|
|
614
|
+
type: "link",
|
|
615
|
+
label: link.label,
|
|
616
|
+
url: link.url,
|
|
617
|
+
placement: "header",
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const res = await fetch("/api/nav-items", {
|
|
621
|
+
method: "POST",
|
|
622
|
+
headers: this.#jsonMutationHeaders(),
|
|
623
|
+
body: JSON.stringify(body),
|
|
624
|
+
});
|
|
625
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
626
|
+
|
|
627
|
+
const response =
|
|
628
|
+
(await res.json()) as NavManagerMutationResponse<NavManagerItem>;
|
|
629
|
+
const created = this.#normalizeItem(this.#stripHeaderHtml(response));
|
|
630
|
+
this.#destroySortables();
|
|
631
|
+
this._items = [...this._items, created];
|
|
632
|
+
this.#applySiteHeaderHtml(response.headerHtml);
|
|
633
|
+
showToast(this.labels.suggestedLinkAdded);
|
|
634
|
+
} catch {
|
|
635
|
+
showToast(this.labels.saveFailed, "error");
|
|
636
|
+
} finally {
|
|
637
|
+
this._addingSuggestedKey = null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
451
641
|
// ===========================================================================
|
|
452
642
|
// System toggle handlers
|
|
453
643
|
// ===========================================================================
|
|
@@ -465,10 +655,7 @@ export class JantNavManager extends LitElement {
|
|
|
465
655
|
if (enabled) {
|
|
466
656
|
const res = await fetch("/api/nav-items", {
|
|
467
657
|
method: "POST",
|
|
468
|
-
headers:
|
|
469
|
-
"Content-Type": "application/json",
|
|
470
|
-
Accept: "application/json",
|
|
471
|
-
},
|
|
658
|
+
headers: this.#jsonMutationHeaders(),
|
|
472
659
|
body: JSON.stringify({
|
|
473
660
|
type: "system",
|
|
474
661
|
systemKey: config.key,
|
|
@@ -476,12 +663,12 @@ export class JantNavManager extends LitElement {
|
|
|
476
663
|
});
|
|
477
664
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
478
665
|
|
|
479
|
-
const
|
|
666
|
+
const response =
|
|
667
|
+
(await res.json()) as NavManagerMutationResponse<NavManagerItem>;
|
|
668
|
+
const created = this.#normalizeItem(this.#stripHeaderHtml(response));
|
|
480
669
|
this.#destroySortables();
|
|
481
|
-
this._items = [
|
|
482
|
-
|
|
483
|
-
{ ...created, displayLabel: config.label },
|
|
484
|
-
];
|
|
670
|
+
this._items = [...this._items, created];
|
|
671
|
+
this.#applySiteHeaderHtml(response.headerHtml);
|
|
485
672
|
} else {
|
|
486
673
|
const existing = this._items.find(
|
|
487
674
|
(item) => item.type === "system" && item.systemKey === config.key,
|
|
@@ -489,12 +676,16 @@ export class JantNavManager extends LitElement {
|
|
|
489
676
|
if (existing) {
|
|
490
677
|
const res = await fetch(`/api/nav-items/${existing.id}`, {
|
|
491
678
|
method: "DELETE",
|
|
492
|
-
headers:
|
|
679
|
+
headers: this.#deleteMutationHeaders(),
|
|
493
680
|
});
|
|
494
681
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
495
682
|
|
|
683
|
+
const response = (await res.json()) as NavManagerMutationResponse<{
|
|
684
|
+
success: true;
|
|
685
|
+
}>;
|
|
496
686
|
this.#destroySortables();
|
|
497
687
|
this._items = this._items.filter((item) => item.id !== existing.id);
|
|
688
|
+
this.#applySiteHeaderHtml(response.headerHtml);
|
|
498
689
|
}
|
|
499
690
|
}
|
|
500
691
|
} catch {
|
|
@@ -834,6 +1025,45 @@ export class JantNavManager extends LitElement {
|
|
|
834
1025
|
`;
|
|
835
1026
|
}
|
|
836
1027
|
|
|
1028
|
+
#renderSuggestedLinksSection() {
|
|
1029
|
+
const available = this.#availableSuggestedLinks;
|
|
1030
|
+
if (available.length === 0) return nothing;
|
|
1031
|
+
|
|
1032
|
+
return html`
|
|
1033
|
+
<section class="mt-8">
|
|
1034
|
+
<h2 class="text-lg font-semibold mb-1">
|
|
1035
|
+
${this.labels.suggestedLinks}
|
|
1036
|
+
</h2>
|
|
1037
|
+
<p class="text-sm text-muted-foreground mb-3">
|
|
1038
|
+
${this.labels.suggestedLinksDescription}
|
|
1039
|
+
</p>
|
|
1040
|
+
<div class="nav-suggestions-list">
|
|
1041
|
+
${available.map((link) => {
|
|
1042
|
+
const adding = this._addingSuggestedKey === link.key;
|
|
1043
|
+
return html`
|
|
1044
|
+
<div class="nav-suggestion-item">
|
|
1045
|
+
<div class="nav-suggestion-info">
|
|
1046
|
+
<span class="nav-suggestion-title">${link.label}</span>
|
|
1047
|
+
<span class="nav-suggestion-meta">
|
|
1048
|
+
${link.url} · ${link.targetLabel}
|
|
1049
|
+
</span>
|
|
1050
|
+
</div>
|
|
1051
|
+
<button
|
|
1052
|
+
type="button"
|
|
1053
|
+
class="btn-sm"
|
|
1054
|
+
?disabled=${adding || this._addingSuggestedKey !== null}
|
|
1055
|
+
@click=${() => this.#handleAddSuggestedLink(link)}
|
|
1056
|
+
>
|
|
1057
|
+
${this.labels.addSuggestedLink}
|
|
1058
|
+
</button>
|
|
1059
|
+
</div>
|
|
1060
|
+
`;
|
|
1061
|
+
})}
|
|
1062
|
+
</div>
|
|
1063
|
+
</section>
|
|
1064
|
+
`;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
837
1067
|
#renderAddLinkSection() {
|
|
838
1068
|
return html`
|
|
839
1069
|
<section class="mt-8">
|
|
@@ -1175,8 +1405,8 @@ export class JantNavManager extends LitElement {
|
|
|
1175
1405
|
</div>
|
|
1176
1406
|
</section>
|
|
1177
1407
|
|
|
1178
|
-
${this.#
|
|
1179
|
-
${this.#renderSystemToggles()}
|
|
1408
|
+
${this.#renderSuggestedLinksSection()} ${this.#renderAddLinkSection()}
|
|
1409
|
+
${this.#renderAddCollectionSection()} ${this.#renderSystemToggles()}
|
|
1180
1410
|
`;
|
|
1181
1411
|
}
|
|
1182
1412
|
}
|