mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-14 14:08:18 +00:00
Migrate docs to ReallySimpleDocs
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
---
|
||||
const { site } = Astro.props;
|
||||
const logo = site.logo?.svg || '';
|
||||
---
|
||||
|
||||
<a href="/" class="btn-ghost h-12 w-full justify-start p-2">
|
||||
<div class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||
{logo && <Fragment set:html={logo} />}
|
||||
</div>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{site.title}</span>
|
||||
{site.subtitle && <span class="truncate text-xs text-muted-foreground">{site.subtitle}</span>}
|
||||
</div>
|
||||
</a>
|
||||
@@ -0,0 +1,39 @@
|
||||
@import "../docs/css/fonts.css";
|
||||
@import "../docs/css/themes/claude.css";
|
||||
@import "../docs/css/themes/doom-64.css";
|
||||
@import "../docs/css/themes/supabase.css";
|
||||
|
||||
:root {
|
||||
--font-sans: "Geist Sans", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-heading: "Geist Sans", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
.preview {
|
||||
@apply flex min-h-[350px] w-full items-center justify-center p-10;
|
||||
}
|
||||
|
||||
.prose {
|
||||
> .relative.my-6 {
|
||||
@apply my-6;
|
||||
}
|
||||
|
||||
pre.scrollbar {
|
||||
@apply grid max-h-[650px] overflow-y-auto rounded-lg bg-muted/40 text-sm;
|
||||
}
|
||||
|
||||
pre.scrollbar > code {
|
||||
@apply block rounded-[inherit] bg-transparent p-3.5 font-mono;
|
||||
}
|
||||
}
|
||||
|
||||
.htmx-indicator {
|
||||
@apply invisible opacity-0 transition-discrete transition-all;
|
||||
}
|
||||
.htmx-indicator:not(.htmx-request) {
|
||||
@apply hidden;
|
||||
}
|
||||
.htmx-request,
|
||||
.htmx-request.htmx-indicator {
|
||||
@apply visible opacity-100;
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { collectMenuSlugs, docUrl, fallbackLabelFromSlug } from './menu.js';
|
||||
|
||||
export const registerLlmExports = (eleventyConfig, options) => {
|
||||
const collectFiles = async (dir, predicate) => {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
const out = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...(await collectFiles(fullPath, predicate)));
|
||||
else if (entry.isFile() && predicate(fullPath)) out.push(fullPath);
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
const writeTextFile = async (outputDir, relativePath, content) => {
|
||||
const outPath = path.join(outputDir, relativePath);
|
||||
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
||||
await fs.writeFile(outPath, content, 'utf8');
|
||||
};
|
||||
|
||||
const stripFrontMatter = (source) => {
|
||||
if (!source.startsWith('---')) return source;
|
||||
const match = source.match(/^---\s*\n[\s\S]*?\n---\s*\n?/);
|
||||
return match ? source.slice(match[0].length) : source;
|
||||
};
|
||||
|
||||
const readFrontMatterField = (source, key) => {
|
||||
if (!source.startsWith('---')) return null;
|
||||
const match = source.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
||||
if (!match) return null;
|
||||
const line = match[1]
|
||||
.split('\n')
|
||||
.map((l) => l.trim())
|
||||
.find((l) => l.startsWith(`${key}:`));
|
||||
if (!line) return null;
|
||||
let value = line.slice(`${key}:`.length).trim();
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
return value || null;
|
||||
};
|
||||
|
||||
eleventyConfig.on('afterBuild', async (eventsArg) => {
|
||||
const outputDir = eventsArg?.directories?.output || eventsArg?.dir?.output;
|
||||
const inputDir = eventsArg?.directories?.input || eventsArg?.dir?.input;
|
||||
if (!outputDir || !inputDir) return;
|
||||
|
||||
const docsRoot = path.resolve(inputDir);
|
||||
const siteUrl = options.getSiteUrl();
|
||||
const siteTitle = options.getSiteTitle();
|
||||
|
||||
let docsConfig = null;
|
||||
for (const docsConfigPath of [path.join(docsRoot, '_data', 'docs.json'), path.join(docsRoot, 'docs.json')]) {
|
||||
try {
|
||||
docsConfig = JSON.parse(await fs.readFile(docsConfigPath, 'utf8'));
|
||||
break;
|
||||
} catch (_) {}
|
||||
}
|
||||
const menu = docsConfig?.menu || [];
|
||||
const menuSlugs = collectMenuSlugs(menu);
|
||||
|
||||
const sourceFiles = await collectFiles(docsRoot, (filePath) => filePath.endsWith('.md'));
|
||||
const docs = (
|
||||
await Promise.all(
|
||||
sourceFiles.map(async (filePath) => {
|
||||
const source = await fs.readFile(filePath, 'utf8');
|
||||
const relative = path.relative(docsRoot, filePath).replaceAll(path.sep, '/');
|
||||
const slug = relative.replace(/\.md$/, '') === 'index' ? 'index' : relative.replace(/\.md$/, '');
|
||||
const url = docUrl(slug);
|
||||
const title = readFrontMatterField(source, 'title') || fallbackLabelFromSlug(slug);
|
||||
const description = (readFrontMatterField(source, 'description') || '').trim();
|
||||
const content = stripFrontMatter(source).trim();
|
||||
return { slug, url, title: title.trim(), description, content };
|
||||
}),
|
||||
)
|
||||
).sort((a, b) => a.url.localeCompare(b.url));
|
||||
|
||||
const docsBySlug = new Map(docs.map((d) => [d.slug, d]));
|
||||
const docsInMenu = menuSlugs.map((slug) => docsBySlug.get(slug)).filter(Boolean);
|
||||
const docsNotInMenu = docs.filter((doc) => !docsInMenu.includes(doc));
|
||||
const orderedDocs = [...docsInMenu, ...docsNotInMenu];
|
||||
|
||||
const markdownExportPathFromSlug = (slug) => {
|
||||
if (slug === 'index') return 'index.md';
|
||||
if (slug.endsWith('/index')) return `${slug.slice(0, -'/index'.length)}.md`;
|
||||
return `${slug}.md`;
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
docs.map(async (doc) => {
|
||||
const parts = [`# ${doc.title}`];
|
||||
if (doc.description) parts.push(doc.description);
|
||||
if (doc.content) parts.push(doc.content);
|
||||
await writeTextFile(outputDir, markdownExportPathFromSlug(doc.slug), parts.join('\n\n') + '\n');
|
||||
}),
|
||||
);
|
||||
|
||||
const llmsLines = [`# ${siteTitle}`, '', '## Docs', ''];
|
||||
menu.forEach((group) => {
|
||||
if (group?.type !== 'group') return;
|
||||
llmsLines.push(`### ${group.label || 'Docs'}`, '');
|
||||
const addSlug = (slug) => {
|
||||
const doc = docsBySlug.get(slug);
|
||||
if (!doc) return;
|
||||
const fullUrl = `${siteUrl}${doc.url}`;
|
||||
llmsLines.push(doc.description ? `- [${doc.title}](${fullUrl}): ${doc.description}` : `- [${doc.title}](${fullUrl})`);
|
||||
};
|
||||
(group.items || []).forEach((item) => {
|
||||
if (typeof item === 'string') addSlug(item);
|
||||
else if (item?.type === 'submenu' && Array.isArray(item.items)) item.items.forEach(addSlug);
|
||||
});
|
||||
llmsLines.push('');
|
||||
});
|
||||
await writeTextFile(outputDir, 'llms.txt', llmsLines.join('\n'));
|
||||
|
||||
const fullLines = [];
|
||||
orderedDocs.forEach((doc) => {
|
||||
fullLines.push(`# ${doc.title}`, `Source: ${siteUrl}${doc.url}`, '');
|
||||
if (doc.description) fullLines.push(doc.description, '');
|
||||
fullLines.push(doc.content, '', '---', '');
|
||||
});
|
||||
await writeTextFile(outputDir, 'llms-full.txt', fullLines.join('\n'));
|
||||
});
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
export const docUrl = (slug) => {
|
||||
if (slug === 'index') return '/';
|
||||
if (slug.endsWith('/index')) return '/' + slug.replace(/\/index$/, '/');
|
||||
return '/' + slug + '/';
|
||||
};
|
||||
|
||||
export const flattenMenuSlugs = (menu) => {
|
||||
const out = [];
|
||||
|
||||
const walk = (items) => {
|
||||
(items || []).forEach((item) => {
|
||||
if (typeof item === 'string') {
|
||||
out.push(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item?.type === 'submenu' && Array.isArray(item.items)) {
|
||||
walk(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
(menu || []).forEach((group) => {
|
||||
if (group?.type !== 'group') return;
|
||||
walk(group.items);
|
||||
});
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
export const collectMenuSlugs = (menu) => {
|
||||
return [...new Set(flattenMenuSlugs(menu))];
|
||||
};
|
||||
|
||||
export const fallbackLabelFromSlug = (slug) => {
|
||||
if (slug === 'index') return 'Introduction';
|
||||
const parts = String(slug).split('/');
|
||||
const last = parts[parts.length - 1];
|
||||
const base = last === 'index' && parts.length > 1 ? parts[parts.length - 2] : last;
|
||||
return base.replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
};
|
||||
@@ -1,152 +0,0 @@
|
||||
import { docUrl, fallbackLabelFromSlug, flattenMenuSlugs } from './menu.js';
|
||||
|
||||
export const registerNavigationFilters = (eleventyConfig, lucideIcons) => {
|
||||
const brandIcons = {
|
||||
github:
|
||||
'<svg aria-hidden="true" class="lucide lucide-github" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>',
|
||||
discord:
|
||||
'<svg aria-hidden="true" class="lucide lucide-discord" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>',
|
||||
};
|
||||
|
||||
const lucideSvg = (iconName) => {
|
||||
if (!iconName) return null;
|
||||
const pascalName = iconName
|
||||
.split('-')
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join('');
|
||||
const iconComponent = lucideIcons?.[pascalName];
|
||||
if (!iconComponent) return null;
|
||||
return iconComponent;
|
||||
};
|
||||
|
||||
const resolveIcon = (icon) => {
|
||||
if (!icon) return null;
|
||||
if (typeof icon !== 'string') return icon;
|
||||
const trimmed = icon.trim();
|
||||
if (!trimmed) return null;
|
||||
if (trimmed.startsWith('<svg')) return trimmed;
|
||||
if (brandIcons[trimmed]) return brandIcons[trimmed];
|
||||
return lucideSvg(trimmed) || null;
|
||||
};
|
||||
|
||||
const docsForCollections = (collections) => collections?.all || collections?.docs || [];
|
||||
|
||||
const buildMenus = (menu, collections, currentUrl = '') => {
|
||||
const docs = docsForCollections(collections);
|
||||
const docsByUrl = new Map(docs.map((doc) => [doc.url, doc]));
|
||||
|
||||
const processSlug = (slug) => {
|
||||
const url = docUrl(slug);
|
||||
const doc = docsByUrl.get(url);
|
||||
const title = doc?.data?.title ? String(doc.data.title) : fallbackLabelFromSlug(slug);
|
||||
const description = doc?.data?.description ? String(doc.data.description) : '';
|
||||
const iconSvg = resolveIcon(doc?.data?.icon) || null;
|
||||
|
||||
const sidebarItem = {
|
||||
icon: iconSvg,
|
||||
url,
|
||||
label: title,
|
||||
description,
|
||||
current: url.replace(/\/$/, '') === String(currentUrl).replace(/\/$/, ''),
|
||||
};
|
||||
|
||||
const keywords = [title, description].filter(Boolean).join(' ');
|
||||
const iconHtml = iconSvg || '';
|
||||
const commandItem = {
|
||||
type: 'item',
|
||||
label: `${iconHtml}<span>${title}</span>`,
|
||||
keywords,
|
||||
attrs: {
|
||||
onclick: `window.location.href='${url}'; this.closest('dialog')?.close();`,
|
||||
},
|
||||
};
|
||||
|
||||
return { sidebar: sidebarItem, command: commandItem };
|
||||
};
|
||||
|
||||
const processItem = (item) => {
|
||||
if (typeof item === 'string') return processSlug(item);
|
||||
|
||||
if (item?.type === 'submenu') {
|
||||
const submenuIcon = resolveIcon(item.icon) || null;
|
||||
const processed = (item.items || []).map((sub) => processItem(sub));
|
||||
const commandItems = processed.map((entry) => entry.command).filter(Boolean);
|
||||
return {
|
||||
sidebar: {
|
||||
...item,
|
||||
icon: submenuIcon,
|
||||
open: item.open ?? processed.some((entry) => entry.sidebar?.current),
|
||||
items: processed.map((entry) => entry.sidebar).filter(Boolean),
|
||||
},
|
||||
command: commandItems.length ? {
|
||||
type: 'group',
|
||||
label: item.label,
|
||||
items: commandItems,
|
||||
} : null,
|
||||
};
|
||||
}
|
||||
|
||||
if (item?.url) {
|
||||
const iconSvg = resolveIcon(item.icon) || null;
|
||||
return {
|
||||
sidebar: { ...item, icon: iconSvg, current: item.url === currentUrl },
|
||||
command: item.external
|
||||
? null
|
||||
: {
|
||||
type: 'item',
|
||||
label: `${iconSvg || ''}<span>${item.label}</span>`,
|
||||
attrs: { onclick: `window.location.href='${item.url}'; this.closest('dialog')?.close();` },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { sidebar: null, command: null };
|
||||
};
|
||||
|
||||
const processedGroups = (menu || []).map((group) => {
|
||||
if (group?.type !== 'group') return { sidebar: group, command: group };
|
||||
|
||||
const processed = (group.items || []).map((item) => processItem(item));
|
||||
return {
|
||||
sidebar: { ...group, items: processed.map((entry) => entry.sidebar).filter(Boolean) },
|
||||
command: { type: 'group', label: group.label, items: processed.map((entry) => entry.command).filter(Boolean) },
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
sidebar: processedGroups.map((p) => p.sidebar),
|
||||
command: processedGroups.map((p) => p.command),
|
||||
};
|
||||
};
|
||||
|
||||
eleventyConfig.addFilter('getNavigation', function getNavigation(menu, collections) {
|
||||
const flattened = flattenMenuSlugs(menu);
|
||||
const currentUrl = this.page.url;
|
||||
const index = flattened.findIndex((slug) => docUrl(slug) === currentUrl);
|
||||
if (index === -1) return { prev: null, next: null };
|
||||
|
||||
const docs = docsForCollections(collections);
|
||||
const labelFor = (slug) => {
|
||||
const url = docUrl(slug);
|
||||
const doc = docs.find((d) => d.url === url);
|
||||
if (doc?.data?.title) return String(doc.data.title);
|
||||
return fallbackLabelFromSlug(slug);
|
||||
};
|
||||
|
||||
return {
|
||||
prev: index > 0 ? { url: docUrl(flattened[index - 1]), label: labelFor(flattened[index - 1]) } : null,
|
||||
next:
|
||||
index < flattened.length - 1
|
||||
? { url: docUrl(flattened[index + 1]), label: labelFor(flattened[index + 1]) }
|
||||
: null,
|
||||
};
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter('sidebarMenu', function sidebarMenu(menu, collections) {
|
||||
return buildMenus(menu, collections, this.page.url).sidebar;
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter('commandMenu', function commandMenu(menu, collections) {
|
||||
return buildMenus(menu, collections, this.page.url).command;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const types = ['success', 'error', 'info', 'warning'];
|
||||
|
||||
export function getStaticPaths() {
|
||||
return types.map((type) => ({ params: { type } }));
|
||||
}
|
||||
|
||||
export function GET({ params }) {
|
||||
const type = String(params.type || '');
|
||||
if (!types.includes(type)) return new Response('Not found', { status: 404 });
|
||||
const html = fs.readFileSync(path.resolve('docs/generated/fragments/toast', `${type}.html`), 'utf8');
|
||||
return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8' } });
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export function GET({ site }) {
|
||||
const origin = site?.toString().replace(/\/$/, '') || 'https://basecoatui.com';
|
||||
return new Response(`User-agent: *\nAllow: /\n\nSitemap: ${origin}/sitemap.xml\n`, {
|
||||
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const routePath = (slug) => slug === 'index' ? '/' : `/${slug.replace(/^\/+|\/+$/g, '')}/`;
|
||||
|
||||
function flatten(menu) {
|
||||
const out = [];
|
||||
const walk = (items = []) => {
|
||||
for (const item of items) {
|
||||
if (typeof item === 'string') out.push(item);
|
||||
else if (item?.slug) out.push(item.slug);
|
||||
else if (item?.type === 'submenu') walk(item.items || []);
|
||||
}
|
||||
};
|
||||
for (const group of menu || []) if (group?.type === 'group') walk(group.items || []);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function GET({ site }) {
|
||||
const origin = site?.toString().replace(/\/$/, '') || 'https://basecoatui.com';
|
||||
const manifest = JSON.parse(fs.readFileSync(path.resolve('docs/generated/docs.json'), 'utf8'));
|
||||
const urls = flatten(manifest.menu).map((slug) => ` <url><loc>${origin}${routePath(slug)}</loc></url>`).join('\n');
|
||||
return new Response(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`, {
|
||||
headers: { 'content-type': 'application/xml; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user