@farming-labs/svelte-theme 0.0.1
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 +57 -0
- package/src/components/AskAIDialog.svelte +342 -0
- package/src/components/Breadcrumb.svelte +47 -0
- package/src/components/Callout.svelte +28 -0
- package/src/components/DocsContent.svelte +32 -0
- package/src/components/DocsLayout.svelte +258 -0
- package/src/components/DocsPage.svelte +145 -0
- package/src/components/DocsSidebar.svelte +63 -0
- package/src/components/FloatingAIChat.svelte +474 -0
- package/src/components/MobileNav.svelte +14 -0
- package/src/components/SearchDialog.svelte +91 -0
- package/src/components/TableOfContents.svelte +69 -0
- package/src/components/ThemeToggle.svelte +40 -0
- package/src/index.d.ts +23 -0
- package/src/index.js +23 -0
- package/src/lib/renderMarkdown.js +110 -0
- package/src/themes/darksharp.js +42 -0
- package/src/themes/default.js +42 -0
- package/src/themes/pixel-border.js +38 -0
- package/styles/docs.css +2124 -0
- package/styles/pixel-border-bundle.css +6 -0
- package/styles/pixel-border.css +601 -0
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@farming-labs/svelte-theme",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Svelte UI components for @farming-labs/docs — layout, sidebar, TOC, search, and theme toggle",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"svelte": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.d.ts",
|
|
10
|
+
"svelte": "./src/index.js",
|
|
11
|
+
"import": "./src/index.js",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./fumadocs": {
|
|
15
|
+
"import": "./src/themes/default.js",
|
|
16
|
+
"default": "./src/themes/default.js"
|
|
17
|
+
},
|
|
18
|
+
"./pixel-border": {
|
|
19
|
+
"import": "./src/themes/pixel-border.js",
|
|
20
|
+
"default": "./src/themes/pixel-border.js"
|
|
21
|
+
},
|
|
22
|
+
"./darksharp": {
|
|
23
|
+
"import": "./src/themes/darksharp.js",
|
|
24
|
+
"default": "./src/themes/darksharp.js"
|
|
25
|
+
},
|
|
26
|
+
"./css": "./styles/docs.css",
|
|
27
|
+
"./fumadocs/css": "./styles/docs.css",
|
|
28
|
+
"./styles/pixel-border.css": "./styles/pixel-border.css",
|
|
29
|
+
"./pixel-border/css": "./styles/pixel-border-bundle.css"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"src",
|
|
33
|
+
"styles"
|
|
34
|
+
],
|
|
35
|
+
"keywords": [
|
|
36
|
+
"docs",
|
|
37
|
+
"svelte",
|
|
38
|
+
"sveltekit",
|
|
39
|
+
"theme",
|
|
40
|
+
"documentation"
|
|
41
|
+
],
|
|
42
|
+
"author": "Farming Labs",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"gray-matter": "^4.0.3",
|
|
46
|
+
"sugar-high": "^0.9.5",
|
|
47
|
+
"@farming-labs/docs": "0.0.2-beta.4",
|
|
48
|
+
"@farming-labs/svelte": "0.0.1"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"svelte": ">=5.0.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "echo 'Svelte components are shipped as source'",
|
|
55
|
+
"typecheck": "echo 'ok'"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { onMount, tick } from "svelte";
|
|
3
|
+
import { goto } from "$app/navigation";
|
|
4
|
+
import { renderMarkdown } from "../lib/renderMarkdown.js";
|
|
5
|
+
|
|
6
|
+
let { onclose, api = "/api/docs", suggestedQuestions = [], aiLabel = "AI", hideAITab = false } = $props();
|
|
7
|
+
|
|
8
|
+
let tab = $state("search");
|
|
9
|
+
let searchQuery = $state("");
|
|
10
|
+
let searchResults = $state([]);
|
|
11
|
+
let isSearching = $state(false);
|
|
12
|
+
let activeIndex = $state(0);
|
|
13
|
+
|
|
14
|
+
let messages = $state([]);
|
|
15
|
+
let aiInput = $state("");
|
|
16
|
+
let isStreaming = $state(false);
|
|
17
|
+
|
|
18
|
+
let searchInputEl = $state(null);
|
|
19
|
+
let aiInputEl = $state(null);
|
|
20
|
+
let messagesEndEl = $state(null);
|
|
21
|
+
let debounceTimer;
|
|
22
|
+
|
|
23
|
+
onMount(() => {
|
|
24
|
+
document.body.style.overflow = "hidden";
|
|
25
|
+
setTimeout(() => searchInputEl?.focus(), 50);
|
|
26
|
+
return () => { document.body.style.overflow = ""; };
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function switchTab(t) {
|
|
30
|
+
tab = t;
|
|
31
|
+
if (t === "search") {
|
|
32
|
+
setTimeout(() => searchInputEl?.focus(), 50);
|
|
33
|
+
} else {
|
|
34
|
+
setTimeout(() => aiInputEl?.focus(), 50);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
$effect(() => {
|
|
39
|
+
clearTimeout(debounceTimer);
|
|
40
|
+
if (!searchQuery.trim() || tab !== "search") {
|
|
41
|
+
searchResults = [];
|
|
42
|
+
activeIndex = 0;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
isSearching = true;
|
|
46
|
+
debounceTimer = setTimeout(async () => {
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(`${api}?query=${encodeURIComponent(searchQuery)}`);
|
|
49
|
+
if (res.ok) {
|
|
50
|
+
searchResults = await res.json();
|
|
51
|
+
activeIndex = 0;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
searchResults = [];
|
|
55
|
+
}
|
|
56
|
+
isSearching = false;
|
|
57
|
+
}, 150);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function navigateTo(url) {
|
|
61
|
+
onclose?.();
|
|
62
|
+
goto(url);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleSearchKeydown(e) {
|
|
66
|
+
if (e.key === "ArrowDown") {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
activeIndex = Math.min(activeIndex + 1, searchResults.length - 1);
|
|
69
|
+
} else if (e.key === "ArrowUp") {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
activeIndex = Math.max(activeIndex - 1, 0);
|
|
72
|
+
} else if (e.key === "Enter" && searchResults[activeIndex]) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
navigateTo(searchResults[activeIndex].url);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function handleOverlayKeydown(e) {
|
|
79
|
+
if (e.key === "Escape") onclose?.();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function submitQuestion(question) {
|
|
83
|
+
if (!question.trim() || isStreaming) return;
|
|
84
|
+
const userMsg = { role: "user", content: question };
|
|
85
|
+
const newMessages = [...messages, userMsg];
|
|
86
|
+
aiInput = "";
|
|
87
|
+
isStreaming = true;
|
|
88
|
+
messages = [...newMessages, { role: "assistant", content: "" }];
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(api, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
messages: newMessages.map((m) => ({ role: m.role, content: m.content })),
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
let errMsg = "Something went wrong.";
|
|
101
|
+
try {
|
|
102
|
+
const err = await res.json();
|
|
103
|
+
errMsg = err.error || errMsg;
|
|
104
|
+
} catch {}
|
|
105
|
+
messages = [...newMessages, { role: "assistant", content: errMsg }];
|
|
106
|
+
isStreaming = false;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const reader = res.body.getReader();
|
|
111
|
+
const decoder = new TextDecoder();
|
|
112
|
+
let buffer = "";
|
|
113
|
+
let assistantContent = "";
|
|
114
|
+
|
|
115
|
+
while (true) {
|
|
116
|
+
const { done, value } = await reader.read();
|
|
117
|
+
if (done) break;
|
|
118
|
+
buffer += decoder.decode(value, { stream: true });
|
|
119
|
+
const lines = buffer.split("\n");
|
|
120
|
+
buffer = lines.pop() || "";
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
if (line.startsWith("data: ")) {
|
|
123
|
+
const data = line.slice(6).trim();
|
|
124
|
+
if (data === "[DONE]") continue;
|
|
125
|
+
try {
|
|
126
|
+
const json = JSON.parse(data);
|
|
127
|
+
const content = json.choices?.[0]?.delta?.content;
|
|
128
|
+
if (content) {
|
|
129
|
+
assistantContent += content;
|
|
130
|
+
messages = [...newMessages, { role: "assistant", content: assistantContent }];
|
|
131
|
+
}
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (assistantContent) {
|
|
137
|
+
messages = [...newMessages, { role: "assistant", content: assistantContent }];
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
messages = [
|
|
141
|
+
...newMessages,
|
|
142
|
+
{ role: "assistant", content: "Failed to connect. Please try again." },
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
isStreaming = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function handleAIKeydown(e) {
|
|
149
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
submitQuestion(aiInput);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function clearChat() {
|
|
156
|
+
messages = [];
|
|
157
|
+
aiInput = "";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
$effect(() => {
|
|
161
|
+
if (messages.length > 0) {
|
|
162
|
+
tick().then(() => messagesEndEl?.scrollIntoView({ behavior: "smooth" }));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
let canSend = $derived(aiInput.trim() && !isStreaming);
|
|
167
|
+
</script>
|
|
168
|
+
|
|
169
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
170
|
+
<div class="fd-ai-overlay" onclick={onclose} onkeydown={handleOverlayKeydown} role="dialog" tabindex="-1">
|
|
171
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
172
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
173
|
+
<div
|
|
174
|
+
class="fd-ai-dialog"
|
|
175
|
+
onclick={(e) => e.stopPropagation()}
|
|
176
|
+
role="document"
|
|
177
|
+
style="left:50%;top:50%;transform:translate(-50%,-50%);width:min(680px,calc(100vw - 32px));max-height:min(560px,calc(100vh - 64px));animation:fd-ai-slide-up 200ms ease-out"
|
|
178
|
+
>
|
|
179
|
+
<!-- Tab bar -->
|
|
180
|
+
<div class="fd-ai-tab-bar">
|
|
181
|
+
<button onclick={() => switchTab("search")} class="fd-ai-tab" data-active={tab === "search"}>
|
|
182
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
183
|
+
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
|
|
184
|
+
</svg>
|
|
185
|
+
Search
|
|
186
|
+
</button>
|
|
187
|
+
{#if !hideAITab}
|
|
188
|
+
<button onclick={() => switchTab("ai")} class="fd-ai-tab" data-active={tab === "ai"}>
|
|
189
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
190
|
+
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z" />
|
|
191
|
+
<path d="M20 3v4" /><path d="M22 5h-4" />
|
|
192
|
+
</svg>
|
|
193
|
+
Ask {aiLabel}
|
|
194
|
+
</button>
|
|
195
|
+
{/if}
|
|
196
|
+
<div style="margin-left:auto;display:flex;gap:4px;align-items:center">
|
|
197
|
+
<kbd class="fd-ai-esc">ESC</kbd>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<!-- Search tab -->
|
|
202
|
+
{#if tab === "search"}
|
|
203
|
+
<div style="display:flex;flex-direction:column;flex:1;min-height:0">
|
|
204
|
+
<div class="fd-ai-search-wrap">
|
|
205
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
206
|
+
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
|
|
207
|
+
</svg>
|
|
208
|
+
<input
|
|
209
|
+
bind:this={searchInputEl}
|
|
210
|
+
bind:value={searchQuery}
|
|
211
|
+
class="fd-ai-input"
|
|
212
|
+
placeholder="Search documentation..."
|
|
213
|
+
type="text"
|
|
214
|
+
onkeydown={handleSearchKeydown}
|
|
215
|
+
/>
|
|
216
|
+
{#if isSearching}
|
|
217
|
+
<span class="fd-ai-loading-dots">
|
|
218
|
+
<span class="fd-ai-loading-dot"></span>
|
|
219
|
+
<span class="fd-ai-loading-dot"></span>
|
|
220
|
+
<span class="fd-ai-loading-dot"></span>
|
|
221
|
+
</span>
|
|
222
|
+
{/if}
|
|
223
|
+
</div>
|
|
224
|
+
<div class="fd-ai-results">
|
|
225
|
+
{#if searchResults.length > 0}
|
|
226
|
+
{#each searchResults as result, i}
|
|
227
|
+
<button
|
|
228
|
+
class="fd-ai-result"
|
|
229
|
+
data-active={i === activeIndex}
|
|
230
|
+
onclick={() => navigateTo(result.url)}
|
|
231
|
+
onmouseenter={() => { activeIndex = i; }}
|
|
232
|
+
>
|
|
233
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
234
|
+
<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" />
|
|
235
|
+
</svg>
|
|
236
|
+
<span style="flex:1">{result.content}</span>
|
|
237
|
+
</button>
|
|
238
|
+
{/each}
|
|
239
|
+
{:else}
|
|
240
|
+
<div class="fd-ai-result-empty">
|
|
241
|
+
{#if searchQuery.trim()}
|
|
242
|
+
{isSearching ? "Searching..." : `No results found. Try the Ask ${aiLabel} tab.`}
|
|
243
|
+
{:else}
|
|
244
|
+
Type to search the docs
|
|
245
|
+
{/if}
|
|
246
|
+
</div>
|
|
247
|
+
{/if}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
{/if}
|
|
251
|
+
|
|
252
|
+
<!-- Ask AI tab -->
|
|
253
|
+
{#if tab === "ai"}
|
|
254
|
+
<div style="display:flex;flex-direction:column;flex:1;min-height:0">
|
|
255
|
+
<div class="fd-ai-messages">
|
|
256
|
+
{#if messages.length === 0}
|
|
257
|
+
<div class="fd-ai-empty">
|
|
258
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
259
|
+
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z" />
|
|
260
|
+
<path d="M20 3v4" /><path d="M22 5h-4" />
|
|
261
|
+
</svg>
|
|
262
|
+
<div class="fd-ai-empty-title">Ask anything about the docs</div>
|
|
263
|
+
<div class="fd-ai-empty-desc">
|
|
264
|
+
{aiLabel} will search through the documentation and answer your question with relevant context.
|
|
265
|
+
</div>
|
|
266
|
+
{#if suggestedQuestions.length > 0}
|
|
267
|
+
<div class="fd-ai-suggestions">
|
|
268
|
+
{#each suggestedQuestions as q}
|
|
269
|
+
<button onclick={() => submitQuestion(q)} class="fd-ai-suggestion">
|
|
270
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
271
|
+
<path d="m5 12 7-7 7 7" /><path d="M12 19V5" />
|
|
272
|
+
</svg>
|
|
273
|
+
<span style="flex:1">{q}</span>
|
|
274
|
+
</button>
|
|
275
|
+
{/each}
|
|
276
|
+
</div>
|
|
277
|
+
{/if}
|
|
278
|
+
</div>
|
|
279
|
+
{:else}
|
|
280
|
+
{#each messages as msg}
|
|
281
|
+
<div class="fd-ai-msg" data-role={msg.role}>
|
|
282
|
+
<div class="fd-ai-msg-label">
|
|
283
|
+
{msg.role === "user" ? "You" : aiLabel}
|
|
284
|
+
</div>
|
|
285
|
+
{#if msg.role === "user"}
|
|
286
|
+
<div class="fd-ai-bubble-user">{msg.content}</div>
|
|
287
|
+
{:else}
|
|
288
|
+
<div class="fd-ai-bubble-ai">
|
|
289
|
+
{#if msg.content}
|
|
290
|
+
{@html renderMarkdown(msg.content)}
|
|
291
|
+
{:else}
|
|
292
|
+
<span class="fd-ai-loading">
|
|
293
|
+
<span class="fd-ai-loading-text">{aiLabel} is thinking</span>
|
|
294
|
+
<span class="fd-ai-loading-dots">
|
|
295
|
+
<span class="fd-ai-loading-dot"></span>
|
|
296
|
+
<span class="fd-ai-loading-dot"></span>
|
|
297
|
+
<span class="fd-ai-loading-dot"></span>
|
|
298
|
+
</span>
|
|
299
|
+
</span>
|
|
300
|
+
{/if}
|
|
301
|
+
</div>
|
|
302
|
+
{/if}
|
|
303
|
+
</div>
|
|
304
|
+
{/each}
|
|
305
|
+
<div bind:this={messagesEndEl}></div>
|
|
306
|
+
{/if}
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div class="fd-ai-chat-footer">
|
|
310
|
+
{#if messages.length > 0}
|
|
311
|
+
<div style="display:flex;justify-content:flex-end;padding-bottom:8px">
|
|
312
|
+
<button onclick={clearChat} class="fd-ai-clear-btn">Clear chat</button>
|
|
313
|
+
</div>
|
|
314
|
+
{/if}
|
|
315
|
+
<div class="fd-ai-input-wrap">
|
|
316
|
+
<input
|
|
317
|
+
bind:this={aiInputEl}
|
|
318
|
+
bind:value={aiInput}
|
|
319
|
+
type="text"
|
|
320
|
+
placeholder="Ask a question..."
|
|
321
|
+
onkeydown={handleAIKeydown}
|
|
322
|
+
disabled={isStreaming}
|
|
323
|
+
class="fd-ai-input"
|
|
324
|
+
style="opacity:{isStreaming ? 0.5 : 1}"
|
|
325
|
+
/>
|
|
326
|
+
<button
|
|
327
|
+
onclick={() => submitQuestion(aiInput)}
|
|
328
|
+
disabled={!canSend}
|
|
329
|
+
class="fd-ai-send-btn"
|
|
330
|
+
data-active={canSend}
|
|
331
|
+
aria-label="Send"
|
|
332
|
+
>
|
|
333
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
334
|
+
<path d="m5 12 7-7 7 7" /><path d="M12 19V5" />
|
|
335
|
+
</svg>
|
|
336
|
+
</button>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
{/if}
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* Breadcrumb — Path-based breadcrumb showing parent / current.
|
|
4
|
+
*/
|
|
5
|
+
let { pathname = "", entry = "docs" } = $props();
|
|
6
|
+
|
|
7
|
+
let segments = $derived.by(() => {
|
|
8
|
+
const all = pathname.split("/").filter(Boolean);
|
|
9
|
+
return all.filter((s) => s.toLowerCase() !== entry.toLowerCase());
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
let parentLabel = $derived.by(() => {
|
|
13
|
+
if (segments.length < 2) return "";
|
|
14
|
+
return segments[segments.length - 2]
|
|
15
|
+
.replace(/-/g, " ")
|
|
16
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
let currentLabel = $derived.by(() => {
|
|
20
|
+
if (segments.length < 2) return "";
|
|
21
|
+
return segments[segments.length - 1]
|
|
22
|
+
.replace(/-/g, " ")
|
|
23
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let parentUrl = $derived.by(() => {
|
|
27
|
+
if (segments.length < 2) return "";
|
|
28
|
+
const all = pathname.split("/").filter(Boolean);
|
|
29
|
+
const parentSegment = segments[segments.length - 2];
|
|
30
|
+
const parentIndex = all.indexOf(parentSegment);
|
|
31
|
+
return "/" + all.slice(0, parentIndex + 1).join("/");
|
|
32
|
+
});
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
{#if segments.length >= 2}
|
|
36
|
+
<nav class="fd-breadcrumb" aria-label="Breadcrumb">
|
|
37
|
+
<span class="fd-breadcrumb-item">
|
|
38
|
+
<a href={parentUrl} class="fd-breadcrumb-parent fd-breadcrumb-link">
|
|
39
|
+
{parentLabel}
|
|
40
|
+
</a>
|
|
41
|
+
</span>
|
|
42
|
+
<span class="fd-breadcrumb-item">
|
|
43
|
+
<span class="fd-breadcrumb-sep">/</span>
|
|
44
|
+
<span class="fd-breadcrumb-current">{currentLabel}</span>
|
|
45
|
+
</span>
|
|
46
|
+
</nav>
|
|
47
|
+
{/if}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* Callout — Admonition/alert block for notes, warnings, tips, etc.
|
|
4
|
+
* Matches fumadocs-ui callout styling.
|
|
5
|
+
*/
|
|
6
|
+
let {
|
|
7
|
+
type = "note",
|
|
8
|
+
children,
|
|
9
|
+
} = $props();
|
|
10
|
+
|
|
11
|
+
const icons = {
|
|
12
|
+
note: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
|
|
13
|
+
warning: `<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="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
|
|
14
|
+
tip: `<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="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0018 8 6 6 0 006 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 019 14"/></svg>`,
|
|
15
|
+
important: `<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="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>`,
|
|
16
|
+
caution: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>`,
|
|
17
|
+
};
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div class="fd-callout fd-callout-{type}" role="note">
|
|
21
|
+
<div class="fd-callout-indicator" role="none"></div>
|
|
22
|
+
<div class="fd-callout-icon">
|
|
23
|
+
{@html icons[type] || icons.note}
|
|
24
|
+
</div>
|
|
25
|
+
<div class="fd-callout-content">
|
|
26
|
+
{@render children()}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import DocsPage from "./DocsPage.svelte";
|
|
3
|
+
|
|
4
|
+
let { data, config = null } = $props();
|
|
5
|
+
|
|
6
|
+
let titleSuffix = $derived(
|
|
7
|
+
config?.metadata?.titleTemplate
|
|
8
|
+
? config.metadata.titleTemplate.replace("%s", "")
|
|
9
|
+
: " – Docs"
|
|
10
|
+
);
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<svelte:head>
|
|
14
|
+
<title>{data.title}{titleSuffix}</title>
|
|
15
|
+
{#if data.description}
|
|
16
|
+
<meta name="description" content={data.description} />
|
|
17
|
+
{/if}
|
|
18
|
+
</svelte:head>
|
|
19
|
+
|
|
20
|
+
<DocsPage
|
|
21
|
+
entry={config?.entry ?? "docs"}
|
|
22
|
+
tocEnabled={true}
|
|
23
|
+
breadcrumbEnabled={config?.breadcrumb?.enabled ?? true}
|
|
24
|
+
previousPage={data.previousPage}
|
|
25
|
+
nextPage={data.nextPage}
|
|
26
|
+
editOnGithub={data.editOnGithub}
|
|
27
|
+
lastModified={data.lastModified}
|
|
28
|
+
>
|
|
29
|
+
{#snippet children()}
|
|
30
|
+
{@html data.html}
|
|
31
|
+
{/snippet}
|
|
32
|
+
</DocsPage>
|