@farming-labs/astro-theme 0.0.2-beta.15
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 +75 -0
- package/src/components/DocsContent.astro +39 -0
- package/src/components/DocsLayout.astro +321 -0
- package/src/components/DocsPage.astro +178 -0
- package/src/components/FloatingAIChat.astro +468 -0
- package/src/components/SearchDialog.astro +315 -0
- package/src/components/ThemeToggle.astro +46 -0
- package/src/index.d.ts +10 -0
- package/src/index.js +10 -0
- package/src/lib/renderMarkdown.js +110 -0
- package/src/themes/darksharp.d.ts +4 -0
- package/src/themes/darksharp.js +42 -0
- package/src/themes/default.d.ts +4 -0
- package/src/themes/default.js +42 -0
- package/src/themes/pixel-border.d.ts +4 -0
- package/src/themes/pixel-border.js +38 -0
- package/styles/darksharp-bundle.css +6 -0
- package/styles/darksharp.css +206 -0
- package/styles/docs.css +2176 -0
- package/styles/pixel-border-bundle.css +6 -0
- package/styles/pixel-border.css +606 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
---
|
|
2
|
+
const { config = null } = Astro.props;
|
|
3
|
+
const aiMode = config?.ai?.mode ?? "search";
|
|
4
|
+
const aiInSearch = config?.ai?.enabled && aiMode !== "floating";
|
|
5
|
+
const aiLabel = config?.ai?.aiLabel ?? "AI";
|
|
6
|
+
const suggestedQuestions = config?.ai?.suggestedQuestions ?? [];
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div id="fd-search-config" data-ai-label={aiLabel} style="display:none"></div>
|
|
10
|
+
<div class="fd-ai-overlay" id="fd-search-dialog" style="display:none">
|
|
11
|
+
<div class="fd-ai-dialog" style="left:50%;top:12%;transform:translateX(-50%);width:min(640px,calc(100vw - 32px));max-height:min(520px,calc(100vh - 120px));animation:fd-ai-slide-up 200ms ease-out">
|
|
12
|
+
<div class="fd-ai-tab-bar">
|
|
13
|
+
<button class="fd-ai-tab" data-tab="search" data-active="true">
|
|
14
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
15
|
+
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
|
|
16
|
+
</svg>
|
|
17
|
+
Search
|
|
18
|
+
</button>
|
|
19
|
+
{aiInSearch && (
|
|
20
|
+
<button class="fd-ai-tab" data-tab="ai">
|
|
21
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
22
|
+
<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" />
|
|
23
|
+
</svg>
|
|
24
|
+
Ask {aiLabel}
|
|
25
|
+
</button>
|
|
26
|
+
)}
|
|
27
|
+
<span style="margin-left:auto;display:flex;align-items:center;padding-right:12px">
|
|
28
|
+
<kbd class="fd-ai-esc">ESC</kbd>
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div id="fd-search-panel" style="display:flex;flex-direction:column;flex:1;min-height:0">
|
|
33
|
+
<div class="fd-ai-search-wrap">
|
|
34
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
35
|
+
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
|
|
36
|
+
</svg>
|
|
37
|
+
<input id="fd-search-input" class="fd-ai-input" placeholder="Search documentation..." type="text" />
|
|
38
|
+
</div>
|
|
39
|
+
<div class="fd-ai-results" id="fd-search-results">
|
|
40
|
+
<div class="fd-ai-result-empty">Type to search the docs</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{aiInSearch && (
|
|
45
|
+
<div id="fd-ai-panel" style="display:none;flex-direction:column;flex:1;min-height:0">
|
|
46
|
+
<div class="fd-ai-messages" id="fd-ai-messages">
|
|
47
|
+
<div class="fd-ai-empty">
|
|
48
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
49
|
+
<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" />
|
|
50
|
+
<path d="M20 3v4" /><path d="M22 5h-4" />
|
|
51
|
+
</svg>
|
|
52
|
+
<div class="fd-ai-empty-title">Ask anything about the docs</div>
|
|
53
|
+
<div class="fd-ai-empty-desc">{aiLabel} will search through the documentation and answer your question.</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="fd-ai-chat-footer">
|
|
57
|
+
<div class="fd-ai-input-wrap">
|
|
58
|
+
<input id="fd-ai-input" class="fd-ai-input" placeholder="Ask a question..." type="text" />
|
|
59
|
+
<button id="fd-ai-send" class="fd-ai-send-btn" aria-label="Send">
|
|
60
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
61
|
+
<path d="m5 12 7-7 7 7" /><path d="M12 19V5" />
|
|
62
|
+
</svg>
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<script>
|
|
72
|
+
import { highlight } from 'sugar-high';
|
|
73
|
+
|
|
74
|
+
function escapeHtml(s: string): string {
|
|
75
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildCodeBlock(lang: string, code: string): string {
|
|
79
|
+
const trimmed = code.replace(/\n$/, '');
|
|
80
|
+
const highlighted = highlight(trimmed).replace(/<\/span>\n<span/g, '</span><span');
|
|
81
|
+
const langLabel = lang ? `<div class="fd-ai-code-lang">${escapeHtml(lang)}</div>` : '';
|
|
82
|
+
const copyBtn = `<button class="fd-ai-code-copy" onclick="(function(btn){var code=btn.closest('.fd-ai-code-block').querySelector('code').textContent;navigator.clipboard.writeText(code).then(function(){btn.textContent='Copied!';setTimeout(function(){btn.textContent='Copy'},1500)})})(this)">Copy</button>`;
|
|
83
|
+
return `<div class="fd-ai-code-block"><div class="fd-ai-code-header">${langLabel}${copyBtn}</div><pre><code>${highlighted}</code></pre></div>`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isTableRow(line: string): boolean {
|
|
87
|
+
const t = line.trim();
|
|
88
|
+
return t.startsWith('|') && t.endsWith('|') && t.includes('|');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isTableSeparator(line: string): boolean {
|
|
92
|
+
return /^\|[\s:]*-+[\s:]*(\|[\s:]*-+[\s:]*)*\|$/.test(line.trim());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renderTable(rows: string[]): string {
|
|
96
|
+
const parseRow = (row: string) =>
|
|
97
|
+
row.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim());
|
|
98
|
+
const headerCells = parseRow(rows[0]);
|
|
99
|
+
const thead = `<thead><tr>${headerCells.map(c => `<th>${c}</th>`).join('')}</tr></thead>`;
|
|
100
|
+
const bodyRows = rows.slice(1).map(row => {
|
|
101
|
+
const cells = parseRow(row);
|
|
102
|
+
return `<tr>${cells.map(c => `<td>${c}</td>`).join('')}</tr>`;
|
|
103
|
+
}).join('');
|
|
104
|
+
return `<table>${thead}<tbody>${bodyRows}</tbody></table>`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderMarkdown(text: string): string {
|
|
108
|
+
if (!text) return '';
|
|
109
|
+
const codeBlocks: string[] = [];
|
|
110
|
+
let processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, lang, code) => {
|
|
111
|
+
codeBlocks.push(buildCodeBlock(lang, code));
|
|
112
|
+
return `\x00CB${codeBlocks.length - 1}\x00`;
|
|
113
|
+
});
|
|
114
|
+
processed = processed.replace(/```(\w*)\n([\s\S]*)$/, (_m, lang, code) => {
|
|
115
|
+
codeBlocks.push(buildCodeBlock(lang, code));
|
|
116
|
+
return `\x00CB${codeBlocks.length - 1}\x00`;
|
|
117
|
+
});
|
|
118
|
+
const lines = processed.split('\n');
|
|
119
|
+
const output: string[] = [];
|
|
120
|
+
let i = 0;
|
|
121
|
+
while (i < lines.length) {
|
|
122
|
+
if (isTableRow(lines[i]) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
|
|
123
|
+
const tableLines = [lines[i]];
|
|
124
|
+
i += 2;
|
|
125
|
+
while (i < lines.length && isTableRow(lines[i])) { tableLines.push(lines[i]); i++; }
|
|
126
|
+
output.push(renderTable(tableLines));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
output.push(lines[i]);
|
|
130
|
+
i++;
|
|
131
|
+
}
|
|
132
|
+
let result = output.join('\n');
|
|
133
|
+
result = result
|
|
134
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
135
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
136
|
+
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>')
|
|
137
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
|
138
|
+
.replace(/^### (.*$)/gm, '<h4>$1</h4>')
|
|
139
|
+
.replace(/^## (.*$)/gm, '<h3>$1</h3>')
|
|
140
|
+
.replace(/^# (.*$)/gm, '<h2>$1</h2>')
|
|
141
|
+
.replace(/^[-*] (.*$)/gm, '<div style="display:flex;gap:8px;padding:2px 0"><span style="opacity:0.5">\u2022</span><span>$1</span></div>')
|
|
142
|
+
.replace(/^(\d+)\. (.*$)/gm, '<div style="display:flex;gap:8px;padding:2px 0"><span style="opacity:0.5">$1.</span><span>$2</span></div>')
|
|
143
|
+
.replace(/\n\n/g, '<div style="height:8px"></div>')
|
|
144
|
+
.replace(/\n/g, '<br>');
|
|
145
|
+
result = result.replace(/\x00CB(\d+)\x00/g, (_m, idx) => codeBlocks[Number(idx)]);
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function initSearchDialog() {
|
|
150
|
+
const cfgEl = document.getElementById('fd-search-config');
|
|
151
|
+
const aiLabel = cfgEl?.dataset.aiLabel || 'AI';
|
|
152
|
+
const dialog = document.getElementById('fd-search-dialog');
|
|
153
|
+
const searchInput = document.getElementById('fd-search-input');
|
|
154
|
+
const searchResults = document.getElementById('fd-search-results');
|
|
155
|
+
const tabs = dialog?.querySelectorAll('.fd-ai-tab');
|
|
156
|
+
const searchPanel = document.getElementById('fd-search-panel');
|
|
157
|
+
const aiPanel = document.getElementById('fd-ai-panel');
|
|
158
|
+
let debounceTimer: ReturnType<typeof setTimeout>;
|
|
159
|
+
|
|
160
|
+
dialog?.addEventListener('click', (e) => {
|
|
161
|
+
if (e.target === dialog) dialog.style.display = 'none';
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
dialog?.querySelector('.fd-ai-dialog')?.addEventListener('click', (e) => {
|
|
165
|
+
e.stopPropagation();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
tabs?.forEach(tab => {
|
|
169
|
+
tab.addEventListener('click', () => {
|
|
170
|
+
const target = tab.getAttribute('data-tab');
|
|
171
|
+
tabs.forEach(t => t.removeAttribute('data-active'));
|
|
172
|
+
tab.setAttribute('data-active', 'true');
|
|
173
|
+
if (searchPanel) searchPanel.style.display = target === 'search' ? 'flex' : 'none';
|
|
174
|
+
if (aiPanel) aiPanel.style.display = target === 'ai' ? 'flex' : 'none';
|
|
175
|
+
if (target === 'search') searchInput?.focus();
|
|
176
|
+
else document.getElementById('fd-ai-input')?.focus();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
searchInput?.addEventListener('input', () => {
|
|
181
|
+
clearTimeout(debounceTimer);
|
|
182
|
+
const query = (searchInput as HTMLInputElement).value.trim();
|
|
183
|
+
if (!query) {
|
|
184
|
+
if (searchResults) searchResults.innerHTML = '<div class="fd-ai-result-empty">Type to search the docs</div>';
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
debounceTimer = setTimeout(async () => {
|
|
188
|
+
try {
|
|
189
|
+
const res = await fetch(`/api/docs?query=${encodeURIComponent(query)}`);
|
|
190
|
+
if (res.ok) {
|
|
191
|
+
const data = await res.json();
|
|
192
|
+
if (data.length === 0) {
|
|
193
|
+
searchResults!.innerHTML = `<div class="fd-ai-result-empty">No results found</div>`;
|
|
194
|
+
} else {
|
|
195
|
+
searchResults!.innerHTML = data.map((r: any) => `
|
|
196
|
+
<a href="${r.url}" class="fd-ai-result" style="display:flex;align-items:center;gap:8px;text-decoration:none">
|
|
197
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
198
|
+
<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" />
|
|
199
|
+
</svg>
|
|
200
|
+
<span style="flex:1">${r.content}</span>
|
|
201
|
+
</a>
|
|
202
|
+
`).join('');
|
|
203
|
+
searchResults!.querySelectorAll('a').forEach(a => {
|
|
204
|
+
a.addEventListener('click', () => { dialog!.style.display = 'none'; });
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch {}
|
|
209
|
+
}, 150);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// AI chat
|
|
213
|
+
const aiInput = document.getElementById('fd-ai-input') as HTMLInputElement | null;
|
|
214
|
+
const aiSend = document.getElementById('fd-ai-send');
|
|
215
|
+
const aiMessages = document.getElementById('fd-ai-messages');
|
|
216
|
+
let messages: Array<{ role: string; content: string }> = [];
|
|
217
|
+
let isStreaming = false;
|
|
218
|
+
|
|
219
|
+
let renderScheduled = false;
|
|
220
|
+
function scheduleRender() {
|
|
221
|
+
if (renderScheduled) return;
|
|
222
|
+
renderScheduled = true;
|
|
223
|
+
requestAnimationFrame(() => { renderScheduled = false; doRender(); });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function submitAI(question: string) {
|
|
227
|
+
if (!question.trim() || isStreaming) return;
|
|
228
|
+
isStreaming = true;
|
|
229
|
+
messages.push({ role: 'user', content: question });
|
|
230
|
+
messages.push({ role: 'assistant', content: '' });
|
|
231
|
+
if (aiInput) aiInput.value = '';
|
|
232
|
+
doRender();
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const res = await fetch('/api/docs', {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: { 'Content-Type': 'application/json' },
|
|
238
|
+
body: JSON.stringify({ messages: messages.filter(m => m.content).map(m => ({ role: m.role, content: m.content })) }),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (!res.ok) {
|
|
242
|
+
let errMsg = 'Something went wrong.';
|
|
243
|
+
try { const err = await res.json(); errMsg = err.error || errMsg; } catch {}
|
|
244
|
+
messages[messages.length - 1].content = errMsg;
|
|
245
|
+
doRender();
|
|
246
|
+
isStreaming = false;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const reader = res.body!.getReader();
|
|
251
|
+
const decoder = new TextDecoder();
|
|
252
|
+
let buffer = '';
|
|
253
|
+
let assistantContent = '';
|
|
254
|
+
|
|
255
|
+
while (true) {
|
|
256
|
+
const { done, value } = await reader.read();
|
|
257
|
+
if (done) break;
|
|
258
|
+
buffer += decoder.decode(value, { stream: true });
|
|
259
|
+
const lines = buffer.split('\n');
|
|
260
|
+
buffer = lines.pop() || '';
|
|
261
|
+
for (const line of lines) {
|
|
262
|
+
if (line.startsWith('data: ')) {
|
|
263
|
+
const data = line.slice(6).trim();
|
|
264
|
+
if (data === '[DONE]') continue;
|
|
265
|
+
try {
|
|
266
|
+
const json = JSON.parse(data);
|
|
267
|
+
const content = json.choices?.[0]?.delta?.content;
|
|
268
|
+
if (content) {
|
|
269
|
+
assistantContent += content;
|
|
270
|
+
messages[messages.length - 1].content = assistantContent;
|
|
271
|
+
scheduleRender();
|
|
272
|
+
}
|
|
273
|
+
} catch {}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
messages[messages.length - 1].content = messages[messages.length - 1].content || 'Failed to connect.';
|
|
279
|
+
doRender();
|
|
280
|
+
}
|
|
281
|
+
isStreaming = false;
|
|
282
|
+
doRender();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function doRender() {
|
|
286
|
+
if (!aiMessages) return;
|
|
287
|
+
if (messages.length === 0) {
|
|
288
|
+
aiMessages.innerHTML = '<div class="fd-ai-empty"><div class="fd-ai-empty-title">Ask anything about the docs</div></div>';
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const THINKING = `<span class="fd-ai-loading"><span class="fd-ai-loading-text">Thinking</span><span class="fd-ai-loading-dots"><span class="fd-ai-loading-dot"></span><span class="fd-ai-loading-dot"></span><span class="fd-ai-loading-dot"></span></span></span>`;
|
|
292
|
+
aiMessages.innerHTML = messages.map(m => {
|
|
293
|
+
const renderedContent = m.role === 'user'
|
|
294
|
+
? escapeHtml(m.content)
|
|
295
|
+
: (m.content ? renderMarkdown(m.content) : THINKING);
|
|
296
|
+
return `<div class="fd-ai-msg" data-role="${m.role}"><div class="fd-ai-msg-label">${m.role === 'user' ? 'You' : aiLabel}</div><div class="${m.role === 'user' ? 'fd-ai-bubble-user' : 'fd-ai-bubble-ai'}">${renderedContent}</div></div>`;
|
|
297
|
+
}).join('');
|
|
298
|
+
aiMessages.scrollTop = aiMessages.scrollHeight;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
aiSend?.addEventListener('click', () => {
|
|
302
|
+
if (aiInput?.value) submitAI(aiInput.value);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
aiInput?.addEventListener('keydown', (e) => {
|
|
306
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
307
|
+
e.preventDefault();
|
|
308
|
+
if (aiInput.value) submitAI(aiInput.value);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
initSearchDialog();
|
|
314
|
+
document.addEventListener('astro:after-swap', initSearchDialog);
|
|
315
|
+
</script>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
<button class="fd-theme-toggle" id="fd-theme-toggle" aria-label="Toggle theme">
|
|
4
|
+
<svg id="fd-theme-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none">
|
|
5
|
+
<circle cx="12" cy="12" r="5" />
|
|
6
|
+
<line x1="12" y1="1" x2="12" y2="3" />
|
|
7
|
+
<line x1="12" y1="21" x2="12" y2="23" />
|
|
8
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
9
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
10
|
+
<line x1="1" y1="12" x2="3" y2="12" />
|
|
11
|
+
<line x1="21" y1="12" x2="23" y2="12" />
|
|
12
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
13
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
14
|
+
</svg>
|
|
15
|
+
<svg id="fd-theme-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none">
|
|
16
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
17
|
+
</svg>
|
|
18
|
+
</button>
|
|
19
|
+
|
|
20
|
+
<script>
|
|
21
|
+
function initThemeToggle() {
|
|
22
|
+
const btn = document.getElementById('fd-theme-toggle');
|
|
23
|
+
const sun = document.getElementById('fd-theme-sun');
|
|
24
|
+
const moon = document.getElementById('fd-theme-moon');
|
|
25
|
+
|
|
26
|
+
function update() {
|
|
27
|
+
const isDark = document.documentElement.classList.contains('dark');
|
|
28
|
+
if (sun) sun.style.display = isDark ? 'block' : 'none';
|
|
29
|
+
if (moon) moon.style.display = isDark ? 'none' : 'block';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
update();
|
|
33
|
+
|
|
34
|
+
btn?.addEventListener('click', () => {
|
|
35
|
+
const isDark = document.documentElement.classList.contains('dark');
|
|
36
|
+
const next = isDark ? 'light' : 'dark';
|
|
37
|
+
document.documentElement.classList.remove('light', 'dark');
|
|
38
|
+
document.documentElement.classList.add(next);
|
|
39
|
+
document.cookie = `theme=${next};path=/;max-age=31536000`;
|
|
40
|
+
update();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
initThemeToggle();
|
|
45
|
+
document.addEventListener('astro:after-swap', initThemeToggle);
|
|
46
|
+
</script>
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DocsTheme } from "@farming-labs/docs";
|
|
2
|
+
|
|
3
|
+
export declare const fumadocs: (overrides?: Partial<DocsTheme>) => DocsTheme;
|
|
4
|
+
export declare const DefaultUIDefaults: Record<string, any>;
|
|
5
|
+
|
|
6
|
+
export declare const pixelBorder: (overrides?: Partial<DocsTheme>) => DocsTheme;
|
|
7
|
+
export declare const PixelBorderUIDefaults: Record<string, any>;
|
|
8
|
+
|
|
9
|
+
export declare const darksharp: (overrides?: Partial<DocsTheme>) => DocsTheme;
|
|
10
|
+
export declare const DarksharpUIDefaults: Record<string, any>;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @farming-labs/astro-theme
|
|
3
|
+
*
|
|
4
|
+
* Astro UI components and theme presets for documentation sites.
|
|
5
|
+
* Port of @farming-labs/theme for Astro.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { fumadocs, DefaultUIDefaults } from "./themes/default.js";
|
|
9
|
+
export { pixelBorder, PixelBorderUIDefaults } from "./themes/pixel-border.js";
|
|
10
|
+
export { darksharp, DarksharpUIDefaults } from "./themes/darksharp.js";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown renderer for AI chat — exact port of the Next.js fumadocs version.
|
|
3
|
+
*
|
|
4
|
+
* Uses sugar-high for syntax highlighting in code blocks.
|
|
5
|
+
* Supports: fenced code blocks, tables, inline code, bold, italic,
|
|
6
|
+
* links, headings, bullet lists, numbered lists.
|
|
7
|
+
*/
|
|
8
|
+
import { highlight } from "sugar-high";
|
|
9
|
+
|
|
10
|
+
function escapeHtml(s) {
|
|
11
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function buildCodeBlock(lang, code) {
|
|
15
|
+
const trimmed = code.replace(/\n$/, "");
|
|
16
|
+
const highlighted = highlight(trimmed).replace(/<\/span>\n<span/g, "</span><span");
|
|
17
|
+
const langLabel = lang ? `<div class="fd-ai-code-lang">${escapeHtml(lang)}</div>` : "";
|
|
18
|
+
const copyBtn = `<button class="fd-ai-code-copy" onclick="(function(btn){var code=btn.closest('.fd-ai-code-block').querySelector('code').textContent;navigator.clipboard.writeText(code).then(function(){btn.textContent='Copied!';setTimeout(function(){btn.textContent='Copy'},1500)})})(this)">Copy</button>`;
|
|
19
|
+
return `<div class="fd-ai-code-block">`
|
|
20
|
+
+ `<div class="fd-ai-code-header">${langLabel}${copyBtn}</div>`
|
|
21
|
+
+ `<pre><code>${highlighted}</code></pre>`
|
|
22
|
+
+ `</div>`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isTableRow(line) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isTableSeparator(line) {
|
|
31
|
+
return /^\|[\s:]*-+[\s:]*(\|[\s:]*-+[\s:]*)*\|$/.test(line.trim());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderTable(rows) {
|
|
35
|
+
const parseRow = (row) =>
|
|
36
|
+
row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map(c => c.trim());
|
|
37
|
+
|
|
38
|
+
const headerCells = parseRow(rows[0]);
|
|
39
|
+
const thead = `<thead><tr>${headerCells.map(c => `<th>${c}</th>`).join("")}</tr></thead>`;
|
|
40
|
+
|
|
41
|
+
const bodyRows = rows.slice(1).map(row => {
|
|
42
|
+
const cells = parseRow(row);
|
|
43
|
+
return `<tr>${cells.map(c => `<td>${c}</td>`).join("")}</tr>`;
|
|
44
|
+
}).join("");
|
|
45
|
+
|
|
46
|
+
return `<table>${thead}<tbody>${bodyRows}</tbody></table>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function renderMarkdown(text) {
|
|
50
|
+
if (!text) return "";
|
|
51
|
+
|
|
52
|
+
const codeBlocks = [];
|
|
53
|
+
|
|
54
|
+
// Complete fences: ```lang\n...\n```
|
|
55
|
+
let processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
|
|
56
|
+
codeBlocks.push(buildCodeBlock(lang, code));
|
|
57
|
+
return `\x00CB${codeBlocks.length - 1}\x00`;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Incomplete fence (streaming): opening ``` with no closing one
|
|
61
|
+
processed = processed.replace(/```(\w*)\n([\s\S]*)$/, (_match, lang, code) => {
|
|
62
|
+
codeBlocks.push(buildCodeBlock(lang, code));
|
|
63
|
+
return `\x00CB${codeBlocks.length - 1}\x00`;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const lines = processed.split("\n");
|
|
67
|
+
const output = [];
|
|
68
|
+
let i = 0;
|
|
69
|
+
|
|
70
|
+
while (i < lines.length) {
|
|
71
|
+
if (isTableRow(lines[i]) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
|
|
72
|
+
const tableLines = [lines[i]];
|
|
73
|
+
i++; // separator
|
|
74
|
+
i++; // move past separator
|
|
75
|
+
while (i < lines.length && isTableRow(lines[i])) {
|
|
76
|
+
tableLines.push(lines[i]);
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
output.push(renderTable(tableLines));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
output.push(lines[i]);
|
|
83
|
+
i++;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let result = output.join("\n");
|
|
87
|
+
|
|
88
|
+
result = result
|
|
89
|
+
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
90
|
+
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
|
91
|
+
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<em>$1</em>")
|
|
92
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
|
93
|
+
.replace(/^### (.*$)/gm, "<h4>$1</h4>")
|
|
94
|
+
.replace(/^## (.*$)/gm, "<h3>$1</h3>")
|
|
95
|
+
.replace(/^# (.*$)/gm, "<h2>$1</h2>")
|
|
96
|
+
.replace(
|
|
97
|
+
/^[-*] (.*$)/gm,
|
|
98
|
+
'<div style="display:flex;gap:8px;padding:2px 0"><span style="opacity:0.5">\u2022</span><span>$1</span></div>',
|
|
99
|
+
)
|
|
100
|
+
.replace(
|
|
101
|
+
/^(\d+)\. (.*$)/gm,
|
|
102
|
+
'<div style="display:flex;gap:8px;padding:2px 0"><span style="opacity:0.5">$1.</span><span>$2</span></div>',
|
|
103
|
+
)
|
|
104
|
+
.replace(/\n\n/g, '<div style="height:8px"></div>')
|
|
105
|
+
.replace(/\n/g, "<br>");
|
|
106
|
+
|
|
107
|
+
result = result.replace(/\x00CB(\d+)\x00/g, (_m, idx) => codeBlocks[Number(idx)]);
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createTheme } from "@farming-labs/docs";
|
|
2
|
+
|
|
3
|
+
const DarksharpUIDefaults = {
|
|
4
|
+
colors: {
|
|
5
|
+
primary: "#fafaf9",
|
|
6
|
+
background: "#000000",
|
|
7
|
+
muted: "#a8a29e",
|
|
8
|
+
border: "#292524",
|
|
9
|
+
},
|
|
10
|
+
typography: {
|
|
11
|
+
font: {
|
|
12
|
+
style: {
|
|
13
|
+
sans: "Geist, system-ui, sans-serif",
|
|
14
|
+
mono: "Geist Mono, monospace",
|
|
15
|
+
},
|
|
16
|
+
h1: { size: "2rem", weight: 700, lineHeight: "1.2", letterSpacing: "-0.02em" },
|
|
17
|
+
h2: { size: "1.5rem", weight: 600, lineHeight: "1.3" },
|
|
18
|
+
h3: { size: "1.25rem", weight: 600, lineHeight: "1.4" },
|
|
19
|
+
h4: { size: "1.125rem", weight: 600, lineHeight: "1.4" },
|
|
20
|
+
body: { size: "1rem", weight: 400, lineHeight: "1.75" },
|
|
21
|
+
small: { size: "0.875rem", weight: 400, lineHeight: "1.5" },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
layout: {
|
|
25
|
+
contentWidth: 768,
|
|
26
|
+
sidebarWidth: 280,
|
|
27
|
+
toc: { enabled: true, depth: 3 },
|
|
28
|
+
header: { height: 56, sticky: true },
|
|
29
|
+
},
|
|
30
|
+
components: {
|
|
31
|
+
Callout: { variant: "soft", icon: true },
|
|
32
|
+
CodeBlock: { showCopyButton: true },
|
|
33
|
+
Tabs: { style: "default" },
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const darksharp = createTheme({
|
|
38
|
+
name: "fumadocs-darksharp",
|
|
39
|
+
ui: DarksharpUIDefaults,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export { DarksharpUIDefaults };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createTheme } from "@farming-labs/docs";
|
|
2
|
+
|
|
3
|
+
const DefaultUIDefaults = {
|
|
4
|
+
colors: {
|
|
5
|
+
primary: "#6366f1",
|
|
6
|
+
background: "#ffffff",
|
|
7
|
+
muted: "#64748b",
|
|
8
|
+
border: "#e5e7eb",
|
|
9
|
+
},
|
|
10
|
+
typography: {
|
|
11
|
+
font: {
|
|
12
|
+
style: {
|
|
13
|
+
sans: "Inter, system-ui, sans-serif",
|
|
14
|
+
mono: "JetBrains Mono, monospace",
|
|
15
|
+
},
|
|
16
|
+
h1: { size: "2rem", weight: 700, lineHeight: "1.2", letterSpacing: "-0.02em" },
|
|
17
|
+
h2: { size: "1.5rem", weight: 600, lineHeight: "1.3" },
|
|
18
|
+
h3: { size: "1.25rem", weight: 600, lineHeight: "1.4" },
|
|
19
|
+
h4: { size: "1.125rem", weight: 600, lineHeight: "1.4" },
|
|
20
|
+
body: { size: "1rem", weight: 400, lineHeight: "1.75" },
|
|
21
|
+
small: { size: "0.875rem", weight: 400, lineHeight: "1.5" },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
layout: {
|
|
25
|
+
contentWidth: 768,
|
|
26
|
+
sidebarWidth: 280,
|
|
27
|
+
toc: { enabled: true, depth: 3 },
|
|
28
|
+
header: { height: 72, sticky: true },
|
|
29
|
+
},
|
|
30
|
+
components: {
|
|
31
|
+
Callout: { variant: "soft", icon: true },
|
|
32
|
+
CodeBlock: { showCopyButton: true },
|
|
33
|
+
Tabs: { style: "default" },
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const fumadocs = createTheme({
|
|
38
|
+
name: "fumadocs-default",
|
|
39
|
+
ui: DefaultUIDefaults,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export { DefaultUIDefaults };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createTheme } from "@farming-labs/docs";
|
|
2
|
+
|
|
3
|
+
const PixelBorderUIDefaults = {
|
|
4
|
+
colors: {
|
|
5
|
+
primary: "oklch(0.985 0.001 106.423)",
|
|
6
|
+
background: "hsl(0 0% 2%)",
|
|
7
|
+
muted: "hsl(0 0% 55%)",
|
|
8
|
+
border: "hsl(0 0% 15%)",
|
|
9
|
+
},
|
|
10
|
+
typography: {
|
|
11
|
+
font: {
|
|
12
|
+
style: {
|
|
13
|
+
sans: "system-ui, -apple-system, sans-serif",
|
|
14
|
+
mono: "ui-monospace, monospace",
|
|
15
|
+
},
|
|
16
|
+
h1: { size: "2.25rem", weight: 700, lineHeight: "1.2", letterSpacing: "-0.02em" },
|
|
17
|
+
h2: { size: "1.5rem", weight: 600, lineHeight: "1.3", letterSpacing: "-0.01em" },
|
|
18
|
+
h3: { size: "1.25rem", weight: 600, lineHeight: "1.4" },
|
|
19
|
+
h4: { size: "1.125rem", weight: 600, lineHeight: "1.4" },
|
|
20
|
+
body: { size: "1rem", weight: 400, lineHeight: "1.75" },
|
|
21
|
+
small: { size: "0.875rem", weight: 400, lineHeight: "1.5" },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
layout: {
|
|
25
|
+
contentWidth: 860,
|
|
26
|
+
sidebarWidth: 286,
|
|
27
|
+
toc: { enabled: true, depth: 3 },
|
|
28
|
+
header: { height: 56, sticky: true },
|
|
29
|
+
},
|
|
30
|
+
components: {},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const pixelBorder = createTheme({
|
|
34
|
+
name: "fumadocs-pixel-border",
|
|
35
|
+
ui: PixelBorderUIDefaults,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export { PixelBorderUIDefaults };
|