@farming-labs/nuxt-theme 0.0.3-beta.2 → 0.0.3-beta.3
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/package.json +2 -2
- package/src/components/DocsContent.vue +289 -10
- package/src/components/DocsLayout.vue +1 -0
- package/src/components/DocsPage.vue +4 -1
- package/src/components/FloatingAIChat.vue +2 -2
- package/src/components/SearchDialog.vue +341 -62
- package/src/components/TableOfContents.vue +98 -54
- package/src/themes/colorful.js +1 -1
- package/src/themes/default.js +1 -1
- package/styles/colorful.css +49 -3
- package/styles/darksharp.css +18 -0
- package/styles/docs.css +202 -12
- package/styles/greentree.css +237 -27
- package/styles/omni.css +292 -0
- package/styles/pixel-border-bundle.css +2 -2
- package/styles/pixel-border.css +108 -2
|
@@ -1,95 +1,374 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Omni command palette — same behavior as website/components/ui/omni-command-palette.tsx
|
|
4
|
+
* and Astro SearchDialog: recents when empty, /api/docs search, keyboard nav, click to navigate.
|
|
5
|
+
*/
|
|
6
|
+
import { ref, computed, watch, onMounted, nextTick } from "vue";
|
|
7
|
+
|
|
8
|
+
const STORAGE_KEY = "fd:omni:recents";
|
|
9
|
+
const MAX_RECENTS = 8;
|
|
10
|
+
const DEBOUNCE_MS = 150;
|
|
3
11
|
|
|
4
12
|
const emit = defineEmits<{ (e: "close"): void }>();
|
|
5
13
|
|
|
6
14
|
const query = ref("");
|
|
7
|
-
const
|
|
15
|
+
const currentResults = ref<{ content: string; url: string; description?: string }[]>([]);
|
|
8
16
|
const loading = ref(false);
|
|
17
|
+
const activeIndex = ref(0);
|
|
9
18
|
const inputEl = ref<HTMLInputElement | null>(null);
|
|
19
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
interface RecentEntry {
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
url: string;
|
|
25
|
+
}
|
|
12
26
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return;
|
|
27
|
+
function getRecents(): RecentEntry[] {
|
|
28
|
+
if (typeof localStorage === "undefined") return [];
|
|
29
|
+
try {
|
|
30
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
31
|
+
return raw ? JSON.parse(raw) : [];
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveRecent(entry: RecentEntry) {
|
|
38
|
+
try {
|
|
39
|
+
const recents = getRecents();
|
|
40
|
+
const next = [entry, ...recents.filter((r) => r.id !== entry.id)].slice(0, MAX_RECENTS);
|
|
41
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const recentsList = ref<RecentEntry[]>([]);
|
|
46
|
+
|
|
47
|
+
const allItems = computed(() => {
|
|
48
|
+
const q = query.value.trim();
|
|
49
|
+
if (q && currentResults.value.length) return currentResults.value.map((r) => ({ id: r.url, label: r.content, url: r.url, subtitle: r.description ?? "Page" }));
|
|
50
|
+
return recentsList.value.map((r) => ({ id: r.id, label: r.label, url: r.url, subtitle: "Recently viewed" }));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const showRecents = computed(() => !query.value.trim());
|
|
54
|
+
const showDocs = computed(() => !!query.value.trim() && currentResults.value.length > 0);
|
|
55
|
+
const showEmpty = computed(() => {
|
|
56
|
+
if (query.value.trim()) return currentResults.value.length === 0 && !loading.value;
|
|
57
|
+
return recentsList.value.length === 0;
|
|
58
|
+
});
|
|
59
|
+
const emptyText = computed(() =>
|
|
60
|
+
query.value.trim() ? "No results found. Try a different query." : "Type to search the docs, or browse recent items.",
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
function loadRecents() {
|
|
64
|
+
recentsList.value = getRecents();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function close() {
|
|
68
|
+
if (typeof document !== "undefined") document.body.style.overflow = "";
|
|
69
|
+
emit("close");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function executeItem(item: { url: string; label?: string; content?: string }) {
|
|
73
|
+
const label = item.label ?? item.content ?? item.url;
|
|
74
|
+
saveRecent({ id: item.url, label, url: item.url });
|
|
75
|
+
if (item.url.startsWith("http")) {
|
|
76
|
+
window.open(item.url, "_blank", "noopener,noreferrer");
|
|
77
|
+
} else {
|
|
78
|
+
navigateTo(item.url);
|
|
18
79
|
}
|
|
19
|
-
|
|
80
|
+
close();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function moveActive(delta: number) {
|
|
84
|
+
const items = allItems.value;
|
|
85
|
+
if (!items.length) return;
|
|
86
|
+
activeIndex.value = activeIndex.value + delta;
|
|
87
|
+
if (activeIndex.value < 0) activeIndex.value = items.length - 1;
|
|
88
|
+
if (activeIndex.value >= items.length) activeIndex.value = 0;
|
|
89
|
+
scrollActiveIntoView();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function executeActive() {
|
|
93
|
+
const items = allItems.value;
|
|
94
|
+
const item = items[activeIndex.value];
|
|
95
|
+
if (item) executeItem(item);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function scrollActiveIntoView() {
|
|
99
|
+
nextTick(() => {
|
|
100
|
+
const listbox = document.getElementById("fd-omni-listbox");
|
|
101
|
+
if (!listbox) return;
|
|
102
|
+
const q = query.value.trim();
|
|
103
|
+
const container = q ? document.getElementById("fd-omni-docs-items") : document.getElementById("fd-omni-recent-items");
|
|
104
|
+
if (!container) return;
|
|
105
|
+
const items = container.querySelectorAll(".omni-item[data-url]");
|
|
106
|
+
if (items[activeIndex.value]) (items[activeIndex.value] as HTMLElement).scrollIntoView({ block: "nearest" });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function onInput() {
|
|
111
|
+
loadRecents();
|
|
112
|
+
const q = query.value.trim();
|
|
113
|
+
loading.value = false;
|
|
114
|
+
currentResults.value = [];
|
|
115
|
+
activeIndex.value = 0;
|
|
116
|
+
if (!q) return;
|
|
117
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
20
118
|
debounceTimer = setTimeout(async () => {
|
|
119
|
+
loading.value = true;
|
|
21
120
|
try {
|
|
22
121
|
const res = await fetch(`/api/docs?query=${encodeURIComponent(q)}`);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
122
|
+
const data = res.ok ? await res.json() : [];
|
|
123
|
+
currentResults.value = Array.isArray(data) ? data : [];
|
|
124
|
+
activeIndex.value = 0;
|
|
27
125
|
} catch {
|
|
28
|
-
|
|
126
|
+
currentResults.value = [];
|
|
29
127
|
} finally {
|
|
30
128
|
loading.value = false;
|
|
31
129
|
}
|
|
32
|
-
},
|
|
33
|
-
}
|
|
130
|
+
}, DEBOUNCE_MS);
|
|
131
|
+
}
|
|
34
132
|
|
|
35
|
-
|
|
36
|
-
inputEl.value?.focus();
|
|
37
|
-
});
|
|
133
|
+
watch(query, onInput);
|
|
38
134
|
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
135
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
136
|
+
if (e.key === "Escape") {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
close();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (e.key === "ArrowDown") {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
moveActive(1);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (e.key === "ArrowUp") {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
moveActive(-1);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (e.key === "Enter") {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
executeActive();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function onOverlayClick() {
|
|
158
|
+
close();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function onContentClick(e: MouseEvent) {
|
|
162
|
+
e.stopPropagation();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function onRowClick(item: { url: string; label?: string; content?: string }) {
|
|
166
|
+
executeItem(item);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function onExternalClick(e: Event, url: string) {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
e.stopPropagation();
|
|
172
|
+
try {
|
|
173
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
174
|
+
} catch {
|
|
44
175
|
window.location.href = url;
|
|
45
176
|
}
|
|
46
177
|
}
|
|
47
178
|
|
|
48
|
-
function
|
|
49
|
-
|
|
179
|
+
function onRowMouseEnter(container: "recent" | "docs", index: number) {
|
|
180
|
+
activeIndex.value = index;
|
|
50
181
|
}
|
|
182
|
+
|
|
183
|
+
onMounted(() => {
|
|
184
|
+
loadRecents();
|
|
185
|
+
if (typeof document !== "undefined") document.body.style.overflow = "hidden";
|
|
186
|
+
nextTick(() => {
|
|
187
|
+
inputEl.value?.focus();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
51
190
|
</script>
|
|
52
191
|
|
|
53
192
|
<template>
|
|
54
|
-
<div class="
|
|
55
|
-
<div
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
193
|
+
<div class="omni-overlay" aria-hidden="true" @click="onOverlayClick" @keydown="handleKeydown">
|
|
194
|
+
<div
|
|
195
|
+
class="omni-content"
|
|
196
|
+
role="dialog"
|
|
197
|
+
aria-label="Search documentation"
|
|
198
|
+
@click="onContentClick"
|
|
199
|
+
>
|
|
200
|
+
<div class="omni-header">
|
|
201
|
+
<div class="omni-search-row">
|
|
202
|
+
<span class="omni-search-icon" aria-hidden="true">
|
|
203
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
204
|
+
<circle cx="11" cy="11" r="8" />
|
|
205
|
+
<path d="m21 21-4.3-4.3" />
|
|
206
|
+
</svg>
|
|
207
|
+
</span>
|
|
208
|
+
<input
|
|
209
|
+
ref="inputEl"
|
|
210
|
+
v-model="query"
|
|
211
|
+
type="text"
|
|
212
|
+
class="omni-search-input"
|
|
213
|
+
role="combobox"
|
|
214
|
+
aria-expanded="true"
|
|
215
|
+
aria-controls="fd-omni-listbox"
|
|
216
|
+
placeholder="Search documentation…"
|
|
217
|
+
autocomplete="off"
|
|
218
|
+
@keydown="handleKeydown"
|
|
219
|
+
/>
|
|
220
|
+
<kbd class="omni-kbd">⌘K</kbd>
|
|
221
|
+
<button type="button" aria-label="Close" class="omni-close-btn" @click="close">
|
|
222
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
223
|
+
<path d="M18 6 6 18" />
|
|
224
|
+
<path d="m6 6 12 12" />
|
|
225
|
+
</svg>
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div id="fd-omni-listbox" class="omni-body" role="listbox" aria-label="Search results">
|
|
231
|
+
<div v-if="loading" class="omni-loading">
|
|
232
|
+
<svg class="omni-spin" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
233
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
234
|
+
</svg>
|
|
235
|
+
Searching…
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div v-else-if="showRecents && recentsList.length" id="fd-omni-recent-group" class="omni-group">
|
|
239
|
+
<div class="omni-group-label">Recent</div>
|
|
240
|
+
<div id="fd-omni-recent-items" class="omni-group-items">
|
|
241
|
+
<div
|
|
242
|
+
v-for="(r, i) in recentsList"
|
|
243
|
+
:key="r.id"
|
|
244
|
+
class="omni-item"
|
|
245
|
+
:class="{ 'omni-item-active': showRecents && i === activeIndex }"
|
|
246
|
+
:data-url="r.url"
|
|
247
|
+
role="option"
|
|
248
|
+
:aria-selected="showRecents && i === activeIndex"
|
|
249
|
+
tabindex="-1"
|
|
250
|
+
@click="onRowClick(r)"
|
|
251
|
+
@mouseenter="onRowMouseEnter('recent', i)"
|
|
252
|
+
>
|
|
253
|
+
<div class="omni-item-icon">
|
|
254
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
255
|
+
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
|
|
256
|
+
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
|
|
257
|
+
</svg>
|
|
258
|
+
</div>
|
|
259
|
+
<div class="omni-item-text">
|
|
260
|
+
<div class="omni-item-label">{{ r.label }}</div>
|
|
261
|
+
<div class="omni-item-subtitle">Recently viewed</div>
|
|
262
|
+
</div>
|
|
263
|
+
<a
|
|
264
|
+
:href="r.url"
|
|
265
|
+
class="omni-item-ext"
|
|
266
|
+
title="Open in new tab"
|
|
267
|
+
target="_blank"
|
|
268
|
+
rel="noopener noreferrer"
|
|
269
|
+
@click.prevent="onExternalClick($event, r.url)"
|
|
270
|
+
>
|
|
271
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
272
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
273
|
+
<polyline points="15 3 21 3 21 9" />
|
|
274
|
+
<line x1="10" y1="14" x2="21" y2="3" />
|
|
275
|
+
</svg>
|
|
276
|
+
</a>
|
|
277
|
+
<span class="omni-item-chevron" aria-hidden="true">
|
|
278
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
279
|
+
<polyline points="9 18 15 12 9 6" />
|
|
280
|
+
</svg>
|
|
281
|
+
</span>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<div v-if="showDocs" id="fd-omni-docs-group" class="omni-group">
|
|
287
|
+
<div class="omni-group-label">Documentation</div>
|
|
288
|
+
<div id="fd-omni-docs-items" class="omni-group-items">
|
|
289
|
+
<div
|
|
290
|
+
v-for="(r, i) in currentResults"
|
|
291
|
+
:key="r.url"
|
|
292
|
+
class="omni-item"
|
|
293
|
+
:class="{ 'omni-item-active': showDocs && i === activeIndex }"
|
|
294
|
+
:data-url="r.url"
|
|
295
|
+
role="option"
|
|
296
|
+
:aria-selected="showDocs && i === activeIndex"
|
|
297
|
+
tabindex="-1"
|
|
298
|
+
@click="onRowClick(r)"
|
|
299
|
+
@mouseenter="onRowMouseEnter('docs', i)"
|
|
300
|
+
>
|
|
301
|
+
<div class="omni-item-icon">
|
|
302
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
303
|
+
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
|
|
304
|
+
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
|
|
305
|
+
</svg>
|
|
306
|
+
</div>
|
|
307
|
+
<div class="omni-item-text">
|
|
308
|
+
<div class="omni-item-label">{{ r.content }}</div>
|
|
309
|
+
<div class="omni-item-subtitle">{{ r.description ?? "Page" }}</div>
|
|
310
|
+
</div>
|
|
311
|
+
<a
|
|
312
|
+
:href="r.url"
|
|
313
|
+
class="omni-item-ext"
|
|
314
|
+
title="Open in new tab"
|
|
315
|
+
target="_blank"
|
|
316
|
+
rel="noopener noreferrer"
|
|
317
|
+
@click.prevent="onExternalClick($event, r.url)"
|
|
318
|
+
>
|
|
319
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
320
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
321
|
+
<polyline points="15 3 21 3 21 9" />
|
|
322
|
+
<line x1="10" y1="14" x2="21" y2="3" />
|
|
323
|
+
</svg>
|
|
324
|
+
</a>
|
|
325
|
+
<span class="omni-item-chevron" aria-hidden="true">
|
|
326
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
327
|
+
<polyline points="9 18 15 12 9 6" />
|
|
328
|
+
</svg>
|
|
329
|
+
</span>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<div v-if="showEmpty" class="omni-empty">
|
|
335
|
+
<div class="omni-empty-icon">
|
|
336
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
337
|
+
<circle cx="12" cy="12" r="10" />
|
|
338
|
+
<path d="M12 6v6l4 2" />
|
|
339
|
+
</svg>
|
|
340
|
+
</div>
|
|
341
|
+
<span>{{ emptyText }}</span>
|
|
342
|
+
</div>
|
|
76
343
|
</div>
|
|
77
344
|
|
|
78
|
-
<div class="
|
|
79
|
-
<div
|
|
80
|
-
|
|
81
|
-
|
|
345
|
+
<div class="omni-footer">
|
|
346
|
+
<div class="omni-footer-inner">
|
|
347
|
+
<div class="omni-footer-hints">
|
|
348
|
+
<span class="omni-footer-hint">
|
|
349
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
350
|
+
<polyline points="9 18 15 12 9 6" />
|
|
351
|
+
</svg>
|
|
352
|
+
to select
|
|
353
|
+
</span>
|
|
354
|
+
<span class="omni-footer-hint">
|
|
355
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
356
|
+
<path d="M18 15l-6-6-6 6" />
|
|
357
|
+
</svg>
|
|
358
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
359
|
+
<path d="M6 9l6 6 6-6" />
|
|
360
|
+
</svg>
|
|
361
|
+
to navigate
|
|
362
|
+
</span>
|
|
363
|
+
<span class="omni-footer-hint omni-footer-hint-desktop">
|
|
364
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
365
|
+
<path d="M18 6 6 18" />
|
|
366
|
+
<path d="m6 6 12 12" />
|
|
367
|
+
</svg>
|
|
368
|
+
to close
|
|
369
|
+
</span>
|
|
370
|
+
</div>
|
|
82
371
|
</div>
|
|
83
|
-
<button
|
|
84
|
-
v-for="result in results"
|
|
85
|
-
v-else
|
|
86
|
-
:key="result.url ?? result.content"
|
|
87
|
-
class="fd-search-result"
|
|
88
|
-
@click="result.url && navigate(result.url)"
|
|
89
|
-
>
|
|
90
|
-
<span class="fd-search-result-title">{{ result.content }}</span>
|
|
91
|
-
<span v-if="result.url" class="fd-search-result-url">{{ result.url }}</span>
|
|
92
|
-
</button>
|
|
93
372
|
</div>
|
|
94
373
|
</div>
|
|
95
374
|
</div>
|
|
@@ -22,7 +22,38 @@ const thumbTop = ref(0);
|
|
|
22
22
|
const thumbHeight = ref(0);
|
|
23
23
|
const svgPath = ref("");
|
|
24
24
|
|
|
25
|
+
const ACTIVE_ZONE_TOP = 120; // px from top of viewport — heading "active" when near this line
|
|
26
|
+
|
|
25
27
|
let observer: IntersectionObserver | null = null;
|
|
28
|
+
let scrollRafId = 0;
|
|
29
|
+
|
|
30
|
+
function getActiveIdFromScroll(): Set<string> {
|
|
31
|
+
const ids = items.value.map((item) => item.url.slice(1));
|
|
32
|
+
let bestId: string | null = null;
|
|
33
|
+
let bestDistance = Infinity;
|
|
34
|
+
for (const id of ids) {
|
|
35
|
+
const el = document.getElementById(id);
|
|
36
|
+
if (!el) continue;
|
|
37
|
+
const rect = el.getBoundingClientRect();
|
|
38
|
+
const mid = rect.top + rect.height / 2;
|
|
39
|
+
const distance = Math.abs(mid - ACTIVE_ZONE_TOP);
|
|
40
|
+
if (distance < bestDistance) {
|
|
41
|
+
bestDistance = distance;
|
|
42
|
+
bestId = id;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return bestId ? new Set([bestId]) : new Set();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function updateActiveFromScroll() {
|
|
49
|
+
scrollRafId = 0;
|
|
50
|
+
activeIds.value = getActiveIdFromScroll();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function scheduleActiveUpdate() {
|
|
54
|
+
if (scrollRafId !== 0) return;
|
|
55
|
+
scrollRafId = requestAnimationFrame(updateActiveFromScroll);
|
|
56
|
+
}
|
|
26
57
|
|
|
27
58
|
function getItemOffset(depth: number): number {
|
|
28
59
|
if (depth <= 2) return 14;
|
|
@@ -155,41 +186,49 @@ function maskSvgUrl(): string {
|
|
|
155
186
|
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
|
|
156
187
|
}
|
|
157
188
|
|
|
189
|
+
function onScroll() {
|
|
190
|
+
scheduleActiveUpdate();
|
|
191
|
+
}
|
|
192
|
+
|
|
158
193
|
onMounted(() => {
|
|
159
194
|
observer = new IntersectionObserver(
|
|
160
|
-
(
|
|
161
|
-
|
|
162
|
-
for (const entry of entries) {
|
|
163
|
-
if (entry.isIntersecting) {
|
|
164
|
-
next.add(entry.target.id);
|
|
165
|
-
} else {
|
|
166
|
-
next.delete(entry.target.id);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
activeIds.value = next;
|
|
195
|
+
() => {
|
|
196
|
+
scheduleActiveUpdate();
|
|
170
197
|
},
|
|
171
|
-
{ rootMargin: "-80px 0px -
|
|
198
|
+
{ rootMargin: "-80px 0px -60% 0px" },
|
|
172
199
|
);
|
|
173
200
|
observeHeadings();
|
|
201
|
+
window.addEventListener("scroll", onScroll, { passive: true, capture: true });
|
|
202
|
+
document.addEventListener("scroll", onScroll, { passive: true, capture: true });
|
|
174
203
|
|
|
175
|
-
|
|
176
|
-
|
|
204
|
+
nextTick(() => {
|
|
205
|
+
activeIds.value = getActiveIdFromScroll();
|
|
206
|
+
if (isDirectional.value) {
|
|
177
207
|
buildSvgPath();
|
|
178
208
|
calcThumb();
|
|
179
|
-
}
|
|
180
|
-
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// Run again after content has finished rendering (e.g. after async slot)
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
activeIds.value = getActiveIdFromScroll();
|
|
214
|
+
if (isDirectional.value) {
|
|
215
|
+
buildSvgPath();
|
|
216
|
+
calcThumb();
|
|
217
|
+
}
|
|
218
|
+
}, 150);
|
|
181
219
|
});
|
|
182
220
|
|
|
183
221
|
watch(
|
|
184
222
|
items,
|
|
185
223
|
() => {
|
|
186
224
|
observeHeadings();
|
|
187
|
-
|
|
188
|
-
|
|
225
|
+
nextTick(() => {
|
|
226
|
+
activeIds.value = getActiveIdFromScroll();
|
|
227
|
+
if (isDirectional.value) {
|
|
189
228
|
buildSvgPath();
|
|
190
229
|
calcThumb();
|
|
191
|
-
}
|
|
192
|
-
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
193
232
|
},
|
|
194
233
|
{ flush: "post" },
|
|
195
234
|
);
|
|
@@ -214,6 +253,10 @@ watch(isDirectional, (val) => {
|
|
|
214
253
|
});
|
|
215
254
|
|
|
216
255
|
onUnmounted(() => {
|
|
256
|
+
if (scrollRafId) cancelAnimationFrame(scrollRafId);
|
|
257
|
+
scrollRafId = 0;
|
|
258
|
+
window.removeEventListener("scroll", onScroll, { capture: true });
|
|
259
|
+
document.removeEventListener("scroll", onScroll, { capture: true });
|
|
217
260
|
observer?.disconnect();
|
|
218
261
|
});
|
|
219
262
|
</script>
|
|
@@ -252,41 +295,42 @@ onUnmounted(() => {
|
|
|
252
295
|
</li>
|
|
253
296
|
</ul>
|
|
254
297
|
|
|
255
|
-
<!-- Clerk / directional style -->
|
|
256
|
-
<
|
|
257
|
-
<
|
|
258
|
-
<
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
<!-- Vertical line segment -->
|
|
265
|
-
<div :style="verticalLineStyle(item, index)" />
|
|
266
|
-
|
|
267
|
-
<!-- Diagonal SVG connector when depth changes -->
|
|
268
|
-
<svg
|
|
269
|
-
v-if="hasDiagonal(index)"
|
|
270
|
-
viewBox="0 0 16 16"
|
|
271
|
-
width="16"
|
|
272
|
-
height="16"
|
|
273
|
-
style="position: absolute; top: -6px; left: 0"
|
|
298
|
+
<!-- Clerk / directional style: wrapper for valid HTML (ul may not contain div) -->
|
|
299
|
+
<div v-else class="fd-toc-clerk-wrap" style="position: relative">
|
|
300
|
+
<ul ref="listRef" class="fd-toc-list fd-toc-clerk">
|
|
301
|
+
<li v-for="(item, index) in items" :key="item.url" class="fd-toc-item">
|
|
302
|
+
<a
|
|
303
|
+
:href="item.url"
|
|
304
|
+
class="fd-toc-link fd-toc-clerk-link"
|
|
305
|
+
:style="clerkLinkStyle(item)"
|
|
306
|
+
:data-active="isActive(item) || undefined"
|
|
274
307
|
>
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
308
|
+
<!-- Vertical line segment -->
|
|
309
|
+
<div :style="verticalLineStyle(item, index)" />
|
|
310
|
+
|
|
311
|
+
<!-- Diagonal SVG connector when depth changes -->
|
|
312
|
+
<svg
|
|
313
|
+
v-if="hasDiagonal(index)"
|
|
314
|
+
viewBox="0 0 16 16"
|
|
315
|
+
width="16"
|
|
316
|
+
height="16"
|
|
317
|
+
style="position: absolute; top: -6px; left: 0"
|
|
318
|
+
>
|
|
319
|
+
<line
|
|
320
|
+
:x1="diagonalSvg(index).upperOffset"
|
|
321
|
+
y1="0"
|
|
322
|
+
:x2="diagonalSvg(index).currentOffset"
|
|
323
|
+
y2="12"
|
|
324
|
+
stroke="hsla(0, 0%, 50%, 0.1)"
|
|
325
|
+
stroke-width="1"
|
|
326
|
+
/>
|
|
327
|
+
</svg>
|
|
328
|
+
|
|
329
|
+
{{ item.title }}
|
|
330
|
+
</a>
|
|
331
|
+
</li>
|
|
332
|
+
</ul>
|
|
333
|
+
<!-- Mask container with thumb for active highlight (sibling to ul for valid HTML) -->
|
|
290
334
|
<div
|
|
291
335
|
v-if="svgPath"
|
|
292
336
|
class="fd-toc-clerk-mask"
|
|
@@ -313,6 +357,6 @@ onUnmounted(() => {
|
|
|
313
357
|
}"
|
|
314
358
|
/>
|
|
315
359
|
</div>
|
|
316
|
-
</
|
|
360
|
+
</div>
|
|
317
361
|
</div>
|
|
318
362
|
</template>
|
package/src/themes/colorful.js
CHANGED
|
@@ -24,7 +24,7 @@ const ColorfulUIDefaults = {
|
|
|
24
24
|
layout: {
|
|
25
25
|
contentWidth: 768,
|
|
26
26
|
sidebarWidth: 260,
|
|
27
|
-
toc: { enabled: true, depth: 3, style: "
|
|
27
|
+
toc: { enabled: true, depth: 3, style: "default" },
|
|
28
28
|
header: { height: 56, sticky: true },
|
|
29
29
|
},
|
|
30
30
|
components: {
|
package/src/themes/default.js
CHANGED