@farming-labs/svelte-theme 0.0.3-beta.2 → 0.0.3-beta.4
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 +3 -3
- package/src/components/DocsContent.svelte +288 -6
- package/src/components/DocsLayout.svelte +2 -6
- package/src/components/FloatingAIChat.svelte +2 -2
- package/src/components/SearchDialog.svelte +354 -55
- package/src/components/TableOfContents.svelte +83 -34
- package/src/themes/colorful.js +1 -1
- package/styles/colorful.css +72 -0
- package/styles/darksharp.css +71 -0
- package/styles/docs.css +197 -11
- package/styles/greentree.css +307 -39
- package/styles/omni.css +362 -0
- package/styles/pixel-border.css +228 -1
|
@@ -1,91 +1,390 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Omni command palette — pixel-perfect default for SvelteKit, aligned with
|
|
4
|
+
* website/components/ui/omni-command-palette.tsx. Recents when empty,
|
|
5
|
+
* /api/docs search, keyboard nav, same copy and structure.
|
|
5
6
|
*/
|
|
6
|
-
import { onMount } from "svelte";
|
|
7
|
+
import { onMount, onDestroy, tick } from "svelte";
|
|
7
8
|
import { goto } from "$app/navigation";
|
|
8
9
|
|
|
10
|
+
const STORAGE_KEY = "fd:omni:recents";
|
|
11
|
+
const MAX_RECENTS = 8;
|
|
12
|
+
const DEBOUNCE_MS = 120;
|
|
13
|
+
const PLACEHOLDER = "Search documentation…";
|
|
14
|
+
|
|
9
15
|
let { onclose } = $props();
|
|
16
|
+
|
|
10
17
|
let query = $state("");
|
|
11
|
-
let
|
|
18
|
+
let currentResults = $state([]);
|
|
12
19
|
let loading = $state(false);
|
|
13
|
-
let
|
|
14
|
-
let
|
|
20
|
+
let activeId = $state(null);
|
|
21
|
+
let recentsList = $state([]);
|
|
22
|
+
let inputEl = $state(null);
|
|
23
|
+
let listRef = $state(null);
|
|
24
|
+
let debounceTimer = null;
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
let flatItems = $derived.by(() => {
|
|
27
|
+
const q = query.trim();
|
|
28
|
+
if (q && currentResults.length) {
|
|
29
|
+
return currentResults.map((r) => ({
|
|
30
|
+
id: r.url,
|
|
31
|
+
label: r.content,
|
|
32
|
+
url: r.url,
|
|
33
|
+
subtitle: r.description ?? "Page",
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
return recentsList.map((r) => ({
|
|
37
|
+
id: r.id,
|
|
38
|
+
label: r.label,
|
|
39
|
+
url: r.url,
|
|
40
|
+
subtitle: "Recently used",
|
|
41
|
+
}));
|
|
18
42
|
});
|
|
19
43
|
|
|
20
|
-
$
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
44
|
+
let showRecents = $derived(!query.trim());
|
|
45
|
+
let showDocs = $derived(!!query.trim() && currentResults.length > 0);
|
|
46
|
+
let showEmpty = $derived(
|
|
47
|
+
query.trim() ? currentResults.length === 0 && !loading : recentsList.length === 0
|
|
48
|
+
);
|
|
49
|
+
let emptyText = $derived(
|
|
50
|
+
query.trim()
|
|
51
|
+
? "No results found. Try a different query."
|
|
52
|
+
: "Type to search the docs, or browse recent items."
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
function getRecents() {
|
|
56
|
+
if (typeof document === "undefined" || typeof localStorage === "undefined") return [];
|
|
57
|
+
try {
|
|
58
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
59
|
+
return raw ? JSON.parse(raw) : [];
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function saveRecent(entry) {
|
|
66
|
+
if (typeof localStorage === "undefined") return;
|
|
67
|
+
try {
|
|
68
|
+
const recents = getRecents();
|
|
69
|
+
const next = [entry, ...recents.filter((r) => r.id !== entry.id)].slice(0, MAX_RECENTS);
|
|
70
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function loadRecents() {
|
|
75
|
+
recentsList = getRecents();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function close() {
|
|
79
|
+
if (typeof document !== "undefined") document.body.style.overflow = "";
|
|
80
|
+
onclose?.();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function executeItem(item) {
|
|
84
|
+
saveRecent({ id: item.url, label: item.label ?? item.content ?? item.url, url: item.url });
|
|
85
|
+
if (item.url.startsWith("http")) {
|
|
86
|
+
if (typeof window !== "undefined") window.open(item.url, "_blank", "noopener,noreferrer");
|
|
87
|
+
} else {
|
|
88
|
+
goto(item.url);
|
|
25
89
|
}
|
|
26
|
-
|
|
90
|
+
close();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function moveActive(delta) {
|
|
94
|
+
if (!flatItems.length) return;
|
|
95
|
+
const idx = flatItems.findIndex((i) => i.id === activeId);
|
|
96
|
+
const nextIdx = idx < 0 ? 0 : (((idx + delta) % flatItems.length) + flatItems.length) % flatItems.length;
|
|
97
|
+
activeId = flatItems[nextIdx].id;
|
|
98
|
+
scrollActiveIntoView();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function executeActive() {
|
|
102
|
+
const idx = activeId != null ? flatItems.findIndex((i) => i.id === activeId) : 0;
|
|
103
|
+
const item = flatItems[idx ?? 0];
|
|
104
|
+
if (item) executeItem(item);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function scrollActiveIntoView() {
|
|
108
|
+
tick().then(() => {
|
|
109
|
+
if (typeof document === "undefined" || !listRef) return;
|
|
110
|
+
const node = listRef.querySelector(`[data-id="${activeId}"]`);
|
|
111
|
+
node?.scrollIntoView({ block: "nearest" });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function onInput() {
|
|
116
|
+
loadRecents();
|
|
117
|
+
loading = false;
|
|
118
|
+
currentResults = [];
|
|
119
|
+
activeId = null;
|
|
120
|
+
const q = query.trim();
|
|
121
|
+
if (!q) return;
|
|
122
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
27
123
|
debounceTimer = setTimeout(async () => {
|
|
124
|
+
loading = true;
|
|
28
125
|
try {
|
|
29
|
-
const res = await fetch(`/api/docs?query=${encodeURIComponent(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
126
|
+
const res = await fetch(`/api/docs?query=${encodeURIComponent(q)}`);
|
|
127
|
+
const data = res.ok ? await res.json() : [];
|
|
128
|
+
currentResults = Array.isArray(data) ? data : [];
|
|
129
|
+
activeId = currentResults[0]?.url ?? null;
|
|
34
130
|
} catch {
|
|
35
|
-
|
|
131
|
+
currentResults = [];
|
|
36
132
|
} finally {
|
|
37
133
|
loading = false;
|
|
38
134
|
}
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
function navigate(url) {
|
|
43
|
-
onclose?.();
|
|
44
|
-
goto(url);
|
|
135
|
+
}, DEBOUNCE_MS);
|
|
45
136
|
}
|
|
46
137
|
|
|
138
|
+
$effect(() => {
|
|
139
|
+
void query;
|
|
140
|
+
onInput();
|
|
141
|
+
});
|
|
142
|
+
|
|
47
143
|
function handleKeydown(e) {
|
|
48
144
|
if (e.key === "Escape") {
|
|
49
|
-
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
close();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (e.key === "ArrowDown") {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
moveActive(1);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (e.key === "ArrowUp") {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
moveActive(-1);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (e.key === "Enter") {
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
executeActive();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function onOverlayClick() {
|
|
166
|
+
close();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function onContentClick(e) {
|
|
170
|
+
e.stopPropagation();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function onExternalClick(e, url) {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
e.stopPropagation();
|
|
176
|
+
try {
|
|
177
|
+
if (typeof window !== "undefined") window.open(url, "_blank", "noopener,noreferrer");
|
|
178
|
+
} catch {
|
|
179
|
+
if (typeof window !== "undefined") window.location.href = url;
|
|
50
180
|
}
|
|
51
181
|
}
|
|
182
|
+
|
|
183
|
+
const FileIcon = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>`;
|
|
184
|
+
const ExternalIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`;
|
|
185
|
+
const ChevronIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>`;
|
|
186
|
+
const EmptyIcon = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>`;
|
|
187
|
+
|
|
188
|
+
onMount(() => {
|
|
189
|
+
loadRecents();
|
|
190
|
+
if (typeof document !== "undefined") document.body.style.overflow = "hidden";
|
|
191
|
+
tick().then(() => inputEl?.focus());
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
onDestroy(() => {
|
|
195
|
+
if (typeof document !== "undefined") document.body.style.overflow = "";
|
|
196
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
197
|
+
});
|
|
52
198
|
</script>
|
|
53
199
|
|
|
200
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
54
201
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
55
|
-
<div
|
|
202
|
+
<div
|
|
203
|
+
class="omni-overlay"
|
|
204
|
+
aria-hidden="true"
|
|
205
|
+
role="presentation"
|
|
206
|
+
onclick={onOverlayClick}
|
|
207
|
+
onkeydown={handleKeydown}
|
|
208
|
+
>
|
|
56
209
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
57
210
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
58
|
-
<div
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class="
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
211
|
+
<div
|
|
212
|
+
class="omni-content"
|
|
213
|
+
role="dialog"
|
|
214
|
+
aria-label="Command palette"
|
|
215
|
+
tabindex="-1"
|
|
216
|
+
onclick={onContentClick}
|
|
217
|
+
>
|
|
218
|
+
<div class="omni-header">
|
|
219
|
+
<div class="omni-search-row">
|
|
220
|
+
<span class="omni-search-icon" aria-hidden="true">
|
|
221
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
222
|
+
<circle cx="11" cy="11" r="8" />
|
|
223
|
+
<path d="m21 21-4.3-4.3" />
|
|
224
|
+
</svg>
|
|
225
|
+
</span>
|
|
226
|
+
<input
|
|
227
|
+
bind:this={inputEl}
|
|
228
|
+
bind:value={query}
|
|
229
|
+
type="text"
|
|
230
|
+
class="omni-search-input"
|
|
231
|
+
role="combobox"
|
|
232
|
+
aria-expanded="true"
|
|
233
|
+
aria-controls="omni-listbox"
|
|
234
|
+
aria-activedescendant={activeId ? `omni-item-${activeId}` : undefined}
|
|
235
|
+
placeholder={PLACEHOLDER}
|
|
236
|
+
autocomplete="off"
|
|
237
|
+
onkeydown={handleKeydown}
|
|
238
|
+
/>
|
|
239
|
+
<kbd class="omni-kbd">⌘K</kbd>
|
|
240
|
+
<button type="button" aria-label="Close" class="omni-close-btn" onclick={close}>
|
|
241
|
+
<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">
|
|
242
|
+
<path d="M18 6 6 18" />
|
|
243
|
+
<path d="m6 6 12 12" />
|
|
244
|
+
</svg>
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
72
247
|
</div>
|
|
73
248
|
|
|
74
|
-
<div
|
|
249
|
+
<div
|
|
250
|
+
bind:this={listRef}
|
|
251
|
+
id="omni-listbox"
|
|
252
|
+
class="omni-body"
|
|
253
|
+
role="listbox"
|
|
254
|
+
aria-label="Command results"
|
|
255
|
+
>
|
|
75
256
|
{#if loading}
|
|
76
|
-
<div class="
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
257
|
+
<div class="omni-loading">
|
|
258
|
+
<svg class="omni-spin" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
259
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
260
|
+
</svg>
|
|
261
|
+
Fetching results…
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
{:else if showRecents && recentsList.length > 0}
|
|
265
|
+
<div class="omni-group">
|
|
266
|
+
<div class="omni-group-label">Recent</div>
|
|
267
|
+
<div class="omni-group-items">
|
|
268
|
+
{#each flatItems as item, i}
|
|
269
|
+
{@const active = item.id === activeId || (activeId == null && i === 0)}
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
id="omni-item-{item.id}"
|
|
273
|
+
data-id={item.id}
|
|
274
|
+
class="omni-item"
|
|
275
|
+
class:omni-item-active={active}
|
|
276
|
+
role="option"
|
|
277
|
+
aria-selected={active}
|
|
278
|
+
onmouseenter={() => (activeId = item.id)}
|
|
279
|
+
onclick={() => executeItem(item)}
|
|
280
|
+
>
|
|
281
|
+
<div class="omni-item-icon">
|
|
282
|
+
{@html FileIcon}
|
|
283
|
+
</div>
|
|
284
|
+
<div class="omni-item-text">
|
|
285
|
+
<div class="omni-item-label">{item.label}</div>
|
|
286
|
+
<div class="omni-item-subtitle">{item.subtitle}</div>
|
|
287
|
+
</div>
|
|
288
|
+
<a
|
|
289
|
+
href={item.url}
|
|
290
|
+
class="omni-item-badge"
|
|
291
|
+
title="Open in new tab"
|
|
292
|
+
target="_blank"
|
|
293
|
+
rel="noopener noreferrer"
|
|
294
|
+
aria-hidden="true"
|
|
295
|
+
onclick={(e) => onExternalClick(e, item.url)}
|
|
296
|
+
>
|
|
297
|
+
{@html ExternalIcon}
|
|
298
|
+
</a>
|
|
299
|
+
<span class="omni-item-chevron" aria-hidden="true">
|
|
300
|
+
{@html ChevronIcon}
|
|
301
|
+
</span>
|
|
302
|
+
</button>
|
|
303
|
+
{/each}
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
{:else if showDocs}
|
|
308
|
+
<div class="omni-group">
|
|
309
|
+
<div class="omni-group-label">Documentation</div>
|
|
310
|
+
<div class="omni-group-items">
|
|
311
|
+
{#each flatItems as item, i}
|
|
312
|
+
{@const active = item.id === activeId || (activeId == null && i === 0)}
|
|
313
|
+
<button
|
|
314
|
+
type="button"
|
|
315
|
+
id="omni-item-{item.id}"
|
|
316
|
+
data-id={item.id}
|
|
317
|
+
class="omni-item"
|
|
318
|
+
class:omni-item-active={active}
|
|
319
|
+
role="option"
|
|
320
|
+
aria-selected={active}
|
|
321
|
+
onmouseenter={() => (activeId = item.id)}
|
|
322
|
+
onclick={() => executeItem(item)}
|
|
323
|
+
>
|
|
324
|
+
<div class="omni-item-icon">
|
|
325
|
+
{@html FileIcon}
|
|
326
|
+
</div>
|
|
327
|
+
<div class="omni-item-text">
|
|
328
|
+
<div class="omni-item-label">{item.label}</div>
|
|
329
|
+
<div class="omni-item-subtitle">{item.subtitle}</div>
|
|
330
|
+
</div>
|
|
331
|
+
<a
|
|
332
|
+
href={item.url}
|
|
333
|
+
class="omni-item-badge"
|
|
334
|
+
title="Open in new tab"
|
|
335
|
+
target="_blank"
|
|
336
|
+
rel="noopener noreferrer"
|
|
337
|
+
aria-hidden="true"
|
|
338
|
+
onclick={(e) => onExternalClick(e, item.url)}
|
|
339
|
+
>
|
|
340
|
+
{@html ExternalIcon}
|
|
341
|
+
</a>
|
|
342
|
+
<span class="omni-item-chevron" aria-hidden="true">
|
|
343
|
+
{@html ChevronIcon}
|
|
344
|
+
</span>
|
|
345
|
+
</button>
|
|
346
|
+
{/each}
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
88
349
|
{/if}
|
|
350
|
+
|
|
351
|
+
{#if showEmpty}
|
|
352
|
+
<div class="omni-empty">
|
|
353
|
+
<div class="omni-empty-icon">
|
|
354
|
+
{@html EmptyIcon}
|
|
355
|
+
</div>
|
|
356
|
+
{emptyText}
|
|
357
|
+
</div>
|
|
358
|
+
{/if}
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<div class="omni-footer">
|
|
362
|
+
<div class="omni-footer-inner">
|
|
363
|
+
<div class="omni-footer-hints">
|
|
364
|
+
<span class="omni-footer-hint">
|
|
365
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
366
|
+
<polyline points="9 18 15 12 9 6" />
|
|
367
|
+
</svg>
|
|
368
|
+
to select
|
|
369
|
+
</span>
|
|
370
|
+
<span class="omni-footer-hint">
|
|
371
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
372
|
+
<path d="M18 15l-6-6-6 6" />
|
|
373
|
+
</svg>
|
|
374
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
375
|
+
<path d="M6 9l6 6 6-6" />
|
|
376
|
+
</svg>
|
|
377
|
+
to navigate
|
|
378
|
+
</span>
|
|
379
|
+
<span class="omni-footer-hint omni-footer-hint-desktop">
|
|
380
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
381
|
+
<path d="M18 6 6 18" />
|
|
382
|
+
<path d="m6 6 12 12" />
|
|
383
|
+
</svg>
|
|
384
|
+
to close
|
|
385
|
+
</span>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
89
388
|
</div>
|
|
90
389
|
</div>
|
|
91
390
|
</div>
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { onMount, onDestroy, tick } from "svelte";
|
|
3
3
|
|
|
4
|
+
const ACTIVE_ZONE_TOP = 120;
|
|
5
|
+
const HYSTERESIS_PX = 65;
|
|
6
|
+
|
|
4
7
|
let { items = [], tocStyle = "default" } = $props();
|
|
5
8
|
let activeIds = $state(new Set());
|
|
6
|
-
let
|
|
7
|
-
let
|
|
9
|
+
let listEl = $state(null);
|
|
10
|
+
let lastStableId = null;
|
|
11
|
+
let scrollRafId = 0;
|
|
8
12
|
|
|
9
13
|
let svgPath = $state("");
|
|
10
14
|
let svgWidth = $state(0);
|
|
@@ -14,6 +18,63 @@
|
|
|
14
18
|
|
|
15
19
|
const isDirectional = $derived(tocStyle === "directional");
|
|
16
20
|
|
|
21
|
+
function getDistanceToZone(id) {
|
|
22
|
+
const el = document.getElementById(id);
|
|
23
|
+
if (!el) return Infinity;
|
|
24
|
+
const rect = el.getBoundingClientRect();
|
|
25
|
+
const mid = rect.top + rect.height / 2;
|
|
26
|
+
return Math.abs(mid - ACTIVE_ZONE_TOP);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getClosestId() {
|
|
30
|
+
const ids = items.map((item) => item.url.slice(1));
|
|
31
|
+
let bestId = null;
|
|
32
|
+
let bestDistance = Infinity;
|
|
33
|
+
for (const id of ids) {
|
|
34
|
+
const d = getDistanceToZone(id);
|
|
35
|
+
if (d < bestDistance) {
|
|
36
|
+
bestDistance = d;
|
|
37
|
+
bestId = id;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return bestId;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isInView(id) {
|
|
44
|
+
const el = document.getElementById(id);
|
|
45
|
+
if (!el) return false;
|
|
46
|
+
const rect = el.getBoundingClientRect();
|
|
47
|
+
return rect.top < window.innerHeight && rect.bottom > 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function updateActiveFromScroll() {
|
|
51
|
+
scrollRafId = 0;
|
|
52
|
+
const newId = getClosestId();
|
|
53
|
+
if (!newId) {
|
|
54
|
+
activeIds = new Set();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (lastStableId === null) {
|
|
58
|
+
lastStableId = newId;
|
|
59
|
+
activeIds = new Set([newId]);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (newId === lastStableId) {
|
|
63
|
+
activeIds = new Set([newId]);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const newDist = getDistanceToZone(newId);
|
|
67
|
+
const currentDist = getDistanceToZone(lastStableId);
|
|
68
|
+
const switchToNew = newDist <= currentDist - HYSTERESIS_PX || !isInView(lastStableId);
|
|
69
|
+
if (switchToNew) lastStableId = newId;
|
|
70
|
+
activeIds = new Set([lastStableId]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function scheduleActiveUpdate() {
|
|
74
|
+
if (scrollRafId !== 0) return;
|
|
75
|
+
scrollRafId = requestAnimationFrame(updateActiveFromScroll);
|
|
76
|
+
}
|
|
77
|
+
|
|
17
78
|
function getItemOffset(depth) {
|
|
18
79
|
if (depth <= 2) return 14;
|
|
19
80
|
if (depth === 3) return 26;
|
|
@@ -82,15 +143,6 @@
|
|
|
82
143
|
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
|
|
83
144
|
}
|
|
84
145
|
|
|
85
|
-
function observeHeadings() {
|
|
86
|
-
if (!observer) return;
|
|
87
|
-
observer.disconnect();
|
|
88
|
-
for (const item of items) {
|
|
89
|
-
const el = document.querySelector(item.url);
|
|
90
|
-
if (el) observer.observe(el);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
146
|
function isActive(item) {
|
|
95
147
|
return activeIds.has(item.url.slice(1));
|
|
96
148
|
}
|
|
@@ -124,33 +176,32 @@
|
|
|
124
176
|
}
|
|
125
177
|
|
|
126
178
|
onMount(() => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (entry.isIntersecting) {
|
|
131
|
-
activeIds.add(entry.target.id);
|
|
132
|
-
} else {
|
|
133
|
-
activeIds.delete(entry.target.id);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
activeIds = new Set(activeIds);
|
|
137
|
-
},
|
|
138
|
-
{ rootMargin: "-80px 0px -80% 0px" }
|
|
139
|
-
);
|
|
140
|
-
observeHeadings();
|
|
179
|
+
const onScroll = () => scheduleActiveUpdate();
|
|
180
|
+
window.addEventListener("scroll", onScroll, { passive: true, capture: true });
|
|
181
|
+
document.addEventListener("scroll", onScroll, { passive: true, capture: true });
|
|
141
182
|
|
|
142
|
-
|
|
143
|
-
|
|
183
|
+
tick().then(() => {
|
|
184
|
+
const id = getClosestId();
|
|
185
|
+
if (id) {
|
|
186
|
+
lastStableId = id;
|
|
187
|
+
activeIds = new Set([id]);
|
|
188
|
+
}
|
|
189
|
+
if (isDirectional) {
|
|
144
190
|
buildSvgPath();
|
|
145
191
|
calcThumb();
|
|
146
|
-
}
|
|
147
|
-
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return () => {
|
|
196
|
+
window.removeEventListener("scroll", onScroll, { capture: true });
|
|
197
|
+
document.removeEventListener("scroll", onScroll, { capture: true });
|
|
198
|
+
if (scrollRafId) cancelAnimationFrame(scrollRafId);
|
|
199
|
+
};
|
|
148
200
|
});
|
|
149
201
|
|
|
150
202
|
$effect(() => {
|
|
151
203
|
void items;
|
|
152
|
-
|
|
153
|
-
if (isDirectional) {
|
|
204
|
+
if (isDirectional && listEl) {
|
|
154
205
|
tick().then(() => {
|
|
155
206
|
buildSvgPath();
|
|
156
207
|
calcThumb();
|
|
@@ -165,9 +216,7 @@
|
|
|
165
216
|
}
|
|
166
217
|
});
|
|
167
218
|
|
|
168
|
-
onDestroy(() => {
|
|
169
|
-
observer?.disconnect();
|
|
170
|
-
});
|
|
219
|
+
onDestroy(() => {});
|
|
171
220
|
</script>
|
|
172
221
|
|
|
173
222
|
<div class="fd-toc-inner" class:fd-toc-directional={isDirectional}>
|
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: {
|