@jant/core 0.3.31 → 0.3.33
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/dist/client/client.css +1 -1
- package/dist/client/client.js +1442 -989
- package/dist/index.js +1429 -1055
- package/package.json +2 -2
- package/src/__tests__/helpers/app.ts +6 -3
- package/src/__tests__/helpers/db.ts +3 -0
- package/src/client.ts +2 -1
- package/src/db/migrations/0011_add_path_registry.sql +23 -0
- package/src/db/schema.ts +12 -1
- package/src/i18n/locales/en.po +225 -91
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +201 -152
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +201 -152
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/excerpt.test.ts +25 -0
- package/src/lib/__tests__/resolve-config.test.ts +26 -2
- package/src/lib/__tests__/timeline.test.ts +2 -1
- package/src/lib/compose-bridge.ts +30 -1
- package/src/lib/excerpt.ts +16 -7
- package/src/lib/nav-manager-bridge.ts +54 -0
- package/src/lib/navigation.ts +7 -4
- package/src/lib/render.tsx +5 -2
- package/src/lib/resolve-config.ts +7 -0
- package/src/lib/view.ts +42 -10
- package/src/middleware/error-handler.ts +16 -0
- package/src/routes/api/__tests__/posts.test.ts +80 -0
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/posts.ts +6 -29
- package/src/routes/api/upload.ts +2 -14
- package/src/routes/auth/__tests__/setup.test.ts +2 -1
- package/src/routes/compose.tsx +13 -5
- package/src/routes/dash/__tests__/pages.test.ts +2 -1
- package/src/routes/dash/__tests__/settings-avatar.test.ts +151 -33
- package/src/routes/dash/appearance.tsx +71 -4
- package/src/routes/dash/collections.tsx +15 -21
- package/src/routes/dash/media.tsx +1 -13
- package/src/routes/dash/pages.tsx +5 -150
- package/src/routes/dash/posts.tsx +25 -32
- package/src/routes/dash/redirects.tsx +9 -11
- package/src/routes/dash/settings.tsx +29 -111
- package/src/routes/feed/__tests__/rss.test.ts +5 -1
- package/src/routes/pages/__tests__/collections.test.ts +2 -1
- package/src/routes/pages/__tests__/featured.test.ts +2 -1
- package/src/routes/pages/page.tsx +20 -25
- package/src/services/__tests__/collection.test.ts +2 -1
- package/src/services/__tests__/media.test.ts +78 -1
- package/src/services/__tests__/navigation.test.ts +2 -1
- package/src/services/__tests__/page.test.ts +78 -1
- package/src/services/__tests__/path-registry.test.ts +165 -0
- package/src/services/__tests__/post-timeline.test.ts +2 -1
- package/src/services/__tests__/post.test.ts +103 -1
- package/src/services/__tests__/redirect.test.ts +53 -4
- package/src/services/__tests__/search.test.ts +2 -1
- package/src/services/__tests__/settings.test.ts +153 -0
- package/src/services/index.ts +12 -4
- package/src/services/media.ts +72 -4
- package/src/services/page.ts +64 -17
- package/src/services/path-registry.ts +160 -0
- package/src/services/post.ts +119 -24
- package/src/services/redirect.ts +23 -3
- package/src/services/settings.ts +181 -0
- package/src/styles/components.css +135 -0
- package/src/styles/tokens.css +6 -1
- package/src/styles/ui.css +70 -26
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +7 -2
- package/src/types/constants.ts +9 -1
- package/src/types/sortablejs.d.ts +8 -2
- package/src/types/views.ts +1 -1
- package/src/ui/color-themes.ts +31 -31
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +0 -3
- package/src/ui/components/__tests__/jant-settings-general.test.ts +2 -6
- package/src/ui/components/jant-compose-dialog.ts +3 -2
- package/src/ui/components/jant-compose-editor.ts +17 -2
- package/src/ui/components/jant-nav-manager.ts +1067 -0
- package/src/ui/components/jant-settings-general.ts +2 -35
- package/src/ui/components/nav-manager-types.ts +72 -0
- package/src/ui/components/settings-types.ts +0 -3
- package/src/ui/compose/ComposePrompt.tsx +3 -11
- package/src/ui/dash/appearance/AdvancedContent.tsx +0 -3
- package/src/ui/dash/appearance/AppearanceNav.tsx +12 -8
- package/src/ui/dash/appearance/ColorThemeContent.tsx +1 -4
- package/src/ui/dash/appearance/FontThemeContent.tsx +0 -3
- package/src/ui/dash/appearance/NavigationContent.tsx +302 -0
- package/src/ui/dash/pages/PagesContent.tsx +74 -0
- package/src/ui/dash/settings/AccountContent.tsx +0 -3
- package/src/ui/dash/settings/GeneralContent.tsx +1 -19
- package/src/ui/dash/settings/SettingsNav.tsx +2 -6
- package/src/ui/feed/NoteCard.tsx +2 -2
- package/src/ui/layouts/DashLayout.tsx +83 -86
- package/src/ui/layouts/SiteLayout.tsx +82 -21
- package/src/lib/nav-reorder.ts +0 -26
- package/src/ui/dash/pages/LinkFormContent.tsx +0 -119
- package/src/ui/dash/pages/UnifiedPagesContent.tsx +0 -203
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation Manager Component
|
|
3
|
+
*
|
|
4
|
+
* Manages nav item reordering with a live preview:
|
|
5
|
+
* - Renders a preview bar that reflects current item order
|
|
6
|
+
* - Sortable list with inline edit/delete panels
|
|
7
|
+
* - SortableJS drag-and-drop reorder with immediate preview update
|
|
8
|
+
* - Add page/link forms
|
|
9
|
+
* - System nav item toggles with immediate list/preview update
|
|
10
|
+
* - Dispatches events for update/delete (handled by bridge)
|
|
11
|
+
*
|
|
12
|
+
* Light DOM only — BaseCoat and Tailwind classes apply directly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { LitElement, html, nothing } from "lit";
|
|
16
|
+
import type { PropertyValueMap } from "lit";
|
|
17
|
+
import Sortable from "sortablejs";
|
|
18
|
+
import { showToast } from "../../lib/toast.js";
|
|
19
|
+
import type {
|
|
20
|
+
AvailablePage,
|
|
21
|
+
NavManagerItem,
|
|
22
|
+
NavManagerLabels,
|
|
23
|
+
NavManagerUpdateDetail,
|
|
24
|
+
NavManagerDeleteDetail,
|
|
25
|
+
SystemNavConfig,
|
|
26
|
+
} from "./nav-manager-types.js";
|
|
27
|
+
|
|
28
|
+
export class JantNavManager extends LitElement {
|
|
29
|
+
static properties = {
|
|
30
|
+
items: { type: Array },
|
|
31
|
+
labels: { type: Object },
|
|
32
|
+
systemNavItems: { type: Array, attribute: "system-nav-items" },
|
|
33
|
+
availablePages: { type: Array, attribute: "available-pages" },
|
|
34
|
+
siteName: { type: String, attribute: "site-name" },
|
|
35
|
+
maxVisible: { type: Number, attribute: "max-visible" },
|
|
36
|
+
homeDefaultView: { type: String, attribute: "home-default-view" },
|
|
37
|
+
|
|
38
|
+
_items: { state: true },
|
|
39
|
+
_editingId: { state: true },
|
|
40
|
+
_editLabel: { state: true },
|
|
41
|
+
_editUrl: { state: true },
|
|
42
|
+
_togglingKeys: { state: true },
|
|
43
|
+
_showOverflow: { state: true },
|
|
44
|
+
_showPagePicker: { state: true },
|
|
45
|
+
_showLinkForm: { state: true },
|
|
46
|
+
_newLinkLabel: { state: true },
|
|
47
|
+
_newLinkUrl: { state: true },
|
|
48
|
+
_availablePages: { state: true },
|
|
49
|
+
_addingPageId: { state: true },
|
|
50
|
+
_addingLink: { state: true },
|
|
51
|
+
_pageSearchQuery: { state: true },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
declare items: NavManagerItem[];
|
|
55
|
+
declare labels: NavManagerLabels;
|
|
56
|
+
declare systemNavItems: SystemNavConfig[];
|
|
57
|
+
declare availablePages: AvailablePage[];
|
|
58
|
+
declare siteName: string;
|
|
59
|
+
declare maxVisible: number;
|
|
60
|
+
declare homeDefaultView: string;
|
|
61
|
+
|
|
62
|
+
declare _items: NavManagerItem[];
|
|
63
|
+
declare _editingId: number | null;
|
|
64
|
+
declare _editLabel: string;
|
|
65
|
+
declare _editUrl: string;
|
|
66
|
+
/** Keys currently mid-request (to disable switch during toggle) */
|
|
67
|
+
declare _togglingKeys: Set<string>;
|
|
68
|
+
declare _showOverflow: boolean;
|
|
69
|
+
declare _showPagePicker: boolean;
|
|
70
|
+
declare _showLinkForm: boolean;
|
|
71
|
+
declare _newLinkLabel: string;
|
|
72
|
+
declare _newLinkUrl: string;
|
|
73
|
+
declare _availablePages: AvailablePage[];
|
|
74
|
+
/** Page ID currently being added (to disable its button) */
|
|
75
|
+
declare _addingPageId: number | null;
|
|
76
|
+
declare _addingLink: boolean;
|
|
77
|
+
declare _pageSearchQuery: string;
|
|
78
|
+
|
|
79
|
+
#sortable: { destroy(): void } | null = null;
|
|
80
|
+
#initialized = false;
|
|
81
|
+
#closeOverflow = () => {
|
|
82
|
+
this._showOverflow = false;
|
|
83
|
+
document.removeEventListener("click", this.#closeOverflow);
|
|
84
|
+
};
|
|
85
|
+
#closePagePicker = () => {
|
|
86
|
+
this._showPagePicker = false;
|
|
87
|
+
this._pageSearchQuery = "";
|
|
88
|
+
document.removeEventListener("click", this.#closePagePicker);
|
|
89
|
+
};
|
|
90
|
+
#closeLinkForm = () => {
|
|
91
|
+
this._showLinkForm = false;
|
|
92
|
+
document.removeEventListener("click", this.#closeLinkForm);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
createRenderRoot() {
|
|
96
|
+
this.innerHTML = "";
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
constructor() {
|
|
101
|
+
super();
|
|
102
|
+
this.items = [];
|
|
103
|
+
this.labels = {} as NavManagerLabels;
|
|
104
|
+
this.systemNavItems = [];
|
|
105
|
+
this.availablePages = [];
|
|
106
|
+
this.siteName = "";
|
|
107
|
+
this.maxVisible = 3;
|
|
108
|
+
this.homeDefaultView = "latest";
|
|
109
|
+
|
|
110
|
+
this._items = [];
|
|
111
|
+
this._editingId = null;
|
|
112
|
+
this._editLabel = "";
|
|
113
|
+
this._editUrl = "";
|
|
114
|
+
this._togglingKeys = new Set();
|
|
115
|
+
this._showOverflow = false;
|
|
116
|
+
this._showPagePicker = false;
|
|
117
|
+
this._showLinkForm = false;
|
|
118
|
+
this._newLinkLabel = "";
|
|
119
|
+
this._newLinkUrl = "";
|
|
120
|
+
this._availablePages = [];
|
|
121
|
+
this._addingPageId = null;
|
|
122
|
+
this._addingLink = false;
|
|
123
|
+
this._pageSearchQuery = "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
protected update(changedProperties: PropertyValueMap<JantNavManager>): void {
|
|
127
|
+
if (!this.#initialized || changedProperties.has("items")) {
|
|
128
|
+
this._items = [...(this.items ?? [])];
|
|
129
|
+
this.#initialized = true;
|
|
130
|
+
}
|
|
131
|
+
if (changedProperties.has("availablePages" as keyof JantNavManager)) {
|
|
132
|
+
this._availablePages = [...(this.availablePages ?? [])];
|
|
133
|
+
}
|
|
134
|
+
super.update(changedProperties);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
protected updated(): void {
|
|
138
|
+
this.#initSortable();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
disconnectedCallback() {
|
|
142
|
+
super.disconnectedCallback();
|
|
143
|
+
this.#sortable?.destroy();
|
|
144
|
+
this.#sortable = null;
|
|
145
|
+
document.removeEventListener("click", this.#closeOverflow);
|
|
146
|
+
document.removeEventListener("click", this.#closePagePicker);
|
|
147
|
+
document.removeEventListener("click", this.#closeLinkForm);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ===========================================================================
|
|
151
|
+
// SortableJS
|
|
152
|
+
// ===========================================================================
|
|
153
|
+
|
|
154
|
+
#initSortable() {
|
|
155
|
+
const list = this.querySelector<HTMLElement>("#nav-items-list");
|
|
156
|
+
if (!list || this.#sortable) return;
|
|
157
|
+
|
|
158
|
+
this.#sortable = Sortable.create(list, {
|
|
159
|
+
animation: 150,
|
|
160
|
+
handle: "[data-drag-handle]",
|
|
161
|
+
onEnd: (evt) => {
|
|
162
|
+
// Read new order from DOM BEFORE reverting
|
|
163
|
+
const els = [...list.querySelectorAll<HTMLElement>("[data-nav-id]")];
|
|
164
|
+
const ids = els.map((el) => Number(el.dataset.navId));
|
|
165
|
+
|
|
166
|
+
// Revert SortableJS DOM manipulation so Lit can re-render cleanly.
|
|
167
|
+
// SortableJS physically moved the element — put it back where it was.
|
|
168
|
+
const { item, oldIndex, newIndex } = evt;
|
|
169
|
+
if (oldIndex != null && newIndex != null && oldIndex !== newIndex) {
|
|
170
|
+
// Remove the item from its new position
|
|
171
|
+
item.parentNode?.removeChild(item);
|
|
172
|
+
// Re-insert at the original position
|
|
173
|
+
const children = list.children;
|
|
174
|
+
if (oldIndex >= children.length) {
|
|
175
|
+
list.appendChild(item);
|
|
176
|
+
} else {
|
|
177
|
+
list.insertBefore(item, children[oldIndex]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Destroy sortable so it doesn't fight Lit's re-render
|
|
182
|
+
this.#sortable?.destroy();
|
|
183
|
+
this.#sortable = null;
|
|
184
|
+
|
|
185
|
+
// Update internal state so Lit re-renders in the new order
|
|
186
|
+
const itemMap = new Map(this._items.map((i) => [i.id, i]));
|
|
187
|
+
this._items = ids
|
|
188
|
+
.map((id) => itemMap.get(id))
|
|
189
|
+
.filter((i): i is NavManagerItem => i !== undefined);
|
|
190
|
+
|
|
191
|
+
// Persist to server
|
|
192
|
+
fetch("/api/nav-items/reorder", {
|
|
193
|
+
method: "PUT",
|
|
194
|
+
headers: { "Content-Type": "application/json" },
|
|
195
|
+
body: JSON.stringify({ ids }),
|
|
196
|
+
}).then((res) => {
|
|
197
|
+
if (res.ok) showToast(this.labels.orderSaved);
|
|
198
|
+
else showToast(this.labels.saveFailed, "error");
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ===========================================================================
|
|
205
|
+
// Inline edit handlers
|
|
206
|
+
// ===========================================================================
|
|
207
|
+
|
|
208
|
+
#toggleEdit(item: NavManagerItem) {
|
|
209
|
+
if (this._editingId === item.id) {
|
|
210
|
+
this._editingId = null;
|
|
211
|
+
} else {
|
|
212
|
+
this._editingId = item.id;
|
|
213
|
+
this._editLabel = item.label;
|
|
214
|
+
this._editUrl = item.url;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#handleUpdate(item: NavManagerItem) {
|
|
219
|
+
const label = this._editLabel.trim();
|
|
220
|
+
if (!label) {
|
|
221
|
+
showToast(this.labels.labelRequired, "error");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const detail: NavManagerUpdateDetail = {
|
|
226
|
+
id: item.id,
|
|
227
|
+
label,
|
|
228
|
+
...(item.type === "link" && { url: this._editUrl.trim() }),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
this.dispatchEvent(
|
|
232
|
+
new CustomEvent<NavManagerUpdateDetail>("jant:nav-update", {
|
|
233
|
+
bubbles: true,
|
|
234
|
+
detail,
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#handleDelete(item: NavManagerItem) {
|
|
240
|
+
this.dispatchEvent(
|
|
241
|
+
new CustomEvent<NavManagerDeleteDetail>("jant:nav-delete", {
|
|
242
|
+
bubbles: true,
|
|
243
|
+
detail: { id: item.id },
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ===========================================================================
|
|
249
|
+
// Max visible handler
|
|
250
|
+
// ===========================================================================
|
|
251
|
+
|
|
252
|
+
async #handleMaxVisibleChange(value: number) {
|
|
253
|
+
const clamped = Math.max(0, Math.min(5, value));
|
|
254
|
+
this.maxVisible = clamped;
|
|
255
|
+
try {
|
|
256
|
+
const res = await fetch("/dash/appearance/nav-max-visible", {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: { "Content-Type": "application/json" },
|
|
259
|
+
body: JSON.stringify({ value: clamped }),
|
|
260
|
+
});
|
|
261
|
+
if (res.ok) showToast(this.labels.maxVisibleSaved);
|
|
262
|
+
else showToast(this.labels.saveFailed, "error");
|
|
263
|
+
} catch {
|
|
264
|
+
showToast(this.labels.saveFailed, "error");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async #handleHomeViewToggle(useFeatured: boolean) {
|
|
269
|
+
this.homeDefaultView = useFeatured ? "featured" : "latest";
|
|
270
|
+
try {
|
|
271
|
+
const res = await fetch("/dash/appearance/home-default-view", {
|
|
272
|
+
method: "POST",
|
|
273
|
+
headers: { "Content-Type": "application/json" },
|
|
274
|
+
body: JSON.stringify({ value: this.homeDefaultView }),
|
|
275
|
+
});
|
|
276
|
+
if (res.ok) showToast(this.labels.homeViewSaved);
|
|
277
|
+
else showToast(this.labels.saveFailed, "error");
|
|
278
|
+
} catch {
|
|
279
|
+
showToast(this.labels.saveFailed, "error");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ===========================================================================
|
|
284
|
+
// Add page / link handlers
|
|
285
|
+
// ===========================================================================
|
|
286
|
+
|
|
287
|
+
async #handleAddPage(page: AvailablePage) {
|
|
288
|
+
this._addingPageId = page.id;
|
|
289
|
+
try {
|
|
290
|
+
const res = await fetch("/api/nav-items", {
|
|
291
|
+
method: "POST",
|
|
292
|
+
headers: {
|
|
293
|
+
"Content-Type": "application/json",
|
|
294
|
+
Accept: "application/json",
|
|
295
|
+
},
|
|
296
|
+
body: JSON.stringify({
|
|
297
|
+
type: "page",
|
|
298
|
+
label: page.title || page.slug,
|
|
299
|
+
url: `/${page.slug}`,
|
|
300
|
+
pageId: page.id,
|
|
301
|
+
}),
|
|
302
|
+
});
|
|
303
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
304
|
+
|
|
305
|
+
const created: NavManagerItem = await res.json();
|
|
306
|
+
this.#sortable?.destroy();
|
|
307
|
+
this.#sortable = null;
|
|
308
|
+
this._items = [...this._items, created];
|
|
309
|
+
this._availablePages = this._availablePages.filter(
|
|
310
|
+
(p) => p.id !== page.id,
|
|
311
|
+
);
|
|
312
|
+
} catch {
|
|
313
|
+
showToast(this.labels.saveFailed, "error");
|
|
314
|
+
} finally {
|
|
315
|
+
this._addingPageId = null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async #handleAddLink() {
|
|
320
|
+
const label = this._newLinkLabel.trim();
|
|
321
|
+
const url = this._newLinkUrl.trim();
|
|
322
|
+
if (!label || !url) {
|
|
323
|
+
showToast(this.labels.labelAndUrlRequired, "error");
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this._addingLink = true;
|
|
328
|
+
try {
|
|
329
|
+
const res = await fetch("/api/nav-items", {
|
|
330
|
+
method: "POST",
|
|
331
|
+
headers: {
|
|
332
|
+
"Content-Type": "application/json",
|
|
333
|
+
Accept: "application/json",
|
|
334
|
+
},
|
|
335
|
+
body: JSON.stringify({ type: "link", label, url }),
|
|
336
|
+
});
|
|
337
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
338
|
+
|
|
339
|
+
const created: NavManagerItem = await res.json();
|
|
340
|
+
this.#sortable?.destroy();
|
|
341
|
+
this.#sortable = null;
|
|
342
|
+
this._items = [...this._items, created];
|
|
343
|
+
this._newLinkLabel = "";
|
|
344
|
+
this._newLinkUrl = "";
|
|
345
|
+
this._showLinkForm = false;
|
|
346
|
+
document.removeEventListener("click", this.#closeLinkForm);
|
|
347
|
+
} catch {
|
|
348
|
+
showToast(this.labels.saveFailed, "error");
|
|
349
|
+
} finally {
|
|
350
|
+
this._addingLink = false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ===========================================================================
|
|
355
|
+
// System toggle handlers
|
|
356
|
+
// ===========================================================================
|
|
357
|
+
|
|
358
|
+
#isSystemEnabled(config: SystemNavConfig): boolean {
|
|
359
|
+
return this._items.some(
|
|
360
|
+
(item) => item.type === "system" && item.url === config.url,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async #handleSystemToggle(config: SystemNavConfig, enabled: boolean) {
|
|
365
|
+
this._togglingKeys = new Set([...this._togglingKeys, config.key]);
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
if (enabled) {
|
|
369
|
+
const res = await fetch("/api/nav-items", {
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers: {
|
|
372
|
+
"Content-Type": "application/json",
|
|
373
|
+
Accept: "application/json",
|
|
374
|
+
},
|
|
375
|
+
body: JSON.stringify({
|
|
376
|
+
type: "system",
|
|
377
|
+
label: config.defaultLabel,
|
|
378
|
+
url: config.url,
|
|
379
|
+
}),
|
|
380
|
+
});
|
|
381
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
382
|
+
|
|
383
|
+
const created: NavManagerItem = await res.json();
|
|
384
|
+
this.#sortable?.destroy();
|
|
385
|
+
this.#sortable = null;
|
|
386
|
+
this._items = [...this._items, created];
|
|
387
|
+
} else {
|
|
388
|
+
const existing = this._items.find(
|
|
389
|
+
(item) => item.type === "system" && item.url === config.url,
|
|
390
|
+
);
|
|
391
|
+
if (existing) {
|
|
392
|
+
const res = await fetch(`/api/nav-items/${existing.id}`, {
|
|
393
|
+
method: "DELETE",
|
|
394
|
+
headers: { Accept: "application/json" },
|
|
395
|
+
});
|
|
396
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
397
|
+
|
|
398
|
+
this.#sortable?.destroy();
|
|
399
|
+
this.#sortable = null;
|
|
400
|
+
this._items = this._items.filter((item) => item.id !== existing.id);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} catch {
|
|
404
|
+
showToast(this.labels.saveFailed, "error");
|
|
405
|
+
this.requestUpdate();
|
|
406
|
+
} finally {
|
|
407
|
+
const next = new Set(this._togglingKeys);
|
|
408
|
+
next.delete(config.key);
|
|
409
|
+
this._togglingKeys = next;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ===========================================================================
|
|
414
|
+
// Render helpers
|
|
415
|
+
// ===========================================================================
|
|
416
|
+
|
|
417
|
+
#renderPreview() {
|
|
418
|
+
const visible = this._items.slice(0, this.maxVisible);
|
|
419
|
+
const overflow = this._items.slice(this.maxVisible);
|
|
420
|
+
const hasMore = overflow.length > 0;
|
|
421
|
+
|
|
422
|
+
return html`
|
|
423
|
+
<div class="border rounded-lg">
|
|
424
|
+
<p class="text-xs text-muted-foreground px-4 pt-3">
|
|
425
|
+
${this.labels.preview}
|
|
426
|
+
</p>
|
|
427
|
+
<div class="px-5 py-3">
|
|
428
|
+
<div class="site-header-top">
|
|
429
|
+
<a href="/" class="site-logo">${this.siteName}</a>
|
|
430
|
+
<div class="site-header-right">
|
|
431
|
+
${visible.length > 0 || hasMore
|
|
432
|
+
? html`<nav class="site-header-nav">
|
|
433
|
+
${visible.map(
|
|
434
|
+
(item) =>
|
|
435
|
+
html`<a href=${item.url} class="site-header-link">
|
|
436
|
+
${item.label}
|
|
437
|
+
</a>`,
|
|
438
|
+
)}
|
|
439
|
+
${hasMore
|
|
440
|
+
? html`<div class="dropdown-menu site-header-more">
|
|
441
|
+
<button
|
|
442
|
+
type="button"
|
|
443
|
+
class="site-header-more-btn"
|
|
444
|
+
aria-haspopup="menu"
|
|
445
|
+
aria-expanded=${this._showOverflow}
|
|
446
|
+
@click=${(e: Event) => {
|
|
447
|
+
e.stopPropagation();
|
|
448
|
+
this._showOverflow = !this._showOverflow;
|
|
449
|
+
if (this._showOverflow) {
|
|
450
|
+
setTimeout(() => {
|
|
451
|
+
document.addEventListener(
|
|
452
|
+
"click",
|
|
453
|
+
this.#closeOverflow,
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
} else {
|
|
457
|
+
document.removeEventListener(
|
|
458
|
+
"click",
|
|
459
|
+
this.#closeOverflow,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}}
|
|
463
|
+
>
|
|
464
|
+
<svg
|
|
465
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
466
|
+
width="16"
|
|
467
|
+
height="16"
|
|
468
|
+
viewBox="0 0 24 24"
|
|
469
|
+
fill="currentColor"
|
|
470
|
+
>
|
|
471
|
+
<circle cx="5" cy="12" r="2" />
|
|
472
|
+
<circle cx="12" cy="12" r="2" />
|
|
473
|
+
<circle cx="19" cy="12" r="2" />
|
|
474
|
+
</svg>
|
|
475
|
+
</button>
|
|
476
|
+
${this._showOverflow
|
|
477
|
+
? html`<div
|
|
478
|
+
data-popover
|
|
479
|
+
data-align="end"
|
|
480
|
+
aria-hidden="false"
|
|
481
|
+
>
|
|
482
|
+
<div role="menu">
|
|
483
|
+
${overflow.map(
|
|
484
|
+
(item) =>
|
|
485
|
+
html`<a href=${item.url} role="menuitem">
|
|
486
|
+
${item.label}
|
|
487
|
+
</a>`,
|
|
488
|
+
)}
|
|
489
|
+
</div>
|
|
490
|
+
</div>`
|
|
491
|
+
: nothing}
|
|
492
|
+
</div>`
|
|
493
|
+
: nothing}
|
|
494
|
+
</nav>`
|
|
495
|
+
: nothing}
|
|
496
|
+
<span class="site-header-search" aria-hidden="true">
|
|
497
|
+
<svg
|
|
498
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
499
|
+
width="16"
|
|
500
|
+
height="16"
|
|
501
|
+
viewBox="0 0 24 24"
|
|
502
|
+
fill="none"
|
|
503
|
+
stroke="currentColor"
|
|
504
|
+
stroke-width="2"
|
|
505
|
+
stroke-linecap="round"
|
|
506
|
+
stroke-linejoin="round"
|
|
507
|
+
>
|
|
508
|
+
<circle cx="11" cy="11" r="8" />
|
|
509
|
+
<path d="m21 21-4.35-4.35" />
|
|
510
|
+
</svg>
|
|
511
|
+
</span>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
<nav class="site-browse-nav">
|
|
515
|
+
<span class="site-browse-link site-browse-link-active">
|
|
516
|
+
${this.homeDefaultView === "featured"
|
|
517
|
+
? this.labels.featured
|
|
518
|
+
: this.labels.latest}
|
|
519
|
+
</span>
|
|
520
|
+
<span class="site-browse-sep" aria-hidden="true">/</span>
|
|
521
|
+
<span class="site-browse-link">
|
|
522
|
+
${this.homeDefaultView === "featured"
|
|
523
|
+
? this.labels.latest
|
|
524
|
+
: this.labels.featured}
|
|
525
|
+
</span>
|
|
526
|
+
</nav>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
#renderTypeBadge(type: string) {
|
|
533
|
+
const label =
|
|
534
|
+
type === "page"
|
|
535
|
+
? this.labels.page
|
|
536
|
+
: type === "system"
|
|
537
|
+
? this.labels.system
|
|
538
|
+
: this.labels.link;
|
|
539
|
+
return html`<span class="badge-secondary">${label}</span>`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
#renderEditPanel(item: NavManagerItem) {
|
|
543
|
+
if (this._editingId !== item.id) return nothing;
|
|
544
|
+
|
|
545
|
+
if (item.type === "link") {
|
|
546
|
+
return html`
|
|
547
|
+
<div class="pb-4 pl-8 flex flex-col gap-3">
|
|
548
|
+
<div class="field">
|
|
549
|
+
<label class="label">${this.labels.label}</label>
|
|
550
|
+
<input
|
|
551
|
+
type="text"
|
|
552
|
+
class="input"
|
|
553
|
+
required
|
|
554
|
+
.value=${this._editLabel}
|
|
555
|
+
@input=${(e: Event) => {
|
|
556
|
+
this._editLabel = (e.target as HTMLInputElement).value;
|
|
557
|
+
}}
|
|
558
|
+
/>
|
|
559
|
+
</div>
|
|
560
|
+
<div class="field">
|
|
561
|
+
<label class="label">${this.labels.url}</label>
|
|
562
|
+
<input
|
|
563
|
+
type="text"
|
|
564
|
+
class="input"
|
|
565
|
+
required
|
|
566
|
+
.value=${this._editUrl}
|
|
567
|
+
@input=${(e: Event) => {
|
|
568
|
+
this._editUrl = (e.target as HTMLInputElement).value;
|
|
569
|
+
}}
|
|
570
|
+
/>
|
|
571
|
+
</div>
|
|
572
|
+
<div class="flex gap-2">
|
|
573
|
+
<button
|
|
574
|
+
type="button"
|
|
575
|
+
class="btn-sm"
|
|
576
|
+
@click=${() => this.#handleUpdate(item)}
|
|
577
|
+
>
|
|
578
|
+
${this.labels.save}
|
|
579
|
+
</button>
|
|
580
|
+
<button
|
|
581
|
+
type="button"
|
|
582
|
+
class="btn-sm-ghost text-destructive"
|
|
583
|
+
@click=${() => this.#handleDelete(item)}
|
|
584
|
+
>
|
|
585
|
+
${this.labels.delete}
|
|
586
|
+
</button>
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
`;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (item.type === "page") {
|
|
593
|
+
return html`
|
|
594
|
+
<div class="pb-4 pl-8 flex flex-col gap-3">
|
|
595
|
+
<code class="text-sm text-muted-foreground">${item.url}</code>
|
|
596
|
+
<div class="flex gap-2">
|
|
597
|
+
${item.pageId
|
|
598
|
+
? html`<a
|
|
599
|
+
href=${`/dash/pages/${item.pageId}/edit`}
|
|
600
|
+
class="btn-sm-outline"
|
|
601
|
+
>${this.labels.editPage}</a
|
|
602
|
+
>`
|
|
603
|
+
: nothing}
|
|
604
|
+
<button
|
|
605
|
+
type="button"
|
|
606
|
+
class="btn-sm-ghost text-destructive"
|
|
607
|
+
@click=${() => this.#handleDelete(item)}
|
|
608
|
+
>
|
|
609
|
+
${this.labels.remove}
|
|
610
|
+
</button>
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (item.type === "system") {
|
|
617
|
+
return html`
|
|
618
|
+
<div class="pb-4 pl-8 flex flex-col gap-3">
|
|
619
|
+
<code class="text-sm text-muted-foreground">${item.url}</code>
|
|
620
|
+
<div class="field">
|
|
621
|
+
<label class="label">${this.labels.label}</label>
|
|
622
|
+
<input
|
|
623
|
+
type="text"
|
|
624
|
+
class="input"
|
|
625
|
+
required
|
|
626
|
+
.value=${this._editLabel}
|
|
627
|
+
@input=${(e: Event) => {
|
|
628
|
+
this._editLabel = (e.target as HTMLInputElement).value;
|
|
629
|
+
}}
|
|
630
|
+
/>
|
|
631
|
+
</div>
|
|
632
|
+
<div class="flex gap-2">
|
|
633
|
+
<button
|
|
634
|
+
type="button"
|
|
635
|
+
class="btn-sm"
|
|
636
|
+
@click=${() => this.#handleUpdate(item)}
|
|
637
|
+
>
|
|
638
|
+
${this.labels.save}
|
|
639
|
+
</button>
|
|
640
|
+
<button
|
|
641
|
+
type="button"
|
|
642
|
+
class="btn-sm-ghost text-destructive"
|
|
643
|
+
@click=${() => this.#handleDelete(item)}
|
|
644
|
+
>
|
|
645
|
+
${this.labels.remove}
|
|
646
|
+
</button>
|
|
647
|
+
</div>
|
|
648
|
+
</div>
|
|
649
|
+
`;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return nothing;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
#renderItem(item: NavManagerItem) {
|
|
656
|
+
return html`
|
|
657
|
+
<div data-nav-id=${item.id}>
|
|
658
|
+
<div class="flex items-center py-3 gap-2">
|
|
659
|
+
<div
|
|
660
|
+
class="flex items-center gap-3 cursor-grab flex-1 min-w-0"
|
|
661
|
+
data-drag-handle
|
|
662
|
+
>
|
|
663
|
+
<svg
|
|
664
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
665
|
+
width="16"
|
|
666
|
+
height="16"
|
|
667
|
+
viewBox="0 0 24 24"
|
|
668
|
+
fill="none"
|
|
669
|
+
stroke="currentColor"
|
|
670
|
+
stroke-width="2"
|
|
671
|
+
stroke-linecap="round"
|
|
672
|
+
stroke-linejoin="round"
|
|
673
|
+
class="text-muted-foreground shrink-0"
|
|
674
|
+
>
|
|
675
|
+
<circle cx="9" cy="12" r="1" />
|
|
676
|
+
<circle cx="9" cy="5" r="1" />
|
|
677
|
+
<circle cx="9" cy="19" r="1" />
|
|
678
|
+
<circle cx="15" cy="12" r="1" />
|
|
679
|
+
<circle cx="15" cy="5" r="1" />
|
|
680
|
+
<circle cx="15" cy="19" r="1" />
|
|
681
|
+
</svg>
|
|
682
|
+
<span class="font-medium truncate">${item.label}</span>
|
|
683
|
+
</div>
|
|
684
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
685
|
+
${this.#renderTypeBadge(item.type)}
|
|
686
|
+
<button
|
|
687
|
+
type="button"
|
|
688
|
+
class="btn-sm-ghost"
|
|
689
|
+
@click=${() => this.#toggleEdit(item)}
|
|
690
|
+
aria-label=${this.labels.toggleEdit}
|
|
691
|
+
>
|
|
692
|
+
<svg
|
|
693
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
694
|
+
width="16"
|
|
695
|
+
height="16"
|
|
696
|
+
viewBox="0 0 24 24"
|
|
697
|
+
fill="none"
|
|
698
|
+
stroke="currentColor"
|
|
699
|
+
stroke-width="2"
|
|
700
|
+
stroke-linecap="round"
|
|
701
|
+
stroke-linejoin="round"
|
|
702
|
+
>
|
|
703
|
+
<path d="m6 9 6 6 6-6" />
|
|
704
|
+
</svg>
|
|
705
|
+
</button>
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
${this.#renderEditPanel(item)}
|
|
709
|
+
</div>
|
|
710
|
+
`;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
#renderAddArea() {
|
|
714
|
+
return html`
|
|
715
|
+
${this.#renderAddPageSection()} ${this.#renderAddLinkSection()}
|
|
716
|
+
`;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
#renderAddPageSection() {
|
|
720
|
+
const query = this._pageSearchQuery.toLowerCase();
|
|
721
|
+
const filteredPages = query
|
|
722
|
+
? this._availablePages.filter((p) =>
|
|
723
|
+
(p.title || p.slug).toLowerCase().includes(query),
|
|
724
|
+
)
|
|
725
|
+
: this._availablePages;
|
|
726
|
+
|
|
727
|
+
return html`
|
|
728
|
+
<section class="mt-8">
|
|
729
|
+
<h2 class="text-lg font-semibold mb-3">
|
|
730
|
+
${this.labels.addPageToNavigation}
|
|
731
|
+
</h2>
|
|
732
|
+
<div id="nav-page-select" class="select">
|
|
733
|
+
<button
|
|
734
|
+
type="button"
|
|
735
|
+
class="btn-outline w-full sm:w-[280px]"
|
|
736
|
+
id="nav-page-select-trigger"
|
|
737
|
+
aria-haspopup="listbox"
|
|
738
|
+
aria-expanded=${this._showPagePicker}
|
|
739
|
+
aria-controls="nav-page-select-listbox"
|
|
740
|
+
@click=${(e: Event) => {
|
|
741
|
+
e.stopPropagation();
|
|
742
|
+
this._showPagePicker = !this._showPagePicker;
|
|
743
|
+
this._pageSearchQuery = "";
|
|
744
|
+
if (this._showPagePicker) {
|
|
745
|
+
setTimeout(() => {
|
|
746
|
+
document.addEventListener("click", this.#closePagePicker);
|
|
747
|
+
this.querySelector<HTMLInputElement>(
|
|
748
|
+
"#nav-page-search",
|
|
749
|
+
)?.focus();
|
|
750
|
+
});
|
|
751
|
+
} else {
|
|
752
|
+
document.removeEventListener("click", this.#closePagePicker);
|
|
753
|
+
}
|
|
754
|
+
}}
|
|
755
|
+
>
|
|
756
|
+
<span class="truncate">${this.labels.choosePage}</span>
|
|
757
|
+
<svg
|
|
758
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
759
|
+
width="24"
|
|
760
|
+
height="24"
|
|
761
|
+
viewBox="0 0 24 24"
|
|
762
|
+
fill="none"
|
|
763
|
+
stroke="currentColor"
|
|
764
|
+
stroke-width="2"
|
|
765
|
+
stroke-linecap="round"
|
|
766
|
+
stroke-linejoin="round"
|
|
767
|
+
class="text-muted-foreground opacity-50 shrink-0"
|
|
768
|
+
>
|
|
769
|
+
<path d="m7 15 5 5 5-5" />
|
|
770
|
+
<path d="m7 9 5-5 5 5" />
|
|
771
|
+
</svg>
|
|
772
|
+
</button>
|
|
773
|
+
${this._showPagePicker
|
|
774
|
+
? html`
|
|
775
|
+
<div
|
|
776
|
+
id="nav-page-select-popover"
|
|
777
|
+
data-popover
|
|
778
|
+
aria-hidden="false"
|
|
779
|
+
class="w-full sm:w-[280px]"
|
|
780
|
+
@click=${(e: Event) => e.stopPropagation()}
|
|
781
|
+
>
|
|
782
|
+
<header>
|
|
783
|
+
<svg
|
|
784
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
785
|
+
width="24"
|
|
786
|
+
height="24"
|
|
787
|
+
viewBox="0 0 24 24"
|
|
788
|
+
fill="none"
|
|
789
|
+
stroke="currentColor"
|
|
790
|
+
stroke-width="2"
|
|
791
|
+
stroke-linecap="round"
|
|
792
|
+
stroke-linejoin="round"
|
|
793
|
+
>
|
|
794
|
+
<circle cx="11" cy="11" r="8" />
|
|
795
|
+
<path d="m21 21-4.3-4.3" />
|
|
796
|
+
</svg>
|
|
797
|
+
<input
|
|
798
|
+
type="text"
|
|
799
|
+
id="nav-page-search"
|
|
800
|
+
.value=${this._pageSearchQuery}
|
|
801
|
+
placeholder=${this.labels.searchPages}
|
|
802
|
+
autocomplete="off"
|
|
803
|
+
autocorrect="off"
|
|
804
|
+
spellcheck="false"
|
|
805
|
+
aria-autocomplete="list"
|
|
806
|
+
role="combobox"
|
|
807
|
+
aria-expanded="true"
|
|
808
|
+
aria-controls="nav-page-select-listbox"
|
|
809
|
+
aria-labelledby="nav-page-select-trigger"
|
|
810
|
+
@input=${(e: Event) => {
|
|
811
|
+
this._pageSearchQuery = (
|
|
812
|
+
e.target as HTMLInputElement
|
|
813
|
+
).value;
|
|
814
|
+
}}
|
|
815
|
+
/>
|
|
816
|
+
</header>
|
|
817
|
+
<div
|
|
818
|
+
role="listbox"
|
|
819
|
+
id="nav-page-select-listbox"
|
|
820
|
+
aria-orientation="vertical"
|
|
821
|
+
aria-labelledby="nav-page-select-trigger"
|
|
822
|
+
data-empty=${this.labels.noPagesFound}
|
|
823
|
+
>
|
|
824
|
+
${filteredPages.length > 0
|
|
825
|
+
? html`<div class="max-h-64 overflow-y-auto scrollbar">
|
|
826
|
+
${filteredPages.map(
|
|
827
|
+
(page) => html`
|
|
828
|
+
<div
|
|
829
|
+
role="option"
|
|
830
|
+
data-value=${page.id}
|
|
831
|
+
@click=${() => {
|
|
832
|
+
this._showPagePicker = false;
|
|
833
|
+
this._pageSearchQuery = "";
|
|
834
|
+
document.removeEventListener(
|
|
835
|
+
"click",
|
|
836
|
+
this.#closePagePicker,
|
|
837
|
+
);
|
|
838
|
+
this.#handleAddPage(page);
|
|
839
|
+
}}
|
|
840
|
+
>
|
|
841
|
+
${page.title || page.slug}
|
|
842
|
+
</div>
|
|
843
|
+
`,
|
|
844
|
+
)}
|
|
845
|
+
</div>`
|
|
846
|
+
: html`<div
|
|
847
|
+
class="py-6 text-center text-sm text-muted-foreground"
|
|
848
|
+
>
|
|
849
|
+
${this._availablePages.length === 0
|
|
850
|
+
? this.labels.allPagesInNav
|
|
851
|
+
: this.labels.noPagesFound}
|
|
852
|
+
</div>`}
|
|
853
|
+
</div>
|
|
854
|
+
</div>
|
|
855
|
+
`
|
|
856
|
+
: nothing}
|
|
857
|
+
</div>
|
|
858
|
+
</section>
|
|
859
|
+
`;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
#renderAddLinkSection() {
|
|
863
|
+
return html`
|
|
864
|
+
<section class="mt-8">
|
|
865
|
+
<h2 class="text-lg font-semibold mb-3">
|
|
866
|
+
${this.labels.addCustomLinkToNavigation}
|
|
867
|
+
</h2>
|
|
868
|
+
<div id="nav-link-popover" class="popover">
|
|
869
|
+
<button
|
|
870
|
+
id="nav-link-popover-trigger"
|
|
871
|
+
type="button"
|
|
872
|
+
aria-expanded=${this._showLinkForm}
|
|
873
|
+
aria-controls="nav-link-popover-content"
|
|
874
|
+
class="btn-outline"
|
|
875
|
+
@click=${(e: Event) => {
|
|
876
|
+
e.stopPropagation();
|
|
877
|
+
this._showLinkForm = !this._showLinkForm;
|
|
878
|
+
if (this._showLinkForm) {
|
|
879
|
+
setTimeout(() => {
|
|
880
|
+
document.addEventListener("click", this.#closeLinkForm);
|
|
881
|
+
});
|
|
882
|
+
} else {
|
|
883
|
+
document.removeEventListener("click", this.#closeLinkForm);
|
|
884
|
+
}
|
|
885
|
+
}}
|
|
886
|
+
>
|
|
887
|
+
${this.labels.addLink}
|
|
888
|
+
</button>
|
|
889
|
+
${this._showLinkForm
|
|
890
|
+
? html`
|
|
891
|
+
<div
|
|
892
|
+
id="nav-link-popover-content"
|
|
893
|
+
data-popover
|
|
894
|
+
data-side="top"
|
|
895
|
+
aria-hidden="false"
|
|
896
|
+
class="w-80"
|
|
897
|
+
style="bottom: 100%; margin-bottom: 0.5rem;"
|
|
898
|
+
@click=${(e: Event) => e.stopPropagation()}
|
|
899
|
+
>
|
|
900
|
+
<div class="grid gap-4">
|
|
901
|
+
<header class="grid gap-1.5">
|
|
902
|
+
<h4 class="leading-none font-medium">
|
|
903
|
+
${this.labels.addLink}
|
|
904
|
+
</h4>
|
|
905
|
+
<p class="text-muted-foreground text-sm">
|
|
906
|
+
${this.labels.addLinkDescription}
|
|
907
|
+
</p>
|
|
908
|
+
</header>
|
|
909
|
+
<form
|
|
910
|
+
class="form grid gap-2"
|
|
911
|
+
@submit=${(e: Event) => {
|
|
912
|
+
e.preventDefault();
|
|
913
|
+
this.#handleAddLink();
|
|
914
|
+
}}
|
|
915
|
+
>
|
|
916
|
+
<div class="grid grid-cols-3 items-center gap-4">
|
|
917
|
+
<label for="nav-link-label">${this.labels.label}</label>
|
|
918
|
+
<input
|
|
919
|
+
type="text"
|
|
920
|
+
id="nav-link-label"
|
|
921
|
+
class="col-span-2 h-8"
|
|
922
|
+
placeholder="Home"
|
|
923
|
+
required
|
|
924
|
+
.value=${this._newLinkLabel}
|
|
925
|
+
@input=${(e: Event) => {
|
|
926
|
+
this._newLinkLabel = (
|
|
927
|
+
e.target as HTMLInputElement
|
|
928
|
+
).value;
|
|
929
|
+
}}
|
|
930
|
+
autofocus
|
|
931
|
+
/>
|
|
932
|
+
</div>
|
|
933
|
+
<div class="grid grid-cols-3 items-center gap-4">
|
|
934
|
+
<label for="nav-link-url">${this.labels.url}</label>
|
|
935
|
+
<input
|
|
936
|
+
type="text"
|
|
937
|
+
id="nav-link-url"
|
|
938
|
+
class="col-span-2 h-8"
|
|
939
|
+
placeholder=${this.labels.urlPlaceholder}
|
|
940
|
+
required
|
|
941
|
+
.value=${this._newLinkUrl}
|
|
942
|
+
@input=${(e: Event) => {
|
|
943
|
+
this._newLinkUrl = (
|
|
944
|
+
e.target as HTMLInputElement
|
|
945
|
+
).value;
|
|
946
|
+
}}
|
|
947
|
+
/>
|
|
948
|
+
</div>
|
|
949
|
+
<button
|
|
950
|
+
type="submit"
|
|
951
|
+
class="btn-sm mt-2"
|
|
952
|
+
?disabled=${this._addingLink}
|
|
953
|
+
>
|
|
954
|
+
${this.labels.addLink}
|
|
955
|
+
</button>
|
|
956
|
+
</form>
|
|
957
|
+
</div>
|
|
958
|
+
</div>
|
|
959
|
+
`
|
|
960
|
+
: nothing}
|
|
961
|
+
</div>
|
|
962
|
+
</section>
|
|
963
|
+
`;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
#renderSystemToggles() {
|
|
967
|
+
if (!this.systemNavItems?.length) return nothing;
|
|
968
|
+
|
|
969
|
+
return html`
|
|
970
|
+
<section class="mt-8">
|
|
971
|
+
<h2 class="text-lg font-semibold mb-1">${this.labels.systemLinks}</h2>
|
|
972
|
+
<p class="text-sm text-muted-foreground mb-3">
|
|
973
|
+
${this.labels.systemLinksDescription}
|
|
974
|
+
</p>
|
|
975
|
+
<div class="flex flex-col divide-y">
|
|
976
|
+
${this.systemNavItems.map((config) => {
|
|
977
|
+
const enabled = this.#isSystemEnabled(config);
|
|
978
|
+
const toggling = this._togglingKeys.has(config.key);
|
|
979
|
+
return html`
|
|
980
|
+
<div class="flex items-center justify-between py-3">
|
|
981
|
+
<div>
|
|
982
|
+
<p class="font-medium">${config.defaultLabel}</p>
|
|
983
|
+
<p class="text-sm text-muted-foreground">
|
|
984
|
+
${config.description}
|
|
985
|
+
</p>
|
|
986
|
+
</div>
|
|
987
|
+
<input
|
|
988
|
+
type="checkbox"
|
|
989
|
+
role="switch"
|
|
990
|
+
class="input"
|
|
991
|
+
.checked=${enabled}
|
|
992
|
+
?disabled=${toggling}
|
|
993
|
+
@change=${(e: Event) => {
|
|
994
|
+
const checked = (e.target as HTMLInputElement).checked;
|
|
995
|
+
this.#handleSystemToggle(config, checked);
|
|
996
|
+
}}
|
|
997
|
+
/>
|
|
998
|
+
</div>
|
|
999
|
+
`;
|
|
1000
|
+
})}
|
|
1001
|
+
</div>
|
|
1002
|
+
</section>
|
|
1003
|
+
`;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
render() {
|
|
1007
|
+
return html`
|
|
1008
|
+
${this.#renderPreview()}
|
|
1009
|
+
|
|
1010
|
+
<div class="flex flex-col gap-3 mt-3">
|
|
1011
|
+
<div class="flex items-center gap-3">
|
|
1012
|
+
<label class="text-sm" for="nav-max-visible">
|
|
1013
|
+
${this.labels.maxVisibleLinks}
|
|
1014
|
+
</label>
|
|
1015
|
+
<input
|
|
1016
|
+
type="number"
|
|
1017
|
+
id="nav-max-visible"
|
|
1018
|
+
class="input w-16 h-8"
|
|
1019
|
+
min="0"
|
|
1020
|
+
max="5"
|
|
1021
|
+
.value=${String(this.maxVisible)}
|
|
1022
|
+
@change=${(e: Event) => {
|
|
1023
|
+
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
|
1024
|
+
if (!isNaN(val)) this.#handleMaxVisibleChange(val);
|
|
1025
|
+
}}
|
|
1026
|
+
/>
|
|
1027
|
+
</div>
|
|
1028
|
+
<div class="flex items-center gap-3">
|
|
1029
|
+
<label class="text-sm" for="nav-home-view">
|
|
1030
|
+
${this.labels.useFeaturedAsDefault}
|
|
1031
|
+
</label>
|
|
1032
|
+
<input
|
|
1033
|
+
type="checkbox"
|
|
1034
|
+
role="switch"
|
|
1035
|
+
id="nav-home-view"
|
|
1036
|
+
class="input"
|
|
1037
|
+
.checked=${this.homeDefaultView === "featured"}
|
|
1038
|
+
@change=${(e: Event) => {
|
|
1039
|
+
this.#handleHomeViewToggle(
|
|
1040
|
+
(e.target as HTMLInputElement).checked,
|
|
1041
|
+
);
|
|
1042
|
+
}}
|
|
1043
|
+
/>
|
|
1044
|
+
</div>
|
|
1045
|
+
</div>
|
|
1046
|
+
|
|
1047
|
+
<section class="mt-8">
|
|
1048
|
+
<h2 class="text-lg font-semibold mb-3">
|
|
1049
|
+
${this.labels.navigationItems}
|
|
1050
|
+
</h2>
|
|
1051
|
+
${this._items.length === 0
|
|
1052
|
+
? html`<p class="text-sm text-muted-foreground py-4">
|
|
1053
|
+
${this.labels.emptyState}
|
|
1054
|
+
</p>`
|
|
1055
|
+
: html`
|
|
1056
|
+
<div id="nav-items-list" class="flex flex-col divide-y">
|
|
1057
|
+
${this._items.map((item) => this.#renderItem(item))}
|
|
1058
|
+
</div>
|
|
1059
|
+
`}
|
|
1060
|
+
</section>
|
|
1061
|
+
|
|
1062
|
+
${this.#renderAddArea()} ${this.#renderSystemToggles()}
|
|
1063
|
+
`;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
customElements.define("jant-nav-manager", JantNavManager);
|