mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-12 13:18:22 +00:00
Migrate docs to ReallySimpleDocs
This commit is contained in:
-120
@@ -1,120 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import eleventyLucideicons from '@grimlink/eleventy-plugin-lucide-icons';
|
||||
import Shiki from '@shikijs/markdown-it';
|
||||
import pluginTOC from 'eleventy-plugin-toc';
|
||||
import * as lucideIcons from 'lucide-static';
|
||||
import markdownIt from 'markdown-it';
|
||||
import markdownItAnchor from 'markdown-it-anchor';
|
||||
import Nunjucks from 'nunjucks';
|
||||
import prettier from 'prettier';
|
||||
|
||||
import { registerLlmExports } from './src/eleventy/llm-exports.js';
|
||||
import { registerNavigationFilters } from './src/eleventy/navigation.js';
|
||||
|
||||
export default async function(eleventyConfig) {
|
||||
eleventyConfig.addWatchTarget('src/nunjucks/');
|
||||
eleventyConfig.addPassthroughCopy({ 'docs/src/assets': 'assets' });
|
||||
eleventyConfig.addPassthroughCopy({ 'src/js': 'assets/js' });
|
||||
eleventyConfig.addPlugin(eleventyLucideicons);
|
||||
eleventyConfig.addPlugin(pluginTOC);
|
||||
|
||||
const isServe = process.argv.includes('--serve');
|
||||
const siteUrl = process.env.SITE_URL || (isServe ? '' : 'https://basecoatui.com');
|
||||
eleventyConfig.addGlobalData('siteUrl', siteUrl);
|
||||
|
||||
const shiki = await Shiki({
|
||||
themes: {
|
||||
light: 'github-light-default',
|
||||
dark: 'github-dark-default',
|
||||
},
|
||||
defaultColor: false,
|
||||
colorReplacements: {
|
||||
'github-dark-default': {
|
||||
'#24292e': 'oklch(0.145 0 0)',
|
||||
},
|
||||
},
|
||||
langAlias: {
|
||||
njk: 'html',
|
||||
jinja: 'html',
|
||||
},
|
||||
fallbackLanguage: 'text',
|
||||
});
|
||||
|
||||
const markdown = markdownIt({
|
||||
html: true,
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
})
|
||||
.use(markdownItAnchor, {
|
||||
permalink: markdownItAnchor.permalink.headerLink(),
|
||||
})
|
||||
.use(shiki);
|
||||
|
||||
const defaultTableOpen = markdown.renderer.rules.table_open;
|
||||
const defaultTableClose = markdown.renderer.rules.table_close;
|
||||
markdown.renderer.rules.table_open = (tokens, idx, mdOptions, env, self) => {
|
||||
return '<div class="relative w-full overflow-auto my-6"><table>' + (defaultTableOpen ? defaultTableOpen(tokens, idx, mdOptions, env, self) : '');
|
||||
};
|
||||
markdown.renderer.rules.table_close = (tokens, idx, mdOptions, env, self) => {
|
||||
return (defaultTableClose ? defaultTableClose(tokens, idx, mdOptions, env, self) : '') + '</table></div>';
|
||||
};
|
||||
eleventyConfig.setLibrary('md', markdown);
|
||||
|
||||
eleventyConfig.addFilter('markdownUrl', function markdownUrl(pageUrl) {
|
||||
if (pageUrl === '/') return '/index.md';
|
||||
return pageUrl.replace(/\/$/, '') + '.md';
|
||||
});
|
||||
|
||||
registerNavigationFilters(eleventyConfig, lucideIcons);
|
||||
registerLlmExports(eleventyConfig, {
|
||||
getSiteUrl: () => siteUrl,
|
||||
getSiteTitle: () => 'Basecoat',
|
||||
});
|
||||
|
||||
eleventyConfig.addShortcode('fetchCode', function(filePath) {
|
||||
const absolutePath = path.resolve(process.cwd(), filePath);
|
||||
try {
|
||||
return fs.readFileSync(absolutePath, 'utf8');
|
||||
} catch (error) {
|
||||
console.error(`[Eleventy FetchCode Error] Failed to read: ${absolutePath}`, error);
|
||||
return `<!-- Error fetching code for ${filePath} -->`;
|
||||
}
|
||||
});
|
||||
|
||||
eleventyConfig.addNunjucksAsyncFilter('prettyHtml', async (code, callback) => {
|
||||
try {
|
||||
const formattedCode = await prettier.format(code, {
|
||||
parser: 'html',
|
||||
printWidth: 1000,
|
||||
singleAttributePerLine: false,
|
||||
bracketSameLine: true,
|
||||
htmlWhitespaceSensitivity: 'ignore',
|
||||
});
|
||||
callback(null, formattedCode);
|
||||
} catch (err) {
|
||||
console.error('Error formatting HTML with Prettier:', err);
|
||||
callback(err, code);
|
||||
}
|
||||
});
|
||||
|
||||
const nunjucksEnvironment = new Nunjucks.Environment(
|
||||
new Nunjucks.FileSystemLoader([
|
||||
'src/nunjucks',
|
||||
'docs/src/_includes',
|
||||
]),
|
||||
{ autoescape: true },
|
||||
);
|
||||
eleventyConfig.setLibrary('njk', nunjucksEnvironment);
|
||||
|
||||
return {
|
||||
dir: {
|
||||
input: 'docs/src',
|
||||
output: '_site',
|
||||
includes: '_includes',
|
||||
data: '_data',
|
||||
},
|
||||
markdownTemplateEngine: 'njk',
|
||||
};
|
||||
}
|
||||
+7
-3
@@ -1,12 +1,16 @@
|
||||
.*
|
||||
!.gitignore
|
||||
!.gitattributes
|
||||
!.eleventy.js
|
||||
|
||||
_site/
|
||||
|
||||
node_modules/
|
||||
packages/cli/dist/
|
||||
packages/css/dist/
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
npm-debug.log*
|
||||
dist/
|
||||
docs/generated/
|
||||
public/assets/
|
||||
|
||||
public/fragments/
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
## Source Boundaries
|
||||
- Do not hand-edit build outputs:
|
||||
- `packages/*/dist/**`
|
||||
- `_site/**`
|
||||
- `dist/**`
|
||||
- `docs/generated/**`
|
||||
- `public/assets/**`
|
||||
- Generated source entrypoints such as `src/css/basecoat-vega.css` are committed, but must be regenerated through `scripts/generate-css-entrypoints.js` or build scripts, not manually maintained.
|
||||
|
||||
## Public API and Markup
|
||||
|
||||
@@ -65,16 +65,20 @@ Use the styleless base plus a custom style file:
|
||||
```text
|
||||
.
|
||||
├── docs/
|
||||
│ └── src/ Eleventy documentation site
|
||||
│ ├── generated/ Generated RSD/Astro docs input
|
||||
│ └── src/ Documentation source pages, examples, and fragments
|
||||
├── packages/
|
||||
│ ├── cli/ Published CLI package
|
||||
│ └── css/ Published CSS package
|
||||
├── public/ Generated docs assets for Astro
|
||||
├── scripts/ Build and generation scripts
|
||||
└── src/
|
||||
├── components/ Astro overrides for ReallySimpleDocs
|
||||
├── css/
|
||||
│ ├── base/ Shared tokens, base layer, and semantic utilities
|
||||
│ ├── components/ Component structure and behavior hooks
|
||||
│ └── styles/ Style-pack visual rules
|
||||
├── docs.css Documentation-site CSS extension
|
||||
├── jinja/ Jinja component macros
|
||||
├── js/ Vanilla JS components and registry
|
||||
└── nunjucks/ Nunjucks component macros
|
||||
@@ -108,22 +112,22 @@ These entrypoints are generated by `scripts/generate-css-entrypoints.js` and by
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
# Install dependencies.
|
||||
npm i
|
||||
|
||||
# Run the docs site
|
||||
# Generate docs input and run the ReallySimpleDocs/Astro site.
|
||||
npm run docs:dev
|
||||
|
||||
# Build package assets
|
||||
# Build package assets.
|
||||
npm run build
|
||||
|
||||
# Build the static docs site
|
||||
# Build the static docs site.
|
||||
npm run docs:build
|
||||
|
||||
# Run the Workers docs site locally
|
||||
# Run the Workers docs site locally.
|
||||
npm run workers:dev
|
||||
|
||||
# Deploy the Workers docs site
|
||||
# Deploy the Workers docs site.
|
||||
npm run workers:deploy
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import reallySimpleDocs from 'reallysimpledocs/astro';
|
||||
|
||||
export default defineConfig({
|
||||
site: process.env.SITE_URL || 'https://basecoatui.com',
|
||||
integrations: [
|
||||
reallySimpleDocs({
|
||||
docsDir: './docs/generated',
|
||||
routeBase: '/',
|
||||
style: 'nova',
|
||||
customCss: ['./src/docs.css'],
|
||||
components: {
|
||||
SidebarHeader: './src/components/SidebarHeader.astro',
|
||||
},
|
||||
site: {
|
||||
title: 'Basecoat',
|
||||
subtitle: 'v1.0.0-beta.1',
|
||||
description: 'A components library built with Tailwind CSS that works with any web stack.',
|
||||
url: process.env.SITE_URL || 'https://basecoatui.com',
|
||||
keywords: ['components', 'component library', 'component system', 'UI', 'UI kit', 'shadcn', 'shadcn/ui', 'Tailwind CSS', 'Tailwind', 'CSS', 'HTML', 'Jinja', 'Nunjucks', 'JS', 'JavaScript', 'vanilla JS', 'vanilla JavaScript'],
|
||||
author: {
|
||||
name: 'Ronan Berder',
|
||||
x: '@hunvreus',
|
||||
},
|
||||
assets: {
|
||||
favicon: 'favicon.svg',
|
||||
appleTouchIcon: 'apple-touch-icon.png',
|
||||
socialImage: 'social.png',
|
||||
},
|
||||
logo: {
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class="h-4 w-4"><rect width="256" height="256" fill="none"></rect><line x1="208" y1="128" x2="128" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line><line x1="192" y1="40" x2="40" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line></svg>',
|
||||
},
|
||||
links: [
|
||||
{
|
||||
label: 'GitHub',
|
||||
icon: '<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>',
|
||||
iconOnly: true,
|
||||
url: 'https://github.com/hunvreus/basecoat',
|
||||
attrs: { target: '_blank', rel: 'noopener noreferrer', class: 'btn-sm-icon-outline' },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,7 +0,0 @@
|
||||
---
|
||||
permalink: /robots.txt
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
Sitemap: {{ siteUrl }}/sitemap.xml
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
permalink: /sitemap.xml
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
{% for page in collections.all %}
|
||||
{% if page.url and not page.url.startsWith('/fragments/') %}
|
||||
<url>
|
||||
<loc>{{ siteUrl }}{{ page.url | url }}</loc>
|
||||
<lastmod>{{ page.date.toISOString() }}</lastmod>
|
||||
</url>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</urlset>
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
layout: layouts/layout.njk
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
|
||||
{% from "dialog.njk" import dialog %}
|
||||
{% set footer %}
|
||||
<button class="btn-outline" onclick="this.closest('dialog').close()">Cancel</button>
|
||||
<button class="btn" onclick="this.closest('dialog').close()">Save changes</button>
|
||||
{% endset %}
|
||||
{% call dialog(
|
||||
id="demo-dialog-edit-profile",
|
||||
title="Edit profile",
|
||||
description="Make changes to your profile here. Click save when you're done.",
|
||||
trigger="Edit Profile",
|
||||
trigger_attrs={"class": "btn-outline"},
|
||||
dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
|
||||
footer=footer
|
||||
) %}
|
||||
<form class="flex-col gap-4">
|
||||
{% from "select.njk" import select %}
|
||||
{{ select(
|
||||
trigger_attrs={"class": "w-[180px]"},
|
||||
is_combobox=true,
|
||||
items=[
|
||||
{
|
||||
type: "group",
|
||||
label: "Fruits",
|
||||
items: [
|
||||
{ type: "item", value: "apple", label: "Apple" },
|
||||
{ type: "item", value: "banana", label: "Banana" },
|
||||
{ type: "item", value: "blueberry", label: "Blueberry" },
|
||||
{ type: "item", value: "pineapple", label: "Grapes" },
|
||||
{ type: "item", value: "pineapple", label: "Pineapple" }
|
||||
]
|
||||
}
|
||||
]
|
||||
) }}
|
||||
</form>
|
||||
{% endcall %}
|
||||
Generated
+5055
-2472
File diff suppressed because it is too large
Load Diff
+7
-13
@@ -34,29 +34,23 @@
|
||||
"homepage": "https://github.com/hunvreus/basecoat#readme",
|
||||
"scripts": {
|
||||
"build": "node ./scripts/build.js",
|
||||
"docs:build:css": "node ./scripts/build-docs-css.js",
|
||||
"docs:build": "npm run docs:build:css && eleventy",
|
||||
"docs:dev:css": "node ./scripts/build-docs-css.js --watch",
|
||||
"docs:dev:eleventy": "ELEVENTY_ENV=prod eleventy --serve",
|
||||
"docs:dev": "concurrently \"npm:docs:dev:css\" \"npm:docs:dev:eleventy\"",
|
||||
"docs:build": "npm run docs:generate && astro build",
|
||||
"docs:dev": "npm run docs:generate && astro dev",
|
||||
"workers:dev": "npm run docs:build && wrangler dev",
|
||||
"workers:deploy": "npm run docs:build && wrangler deploy",
|
||||
"workers:preview": "npm run docs:build && wrangler versions upload"
|
||||
"workers:preview": "npm run docs:build && wrangler versions upload",
|
||||
"docs:generate": "node ./scripts/prepare-rsd-assets.js && node ./scripts/generate-rsd-docs.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@11ty/eleventy": "^3.0.0",
|
||||
"@fontsource/geist-mono": "^5.2.8",
|
||||
"@fontsource/geist-sans": "^5.2.5",
|
||||
"@grimlink/eleventy-plugin-lucide-icons": "^2.1.4",
|
||||
"@shikijs/markdown-it": "^4.2.0",
|
||||
"@tailwindcss/cli": "^4.1.3",
|
||||
"concurrently": "^9.1.2",
|
||||
"eleventy-plugin-toc": "^1.1.5",
|
||||
"astro": "^6.4.6",
|
||||
"htmx.org": "^2.0.10",
|
||||
"lucide-static": "^1.17.0",
|
||||
"markdown-it": "^14.2.0",
|
||||
"markdown-it-anchor": "^9.2.0",
|
||||
"nunjucks": "^3.2.4",
|
||||
"prettier": "^3.5.3",
|
||||
"reallysimpledocs": "^1.0.0-beta.0",
|
||||
"tailwindcss": "^4.1.3",
|
||||
"terser": "^5.42.0",
|
||||
"wrangler": "^4.50.0"
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { execFile } from 'child_process';
|
||||
import { cp, mkdir } from 'fs/promises';
|
||||
import { promisify } from 'util';
|
||||
import { generateCssEntrypoints } from './generate-css-entrypoints.js';
|
||||
|
||||
const execFilePromise = promisify(execFile);
|
||||
const styles = ['vega', 'nova', 'maia', 'lyra', 'mira', 'luma', 'sera', 'rhea'];
|
||||
const watch = process.argv.includes('--watch');
|
||||
const fontFiles = [
|
||||
'geist-sans-latin-400-normal.woff2',
|
||||
'geist-sans-latin-500-normal.woff2',
|
||||
'geist-sans-latin-600-normal.woff2',
|
||||
'geist-sans-latin-700-normal.woff2',
|
||||
'geist-mono-latin-400-normal.woff2',
|
||||
'geist-mono-latin-500-normal.woff2',
|
||||
'geist-mono-latin-600-normal.woff2',
|
||||
'geist-mono-latin-700-normal.woff2',
|
||||
];
|
||||
|
||||
function tailwindArgs(input, output) {
|
||||
const args = ['tailwindcss', '-i', input, '-o', output];
|
||||
if (watch) args.push('--watch');
|
||||
else args.push('--minify');
|
||||
return args;
|
||||
}
|
||||
|
||||
async function copyDocsAssets() {
|
||||
await mkdir('./docs/src/assets/js', { recursive: true });
|
||||
await cp('./node_modules/htmx.org/dist/htmx.min.js', './docs/src/assets/js/htmx.min.js');
|
||||
|
||||
await mkdir('./docs/src/assets/fonts/geist', { recursive: true });
|
||||
for (const file of fontFiles) {
|
||||
const packageName = file.startsWith('geist-sans') ? 'geist-sans' : 'geist-mono';
|
||||
await cp(`./node_modules/@fontsource/${packageName}/files/${file}`, `./docs/src/assets/fonts/geist/${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
await copyDocsAssets();
|
||||
await generateCssEntrypoints();
|
||||
|
||||
if (watch) {
|
||||
const children = styles.map((style) => {
|
||||
const child = spawn('npx', tailwindArgs(`./docs/css/styles-${style}.css`, `./docs/src/assets/styles-${style}.css`), {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
child.on('exit', (code) => {
|
||||
if (code) process.exitCode = code;
|
||||
});
|
||||
return child;
|
||||
});
|
||||
|
||||
const shutdown = () => {
|
||||
for (const child of children) child.kill('SIGTERM');
|
||||
};
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
} else {
|
||||
for (const style of styles) {
|
||||
await execFilePromise('npx', tailwindArgs(`./docs/css/styles-${style}.css`, `./docs/src/assets/styles-${style}.css`));
|
||||
}
|
||||
await execFilePromise('npx', tailwindArgs('./docs/css/styles-vega.css', './docs/src/assets/styles.css'));
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import * as lucideIcons from 'lucide-static';
|
||||
import Nunjucks from 'nunjucks';
|
||||
import prettier from 'prettier';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const sourceDir = path.join(root, 'docs/src');
|
||||
const outputDir = path.join(root, 'docs/generated');
|
||||
const fragmentsDir = path.join(outputDir, 'fragments');
|
||||
const pkg = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'));
|
||||
|
||||
const pageFiles = [
|
||||
'introduction.md',
|
||||
'installation.md',
|
||||
'customization.md',
|
||||
'cli.md',
|
||||
'kitchen-sink.njk',
|
||||
...(await collectFiles(path.join(sourceDir, 'components'))).map((file) => path.relative(sourceDir, file)),
|
||||
];
|
||||
|
||||
function parseFrontMatter(source) {
|
||||
if (!source.startsWith('---')) return { data: {}, body: source };
|
||||
const match = source.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
||||
if (!match) return { data: {}, body: source };
|
||||
const data = {};
|
||||
for (const line of match[1].split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const keyMatch = trimmed.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
||||
if (!keyMatch) continue;
|
||||
let value = keyMatch[2].trim();
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
data[keyMatch[1]] = value;
|
||||
}
|
||||
return { data, body: source.slice(match[0].length) };
|
||||
}
|
||||
|
||||
async function collectFiles(dir) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
const files = [];
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...(await collectFiles(full)));
|
||||
else if (entry.isFile() && /\.(md|njk)$/.test(entry.name)) files.push(full);
|
||||
}
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
function pascalIconName(iconName) {
|
||||
const normalized = String(iconName || '').trim();
|
||||
if (!normalized) return '';
|
||||
if (normalized.includes('-')) {
|
||||
return normalized.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('');
|
||||
}
|
||||
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
||||
}
|
||||
|
||||
function addAttrs(svg, attrs = {}) {
|
||||
let out = String(svg || '');
|
||||
for (const [name, value] of Object.entries(attrs || {})) {
|
||||
if (value === undefined || value === null || value === false) continue;
|
||||
const escaped = String(value).replaceAll('&', '&').replaceAll('"', '"');
|
||||
if (name === 'class' && out.includes('class="')) out = out.replace('class="', `class="${escaped} `);
|
||||
else out = out.replace('<svg', value === true ? `<svg ${name}` : `<svg ${name}="${escaped}"`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
class LucideExtension {
|
||||
tags = ['lucide'];
|
||||
|
||||
parse(parser, nodes) {
|
||||
const token = parser.nextToken();
|
||||
const args = parser.parseSignature(null, true);
|
||||
parser.advanceAfterBlockEnd(token.value);
|
||||
return new nodes.CallExtension(this, 'run', args);
|
||||
}
|
||||
|
||||
run(_context, iconName, attrs = {}) {
|
||||
const svg = lucideIcons[pascalIconName(iconName)] || '';
|
||||
return new Nunjucks.runtime.SafeString(addAttrs(svg, attrs));
|
||||
}
|
||||
}
|
||||
|
||||
function renderTemplate(env, source, context) {
|
||||
return new Promise((resolve, reject) => {
|
||||
env.renderString(source, context, (error, result) => {
|
||||
if (error) reject(error);
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function outputSlug(relativeFile) {
|
||||
const normalized = relativeFile.replaceAll(path.sep, '/').replace(/\.(md|njk)$/, '');
|
||||
return normalized === 'introduction' ? 'index' : normalized;
|
||||
}
|
||||
|
||||
function normalizeNavigationItem(item) {
|
||||
if (typeof item === 'string') return item === 'introduction' ? 'index' : item;
|
||||
if (item?.type === 'item') {
|
||||
if (item.slug) return { ...item, slug: item.slug === 'introduction' ? 'index' : item.slug };
|
||||
if (item.url && item.url.startsWith('/')) {
|
||||
const slug = item.url.replace(/^\/+|\/+$/g, '') || 'index';
|
||||
return { slug, icon: item.icon };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (item?.type === 'submenu') {
|
||||
return { ...item, items: (item.items || []).map(normalizeNavigationItem).filter(Boolean) };
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await fs.rm(outputDir, { recursive: true, force: true });
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
const docsConfig = JSON.parse(await fs.readFile(path.join(sourceDir, '_data/docs.json'), 'utf8'));
|
||||
const menu = docsConfig.menu.map((group) => ({
|
||||
...group,
|
||||
items: (group.items || []).map(normalizeNavigationItem).filter(Boolean),
|
||||
}));
|
||||
await fs.writeFile(path.join(outputDir, 'docs.json'), `${JSON.stringify({ menu }, null, 2)}\n`);
|
||||
|
||||
const env = new Nunjucks.Environment(
|
||||
new Nunjucks.FileSystemLoader([
|
||||
path.join(root, 'src/nunjucks'),
|
||||
path.join(sourceDir, '_includes'),
|
||||
]),
|
||||
{ autoescape: true },
|
||||
);
|
||||
env.addExtension('LucideExtension', new LucideExtension());
|
||||
env.addFilter('prettyHtml', async (code, callback) => {
|
||||
try {
|
||||
callback(null, await prettier.format(code, {
|
||||
parser: 'html',
|
||||
printWidth: 1000,
|
||||
singleAttributePerLine: false,
|
||||
bracketSameLine: true,
|
||||
htmlWhitespaceSensitivity: 'ignore',
|
||||
}));
|
||||
} catch (error) {
|
||||
callback(error, code);
|
||||
}
|
||||
}, true);
|
||||
|
||||
const context = {
|
||||
pkg,
|
||||
site: {
|
||||
title: 'Basecoat',
|
||||
description: 'A components library built with Tailwind CSS that works with any web stack.',
|
||||
},
|
||||
};
|
||||
|
||||
for (const relativeFile of pageFiles) {
|
||||
const absoluteFile = path.join(sourceDir, relativeFile);
|
||||
const source = await fs.readFile(absoluteFile, 'utf8');
|
||||
const { data, body } = parseFrontMatter(source);
|
||||
const rendered = (await renderTemplate(env, body, context)).replaceAll('<pre class="scrollbar">', '<pre class="scrollbar" data-copy-code-initialized="true">');
|
||||
const slug = outputSlug(relativeFile);
|
||||
const outPath = path.join(outputDir, `${slug}.md`);
|
||||
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
||||
const title = data.title || titleFromSlug(slug);
|
||||
const hasOwnHeading = /<h1[\s>]/.test(rendered);
|
||||
const heading = hasOwnHeading ? '' : `# ${title}`;
|
||||
const description = !hasOwnHeading && data.description ? `\n\n<p class="text-muted-foreground text-[1.05rem] sm:text-base">${escapeHtml(data.description)}</p>` : '';
|
||||
await fs.writeFile(outPath, `${heading}${description}\n\n${rendered.trim()}\n\n<div id="toaster" class="toaster"></div>\n<script src="/assets/js/htmx.min.js" defer></script>\n`);
|
||||
}
|
||||
|
||||
for (const type of ['success', 'error', 'info', 'warning']) {
|
||||
const source = await fs.readFile(path.join(sourceDir, 'fragments/toast', `${type}.njk`), 'utf8');
|
||||
const rendered = await renderTemplate(env, source, context);
|
||||
const outPath = path.join(fragmentsDir, 'toast', `${type}.html`);
|
||||
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
||||
await fs.writeFile(outPath, rendered.trim());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function titleFromSlug(slug) {
|
||||
const last = slug.split('/').at(-1) || slug;
|
||||
return last.replaceAll('-', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -0,0 +1,28 @@
|
||||
import { cp, mkdir, rm } from 'node:fs/promises';
|
||||
|
||||
const fontFiles = [
|
||||
'geist-sans-latin-400-normal.woff2',
|
||||
'geist-sans-latin-500-normal.woff2',
|
||||
'geist-sans-latin-600-normal.woff2',
|
||||
'geist-sans-latin-700-normal.woff2',
|
||||
'geist-mono-latin-400-normal.woff2',
|
||||
'geist-mono-latin-500-normal.woff2',
|
||||
'geist-mono-latin-600-normal.woff2',
|
||||
'geist-mono-latin-700-normal.woff2',
|
||||
];
|
||||
|
||||
await rm('./public/assets', { recursive: true, force: true });
|
||||
await mkdir('./public/assets', { recursive: true });
|
||||
await cp('./docs/src/assets', './public/assets', {
|
||||
recursive: true,
|
||||
filter: (source) => !source.endsWith('.DS_Store') && !/styles(?:-[a-z]+)?\.css$/.test(source),
|
||||
});
|
||||
|
||||
await mkdir('./public/assets/js', { recursive: true });
|
||||
await cp('./node_modules/htmx.org/dist/htmx.min.js', './public/assets/js/htmx.min.js');
|
||||
|
||||
await mkdir('./public/assets/fonts/geist', { recursive: true });
|
||||
for (const file of fontFiles) {
|
||||
const packageName = file.startsWith('geist-sans') ? 'geist-sans' : 'geist-mono';
|
||||
await cp(`./node_modules/@fontsource/${packageName}/files/${file}`, `./public/assets/fonts/geist/${file}`);
|
||||
}
|
||||
@@ -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' },
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"name": "basecoat",
|
||||
"compatibility_date": "2026-06-02",
|
||||
"assets": {
|
||||
"directory": "./_site",
|
||||
"directory": "./dist",
|
||||
"not_found_handling": "404-page"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user