prepare basecoat 1.0 migration

This commit is contained in:
Ronan Berder
2026-06-03 23:08:41 +08:00
parent 7e7d509e90
commit c1cf01da45
125 changed files with 2346 additions and 1169 deletions
+93 -20
View File
@@ -1,17 +1,79 @@
import eleventyLucideicons from "@grimlink/eleventy-plugin-lucide-icons";
import prettier from "prettier";
import Nunjucks from "nunjucks";
import fs from "fs";
import path from "path";
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.setInputDirectory("./docs/src");
eleventyConfig.addPassthroughCopy({"docs/src/assets": "assets"});
eleventyConfig.addPassthroughCopy({ "src/js": "assets/js" });
eleventyConfig.addPassthroughCopy({ "docs/src/llms.txt": "llms.txt" });
eleventyConfig.addWatchTarget('src/nunjucks/');
eleventyConfig.addPassthroughCopy({ 'docs/src/assets': 'assets' });
eleventyConfig.addPassthroughCopy({ 'src/js': 'assets/js' });
eleventyConfig.addPlugin(eleventyLucideicons);
eleventyConfig.addShortcode("fetchCode", function(filePath) {
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');
@@ -21,27 +83,38 @@ export default async function(eleventyConfig) {
}
});
eleventyConfig.addNunjucksAsyncFilter("prettyHtml", async (code, callback) => {
eleventyConfig.addNunjucksAsyncFilter('prettyHtml', async (code, callback) => {
try {
const formattedCode = await prettier.format(code, {
parser: "html",
parser: 'html',
printWidth: 1000,
singleAttributePerLine: false,
bracketSameLine: true,
htmlWhitespaceSensitivity: "ignore"
htmlWhitespaceSensitivity: 'ignore',
});
callback(null, formattedCode);
} catch (err) {
console.error("Error formatting HTML with Prettier:", err);
console.error('Error formatting HTML with Prettier:', err);
callback(err, code);
}
});
let nunjucksEnvironment = new Nunjucks.Environment(
const nunjucksEnvironment = new Nunjucks.Environment(
new Nunjucks.FileSystemLoader([
"src/nunjucks",
"docs/src/_includes"
'src/nunjucks',
'docs/src/_includes',
]),
{ autoescape: true }
{ autoescape: true },
);
eleventyConfig.setLibrary("njk", nunjucksEnvironment);
eleventyConfig.setLibrary('njk', nunjucksEnvironment);
return {
dir: {
input: 'docs/src',
output: '_site',
includes: '_includes',
data: '_data',
},
markdownTemplateEngine: 'njk',
};
}
+3 -3
View File
@@ -22,7 +22,7 @@ Basecoat is not a literal DOM port of shadcn/ui. It is an alternative implementa
- `src/css/basecoat.css` – Aggregate CSS entrypoint
- `src/js/*.js` – Individual component JS files (kebab-case, ESM) + `basecoat.js` (component registry)
- `src/nunjucks/*.njk` and `src/jinja/*.html.jinja` – Component template macros
- `docs/src/components/*.njk` – Component documentation pages with examples
- `docs/src/components/*.md` – Component documentation pages with examples; Markdown can embed Nunjucks macros
- `docs/src/_includes/` – Layout templates, partials, and navigation
- Do not edit build outputs:
- `packages/*/dist` – built CSS/JS bundles
@@ -250,7 +250,7 @@ Per-component file checklist:
1. Basecoat component CSS: `src/css/components/<component>.css`
2. Basecoat templates/macros if relevant: `src/nunjucks/*.njk`, `src/jinja/*.html.jinja`
3. Basecoat docs page: `docs/src/components/<component>.njk`
3. Basecoat docs page: `docs/src/components/<component>.md`
4. Upstream base component: `../_sandbox/shadcn-ui/apps/v4/registry/bases/base/ui/<component>.tsx`
5. Upstream docs page: `../_sandbox/shadcn-ui/apps/v4/content/docs/components/{base,radix}/<component>.mdx`
6. Upstream styles: `../_sandbox/shadcn-ui/apps/v4/registry/styles/style-{vega,nova,maia,lyra,mira,luma,sera,rhea}.css`
@@ -300,7 +300,7 @@ Examples:
- Update a component's CSS/JS: Edit `src/css/basecoat.css` and/or `src/js/*.js`.
- Update a component's CSS/JS: Edit the relevant file in `src/css/components/*.css`, `src/css/styles/style-*.css`, and/or `src/js/*.js`.
- Update a component template: Edit `src/nunjucks/*.njk` and/or `src/jinja/*.html.jinja`.
- Add a new component: Create JS, CSS, templates, and docs page under `docs/src/components/*.njk`.
- Add a new component: Create JS, CSS, templates, and docs page under `docs/src/components/*.md`.
- Update navigation: Edit files in `docs/src/_includes/partials/`.
## Testing
+2 -2
View File
@@ -1,7 +1,7 @@
/* Basecoat docs font override. This is intentionally not part of the package default. */
:root {
--font-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
--font-heading: "Geist", ui-sans-serif, system-ui, sans-serif;
--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;
}
+63
View File
@@ -0,0 +1,63 @@
@font-face {
font-family: "Geist Sans";
font-style: normal;
font-display: swap;
font-weight: 400;
src: url("/assets/fonts/geist/geist-sans-latin-400-normal.woff2") format("woff2");
}
@font-face {
font-family: "Geist Sans";
font-style: normal;
font-display: swap;
font-weight: 500;
src: url("/assets/fonts/geist/geist-sans-latin-500-normal.woff2") format("woff2");
}
@font-face {
font-family: "Geist Sans";
font-style: normal;
font-display: swap;
font-weight: 600;
src: url("/assets/fonts/geist/geist-sans-latin-600-normal.woff2") format("woff2");
}
@font-face {
font-family: "Geist Sans";
font-style: normal;
font-display: swap;
font-weight: 700;
src: url("/assets/fonts/geist/geist-sans-latin-700-normal.woff2") format("woff2");
}
@font-face {
font-family: "Geist Mono";
font-style: normal;
font-display: swap;
font-weight: 400;
src: url("/assets/fonts/geist/geist-mono-latin-400-normal.woff2") format("woff2");
}
@font-face {
font-family: "Geist Mono";
font-style: normal;
font-display: swap;
font-weight: 500;
src: url("/assets/fonts/geist/geist-mono-latin-500-normal.woff2") format("woff2");
}
@font-face {
font-family: "Geist Mono";
font-style: normal;
font-display: swap;
font-weight: 600;
src: url("/assets/fonts/geist/geist-mono-latin-600-normal.woff2") format("woff2");
}
@font-face {
font-family: "Geist Mono";
font-style: normal;
font-display: swap;
font-weight: 700;
src: url("/assets/fonts/geist/geist-mono-latin-700-normal.woff2") format("woff2");
}
+4 -4
View File
@@ -1,9 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Geist+Mono:wght@400&display=swap");
@import "./fonts.css";
@import "tailwindcss";
@import "../../src/css/basecoat-luma.css";
@import "./custom.css";
@import "./highlight.css";
@import "./themes/claude.css";
@import "./themes/doom-64.css";
@import "./themes/supabase.css";
@import "./custom.css";
@import "./highlight.css";
+4 -4
View File
@@ -1,9 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Geist+Mono:wght@400&display=swap");
@import "./fonts.css";
@import "tailwindcss";
@import "../../src/css/basecoat-lyra.css";
@import "./custom.css";
@import "./highlight.css";
@import "./themes/claude.css";
@import "./themes/doom-64.css";
@import "./themes/supabase.css";
@import "./custom.css";
@import "./highlight.css";
+4 -4
View File
@@ -1,9 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Geist+Mono:wght@400&display=swap");
@import "./fonts.css";
@import "tailwindcss";
@import "../../src/css/basecoat-maia.css";
@import "./custom.css";
@import "./highlight.css";
@import "./themes/claude.css";
@import "./themes/doom-64.css";
@import "./themes/supabase.css";
@import "./custom.css";
@import "./highlight.css";
+4 -4
View File
@@ -1,9 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Geist+Mono:wght@400&display=swap");
@import "./fonts.css";
@import "tailwindcss";
@import "../../src/css/basecoat-mira.css";
@import "./custom.css";
@import "./highlight.css";
@import "./themes/claude.css";
@import "./themes/doom-64.css";
@import "./themes/supabase.css";
@import "./custom.css";
@import "./highlight.css";
+4 -4
View File
@@ -1,9 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Geist+Mono:wght@400&display=swap");
@import "./fonts.css";
@import "tailwindcss";
@import "../../src/css/basecoat-nova.css";
@import "./custom.css";
@import "./highlight.css";
@import "./themes/claude.css";
@import "./themes/doom-64.css";
@import "./themes/supabase.css";
@import "./custom.css";
@import "./highlight.css";
+4 -4
View File
@@ -1,9 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Geist+Mono:wght@400&display=swap");
@import "./fonts.css";
@import "tailwindcss";
@import "../../src/css/basecoat-rhea.css";
@import "./custom.css";
@import "./highlight.css";
@import "./themes/claude.css";
@import "./themes/doom-64.css";
@import "./themes/supabase.css";
@import "./custom.css";
@import "./highlight.css";
+4 -4
View File
@@ -1,9 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Geist+Mono:wght@400&display=swap");
@import "./fonts.css";
@import "tailwindcss";
@import "../../src/css/basecoat-sera.css";
@import "./custom.css";
@import "./highlight.css";
@import "./themes/claude.css";
@import "./themes/doom-64.css";
@import "./themes/supabase.css";
@import "./custom.css";
@import "./highlight.css";
+4 -4
View File
@@ -1,9 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Geist+Mono:wght@400&display=swap");
@import "./fonts.css";
@import "tailwindcss";
@import "../../src/css/basecoat-vega.css";
@import "./custom.css";
@import "./highlight.css";
@import "./themes/claude.css";
@import "./themes/doom-64.css";
@import "./themes/supabase.css";
@import "./custom.css";
@import "./highlight.css";
+4 -4
View File
@@ -1,9 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Geist+Mono:wght@400&display=swap");
@import "./fonts.css";
@import "tailwindcss";
@import "../../src/css/basecoat-vega.css";
@import "./custom.css";
@import "./highlight.css";
@import "./themes/claude.css";
@import "./themes/doom-64.css";
@import "./themes/supabase.css";
@import "./custom.css";
@import "./highlight.css";
+82
View File
@@ -0,0 +1,82 @@
{
"menu": [
{
"type": "group",
"label": "Getting started",
"items": [
"introduction",
"installation",
"customization",
"cli",
{
"type": "item",
"label": "Kitchen sink",
"url": "/kitchen-sink/",
"icon": "layout-dashboard"
},
{
"type": "item",
"label": "GitHub",
"url": "https://github.com/hunvreus/basecoat",
"icon": "github",
"external": true,
"attrs": { "target": "_blank", "rel": "noopener" }
},
{
"type": "item",
"label": "Discord",
"url": "https://basecoatui.com/chat",
"icon": "discord",
"external": true,
"attrs": { "target": "_blank", "rel": "noopener" }
}
]
},
{
"type": "group",
"label": "Components",
"items": [
"components/accordion",
"components/alert",
"components/alert-dialog",
"components/avatar",
"components/badge",
"components/breadcrumb",
"components/button",
"components/button-group",
"components/card",
"components/carousel",
"components/chart",
"components/checkbox",
"components/combobox",
"components/command",
"components/dialog",
"components/dropdown-menu",
"components/empty",
"components/field",
"components/input",
"components/input-group",
"components/item",
"components/kbd",
"components/label",
"components/native-select",
"components/pagination",
"components/popover",
"components/progress",
"components/radio-group",
"components/select",
"components/sidebar",
"components/skeleton",
"components/slider",
"components/spinner",
"components/switch",
"components/table",
"components/tabs",
"components/textarea",
"components/theme-switcher",
"components/toast",
"components/tooltip"
]
}
]
}
+2 -2
View File
@@ -98,7 +98,7 @@
<script src="/assets/js/tabs.js?v={{ pkg.version }}" defer></script>
<script src="/assets/js/toast.js?v={{ pkg.version }}" defer></script>
{# HTMX #}
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script src="/assets/js/htmx.min.js" defer></script>
{# Highlight.js #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/django.min.js"></script>
@@ -123,7 +123,7 @@
</script>
{% if head %}{{ head | safe }}{% endif %}
</head>
<body>
<body hx-boost="true" hx-target="#content" hx-select="#content" hx-swap="outerHTML show:window:top">
{{ content | safe }}
{% from "toast.njk" import toaster %}
+100 -2
View File
@@ -1,16 +1,64 @@
---
layout: layouts/layout.njk
---
{% set docsMenu = docs.menu if docs and docs.menu else menu %}
{% set navigation = docsMenu | getNavigation(collections) %}
<main class="mx-auto relative flex w-full max-w-screen-lg gap-10">
<div class="mx-auto w-full flex-1 max-w-screen-md">
<header class="space-y-2">
<h1 class="text-2xl font-semibold tracking-tight sm:text-3xl xl:text-4xl">{{ title }}</h1>
<div class="flex items-start gap-x-2">
<h1 class="text-2xl font-semibold tracking-tight sm:text-3xl xl:text-4xl mr-auto">{{ title }}</h1>
<div class="flex items-center gap-x-2 pt-1">
<button
type="button"
class="btn-sm-secondary h-7 px-2.5 text-[13px] max-sm:size-7"
data-md-url="{{ page.url | markdownUrl }}"
onclick="copyPageMarkdown(this)"
>
{% lucide "copy" %}
<span class="hidden sm:block">Copy page</span>
</button>
<div role="group" class="button-group">
{% if navigation.prev %}
<a href="{{ navigation.prev.url }}" class="btn-sm-icon-secondary hidden sm:flex size-7">
<span class="sr-only">{{ navigation.prev.label }}</span>
{% lucide "arrow-left" %}
</a>
{% endif %}
{% if navigation.prev and navigation.next %}
<hr role="separator">
{% endif %}
{% if navigation.next %}
<a href="{{ navigation.next.url }}" class="btn-sm-icon-secondary hidden sm:flex size-7">
<span class="sr-only">{{ navigation.next.label }}</span>
{% lucide "arrow-right" %}
</a>
{% endif %}
</div>
</div>
</div>
<p class="text-muted-foreground text-[1.05rem] sm:text-base">{{ description }}</p>
</header>
<article class="pb-12 mt-8 content">
{{ content | safe }}
</article>
{% if navigation.prev or navigation.next %}
<footer class="flex items-center gap-2 border-t pt-6 mt-12">
{% if navigation.prev %}
<a href="{{ navigation.prev.url }}" class="btn-sm-secondary mr-auto">
{% lucide "arrow-left" %}
{{ navigation.prev.label }}
</a>
{% endif %}
{% if navigation.next %}
<a href="{{ navigation.next.url }}" class="btn-sm-secondary ml-auto">
{{ navigation.next.label }}
{% lucide "arrow-right" %}
</a>
{% endif %}
</footer>
{% endif %}
</div>
{% if toc %}
<div class="hidden text-sm xl:block w-full max-w-[300px]">
@@ -18,4 +66,54 @@ layout: layouts/layout.njk
{{ toc_nav(toc) }}
</div>
{% endif %}
</main>
</main>
<script>
async function copyPageMarkdown(button) {
const markdownUrl = button?.dataset?.mdUrl;
if (!markdownUrl) return;
const label = button.querySelector('span');
const originalLabel = label?.textContent;
try {
const absoluteMarkdownUrl = new URL(markdownUrl, window.location.href).toString();
const response =
(await fetch(markdownUrl, { credentials: 'same-origin', cache: 'no-store' }).catch(() => null)) ||
(await fetch(absoluteMarkdownUrl, { credentials: 'same-origin', cache: 'no-store' }));
if (!response.ok) throw new Error('Failed to fetch markdown');
const markdown = await response.text();
await writeToClipboard(markdown);
if (label) {
label.textContent = 'Copied';
window.setTimeout(() => {
label.textContent = originalLabel || 'Copy page';
}, 1200);
}
} catch (error) {
const absoluteMarkdownUrl = new URL(markdownUrl, window.location.href).toString();
window.open(absoluteMarkdownUrl, '_blank', 'noopener');
}
}
async function writeToClipboard(text) {
if (navigator.clipboard?.writeText && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed';
textarea.style.top = '-9999px';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
const ok = document.execCommand('copy');
document.body.removeChild(textarea);
if (!ok) throw new Error('Copy failed');
}
</script>
+3 -20
View File
@@ -1,23 +1,6 @@
{% macro code_block(code, language="html", class="my-4") %}
{%- macro code_block(code, language="html", class="my-4") -%}
<div class="relative {{ class }}">
<pre class="scrollbar"><code class="language-{{ language }}">{{- code | escape -}}</code></pre>
<button
onclick="
const button = this;
const code = this.parentElement.querySelector('pre code');
if (!code) return;
navigator.clipboard.writeText(code.textContent || '').then(() => {
button.classList.add('copied');
setTimeout(() => { button.classList.remove('copied') }, 2000);
}).catch(err => {
console.error('Failed to copy text: ', err);
});
"
class="btn-icon-ghost size-8 absolute right-2.5 top-2 text-muted-foreground hover:text-foreground group"
>
{% lucide "copy", {"class": "group-[.copied]:hidden" } %}
{% lucide "check", {"class": "hidden group-[.copied]:block" } %}
</button>
<button onclick="const button = this; const code = this.parentElement.querySelector('pre code'); if (!code) return; navigator.clipboard.writeText(code.textContent || '').then(() => { button.classList.add('copied'); setTimeout(() => { button.classList.remove('copied') }, 2000); }).catch(err => { console.error('Failed to copy text: ', err); });" class="btn-icon-ghost size-8 absolute right-2.5 top-2 text-muted-foreground hover:text-foreground group">{% lucide "copy", {"class": "group-[.copied]:hidden" } %}{% lucide "check", {"class": "hidden group-[.copied]:block" } %}</button>
</div>
{% endmacro %}
{%- endmacro -%}
+2 -2
View File
@@ -1,4 +1,4 @@
{% macro code_preview(id, code, class=None, default_code=false) %}
{%- macro code_preview(id, code, class=None, default_code=false) -%}
{% from "./code_block.njk" import code_block %}
{% set code_panel = code_block(code, class="") %}
{% set preview_panel %}
@@ -23,4 +23,4 @@
default_tab_index=(2 if default_code else 1)
) }}
</div>
{% endmacro %}
{%- endmacro -%}
+36 -10
View File
@@ -1,3 +1,23 @@
{% from "command.njk" import command_dialog %}
{% set docsMenu = docs.menu if docs and docs.menu else menu %}
{% set commandMenu = docsMenu | commandMenu(collections) %}
{{ command_dialog(id="command-search", items=commandMenu, placeholder="Search the docs...") }}
<script>
document.addEventListener('keydown', (event) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
event.preventDefault();
const dialog = document.getElementById('command-search');
if (!dialog) return;
if (dialog.open) dialog.close();
else {
dialog.showModal();
dialog.querySelector('header input')?.focus();
}
}
});
</script>
<div class="flex h-14 w-full items-center gap-2 px-4">
<button
type="button"
@@ -10,10 +30,18 @@
>
{% lucide "panel-left" %}
</button>
<select
class="select h-8 leading-none"
id="theme-select"
<button
type="button"
class="relative text-muted-foreground bg-background dark:bg-input/30 border-input hidden h-8 w-full min-w-0 max-w-72 items-center rounded-md border px-3 py-1 gap-x-2 text-sm outline-none cursor-text focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] md:flex"
onclick="document.getElementById('command-search').showModal(); document.getElementById('command-search').querySelector('header input')?.focus();"
>
{% lucide "search", { "class": "size-4" } %}
Search the docs...
<kbd class="kbd ml-auto -mr-1.5">⌘K</kbd>
</button>
<select class="select h-8 leading-none" id="theme-select" aria-label="Theme">
<option value="">Default</option>
<option value="claude">Claude</option>
<option value="doom-64">Doom 64</option>
@@ -24,7 +52,7 @@
const themeSelect = document.getElementById('theme-select');
const storedTheme = localStorage.getItem('themeVariant');
if (themeSelect && storedTheme) themeSelect.value = storedTheme;
themeSelect.addEventListener('change', () => {
themeSelect?.addEventListener('change', () => {
const newTheme = themeSelect.value;
document.documentElement.classList.forEach(c => {
if (c.startsWith('theme-')) document.documentElement.classList.remove(c);
@@ -38,10 +66,8 @@
});
})();
</script>
<select
class="select h-8 leading-none"
id="style-select"
>
<select class="select h-8 leading-none" id="style-select" aria-label="Style">
<option value="vega">Vega</option>
<option value="nova">Nova</option>
<option value="maia">Maia</option>
@@ -58,14 +84,14 @@
const applyStyle = (styleName) => {
const nextStyle = styleName || 'nova';
document.documentElement.dataset.styleVariant = nextStyle;
styleLink.href = `/assets/styles-${nextStyle}.css`;
if (styleLink) styleLink.href = `/assets/styles-${nextStyle}.css`;
};
const storedStyle = localStorage.getItem('styleVariant') || 'nova';
if (styleSelect) styleSelect.value = storedStyle;
applyStyle(storedStyle);
styleSelect.addEventListener('change', () => {
styleSelect?.addEventListener('change', () => {
const newStyle = styleSelect.value || 'nova';
applyStyle(newStyle);
localStorage.setItem('styleVariant', newStyle);
@@ -8,16 +8,16 @@
<div class="p-4">
<div class="flex flex-wrap items-center gap-2">
<div class="flex flex-wrap items-center gap-2">
<button class="btn-outline" hx-trigger="click" hx-get="/fragments/toast/success" hx-target="#toaster" hx-swap="beforeend">
<button class="btn-outline" hx-trigger="click" hx-disinherit="hx-select hx-target hx-swap" hx-get="/fragments/toast/success" hx-target="#toaster" hx-swap="beforeend">
Success
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/fragments/toast/error" hx-target="#toaster" hx-swap="beforeend">
<button class="btn-outline" hx-trigger="click" hx-disinherit="hx-select hx-target hx-swap" hx-get="/fragments/toast/error" hx-target="#toaster" hx-swap="beforeend">
Error
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/fragments/toast/info" hx-target="#toaster" hx-swap="beforeend">
<button class="btn-outline" hx-trigger="click" hx-disinherit="hx-select hx-target hx-swap" hx-get="/fragments/toast/info" hx-target="#toaster" hx-swap="beforeend">
Info
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/fragments/toast/warning" hx-target="#toaster" hx-swap="beforeend">
<button class="btn-outline" hx-trigger="click" hx-disinherit="hx-select hx-target hx-swap" hx-get="/fragments/toast/warning" hx-target="#toaster" hx-swap="beforeend">
Warning
</button>
</div>
+12 -123
View File
@@ -1,46 +1,9 @@
{% from "sidebar.njk" import sidebar %}
{% from "popover.njk" import popover %}
<script>
(() => {
document.addEventListener('DOMContentLoaded', () => {
const sidebar = document.getElementById('sidebar');
if (!sidebar) return;
const updateCurrentLinks = () => {
const currentPath = window.location.pathname.replace(/\/$/, '');
const currentSearch = window.location.search;
const currentKey = `${currentPath}${currentSearch}`;
sidebar.querySelectorAll('a, button').forEach((item) => {
const href = item.getAttribute('href');
if (!href) {
item.removeAttribute('aria-current');
return;
}
const url = new URL(href, window.location.origin);
const itemPath = url.pathname.replace(/\/$/, '');
const itemKey = `${itemPath}${url.search}`;
if (itemKey === currentKey) {
item.setAttribute('aria-current', 'page');
} else {
item.removeAttribute('aria-current');
}
});
};
document.addEventListener('htmx:afterSettle', updateCurrentLinks);
window.addEventListener('popstate', updateCurrentLinks);
updateCurrentLinks();
});
})();
</script>
{% set footer %}
{% set trigger %}
<img src="https://github.com/hunvreus.png" class="rounded-lg shrink-0 size-8">
<img src="https://github.com/hunvreus.png" class="rounded-lg shrink-0 size-8" alt="Ronan Berder">
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">Ronan Berder</span>
<span class="truncate text-xs">@hunvreus</span>
@@ -61,11 +24,11 @@
<div class="grid gap-4">
<header class="grid gap-1.5">
<h2 class="font-semibold">I hope you like Basecoat...</h2>
<p class="text-muted-foreground text-sm">My name is <a href="https://ronanberder.com" target="_blank">Ronan</a> and I made this (and <a class="underline underline-offset-4" href="https://pagescms.org" target="_target">other things</a>). If you find it useful, please consider sponsoring me on GitHub or following me on X.</p>
<p class="text-muted-foreground text-sm">My name is <a href="https://ronanberder.com" target="_blank" rel="noopener">Ronan</a> and I made this. If you find it useful, please consider sponsoring me on GitHub or following me on X.</p>
</header>
<footer class="grid gap-2">
<a href="https://github.com/sponsors/hunvreus" class="btn-sm" target="_blank">Sponsor me on GitHub</a>
<a href="https://x.com/hunvreus" class="btn-sm-outline" target="_blank">Follow me on X</a>
<a href="https://github.com/sponsors/hunvreus" class="btn-sm" target="_blank" rel="noopener">Sponsor me on GitHub</a>
<a href="https://x.com/hunvreus" class="btn-sm-outline" target="_blank" rel="noopener">Follow me on X</a>
</footer>
</div>
{% endcall %}
@@ -74,94 +37,20 @@
{% set header %}
<a href="/" class="btn-ghost p-2 h-12 w-full justify-start">
<div class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<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>
{% if site.logo and site.logo.svg %}
{{ site.logo.svg | safe }}
{% else %}
<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>
{% endif %}
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">Basecoat</span>
<span class="truncate font-medium">{{ site.title }}</span>
<span class="truncate text-xs">v{{ pkg.version }}</span>
</div>
</a>
{# <div class="flex flex-col gap-2 px-2 text-sm">
<form class="relative">
<label for="search" class="sr-only">Search</label>
<input type="text" placeholder="Search the docs..." class="h-8 w-full shadow-none pl-8">
{% lucide "search", { "class": "pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" } %}
</form>
</div> #}
{% endset %}
{% set link_attrs = {
"hx-boost": "true",
"hx-select": "#content",
"hx-target": "#content",
"hx-swap": "outerHTML"
} %}
{% set icon_home %}{% lucide "house" %}{% endset %}
{% set icon_info %}{% lucide "info" %}{% endset %}
{% set icon_download %}{% lucide "download" %}{% endset %}
{% set icon_square_terminal %}{% lucide "square-terminal" %}{% endset %}
{% set icon_settings_2 %}{% lucide "settings-2" %}{% endset %}
{% set icon_layout_dashboard %}{% lucide "layout-dashboard" %}{% endset %}
{% set icon_github %}<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><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>{% endset %}
{% set icon_discord %}<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Discord</title><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>{% endset %}
{% set icon_github %}<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><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>{% endset %}
{% set icon_discord %}<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Discord</title><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>{% endset %}
{% set menu = [
{ type: "group", label: "Getting started", items: [
{ label: "Introduction", url: "/introduction", icon: icon_info, attrs: link_attrs },
{ label: "Installation", url: "/installation", icon: icon_download, attrs: link_attrs },
{ label: "Customization", url: "/customization", icon: icon_settings_2, attrs: link_attrs },
{ label: "CLI", url: "/cli", icon: icon_square_terminal, attrs: link_attrs },
{ label: "Kitchen sink", url: "/kitchen-sink", icon: icon_layout_dashboard, attrs: link_attrs },
{ label: "GitHub", url: "https://github.com/hunvreus/basecoat", icon: icon_github, attrs: { "target": "_blank" } },
{ label: "Discord", url: "https://basecoatui.com/chat", icon: icon_discord, attrs: { "target": "_blank" } }
]},
{ type: 'group', label: 'Components', items: [
{ label: "Accordion", url: "/components/accordion", attrs: link_attrs },
{ label: "Alert", url: "/components/alert", attrs: link_attrs },
{ label: "Alert Dialog", url: "/components/alert-dialog", attrs: link_attrs },
{ label: "Avatar", url: "/components/avatar", attrs: link_attrs },
{ label: "Badge", url: "/components/badge", attrs: link_attrs },
{ label: "Breadcrumb", url: "/components/breadcrumb", attrs: link_attrs },
{ label: "Button", url: "/components/button", attrs: link_attrs },
{ label: "Button Group", url: "/components/button-group", attrs: link_attrs },
{ label: "Card", url: "/components/card", attrs: link_attrs },
{ label: "Checkbox", url: "/components/checkbox", attrs: link_attrs },
{ label: "Command", url: "/components/command", attrs: link_attrs },
{ label: "Combobox", url: "/components/combobox", attrs: link_attrs },
{ label: "Dialog", url: "/components/dialog", attrs: link_attrs },
{ label: "Dropdown Menu", url: "/components/dropdown-menu", attrs: link_attrs },
{ label: "Empty", url: "/components/empty", attrs: link_attrs },
{ label: "Field", url: "/components/field", attrs: link_attrs },
{ label: "Input", url: "/components/input", attrs: link_attrs },
{ label: "Input Group", url: "/components/input-group", attrs: link_attrs },
{ label: "Item", url: "/components/item", attrs: link_attrs },
{ label: "Kbd", url: "/components/kbd", attrs: link_attrs },
{ label: "Label", url: "/components/label", attrs: link_attrs },
{ label: "Native Select", url: "/components/native-select", attrs: link_attrs },
{ label: "Pagination", url: "/components/pagination", attrs: link_attrs },
{ label: "Popover", url: "/components/popover", attrs: link_attrs },
{ label: "Progress", url: "/components/progress", attrs: link_attrs },
{ label: "Radio Group", url: "/components/radio-group", attrs: link_attrs },
{ label: "Select", url: "/components/select", attrs: link_attrs },
{ label: "Sidebar", url: "/components/sidebar", attrs: link_attrs },
{ label: "Skeleton", url: "/components/skeleton", attrs: link_attrs },
{ label: "Slider", url: "/components/slider", attrs: link_attrs },
{ label: "Spinner", url: "/components/spinner", attrs: link_attrs },
{ label: "Switch", url: "/components/switch", attrs: link_attrs },
{ label: "Table", url: "/components/table", attrs: link_attrs },
{ label: "Tabs", url: "/components/tabs", attrs: link_attrs },
{ label: "Textarea", url: "/components/textarea", attrs: link_attrs },
{ label: "Theme Switcher", url: "/components/theme-switcher", attrs: link_attrs },
{ label: "Toast", url: "/components/toast", attrs: link_attrs },
{ label: "Tooltip", url: "/components/tooltip", attrs: link_attrs }
]}
] %}
{% set docsMenu = docs.menu if docs and docs.menu else menu %}
{{ sidebar(
id="sidebar",
@@ -169,5 +58,5 @@
header=header,
footer=footer,
content_attrs={ "class": "scrollbar" },
menu=menu
menu=docsMenu | sidebarMenu(collections)
) }}
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
+2
View File
@@ -1,7 +1,9 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: CLI
description: Use the Basecoat CLI and template macros.
icon: square-terminal
toc:
- label: Use the CLI
id: cli
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Accordion
description: A vertically stacked set of interactive headings that each reveal a section of content.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Alert Dialog
description: A modal dialog that interrupts the user with important content and expects a response.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Alert
description: Displays a callout for user attention.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Avatar
description: An image element with a fallback for representing the user.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Badge
description: Displays a badge or a component that looks like a badge.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Breadcrumb
description: Displays the path to the current resource using a hierarchy of links.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Button Group
description: A container that groups related buttons together with consistent styling.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Button
description: Displays a button or a component that looks like a button.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Card
description: Displays a card with header, content, and footer.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Carousel
description: A carousel with motion and swipe.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Chart
description: Displays the path to the current resource using a hierarchy of links.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Checkbox
description: A control that allows the user to toggle between checked and not checked.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Combobox
description: Autocomplete input with a list of suggestions.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Command
description: Command menu for search and quick actions.
@@ -82,7 +83,7 @@ toc:
{% endmacro %}
{% set code_standalone %}
{% call command(id="demo-command-standalone") %}
{% call command(id="demo-command-standalone", main_attrs={ "class": "border" }) %}
{{ command_items("suggestions", "settings") }}
{% endcall %}
{% endset %}
@@ -256,7 +257,7 @@ toc:
<h3 id="example-shortcuts"><a href="#example-shortcuts">Shortcuts</a></h3>
{% set code_shortcuts %}
{% call command(id="command-shortcuts") %}
{% call command(id="command-shortcuts", main_attrs={ "class": "border" }) %}
{{ command_items("command-shortcuts-suggestions", "command-shortcuts-settings", true) }}
{% endcall %}
{% endset %}
@@ -265,7 +266,7 @@ toc:
<h3 id="example-groups"><a href="#example-groups">Groups</a></h3>
{% set code_groups %}
{% call command(id="command-groups") %}
{% call command(id="command-groups", main_attrs={ "class": "border" }) %}
{{ command_items("command-groups-suggestions", "command-groups-settings") }}
{% endcall %}
{% endset %}
@@ -274,7 +275,7 @@ toc:
<h3 id="example-scrollable"><a href="#example-scrollable">Scrollable</a></h3>
{% set code_scrollable %}
{% call command(id="command-scrollable") %}
{% call command(id="command-scrollable", main_attrs={ "class": "border" }) %}
<div role="group" aria-labelledby="command-scrollable-heading">
<span role="heading" id="command-scrollable-heading">Pages</span>
{% for i in range(0, 18) %}
@@ -296,7 +297,7 @@ toc:
{% set code_rtl %}
<div dir="rtl">
{% call command(id="command-rtl", placeholder="اكتب أمراً أو ابحث...") %}
{% call command(id="command-rtl", placeholder="اكتب أمراً أو ابحث...", main_attrs={ "class": "border" }) %}
<div role="group" aria-labelledby="command-rtl-suggestions">
<span role="heading" id="command-rtl-suggestions">اقتراحات</span>
<div role="menuitem" data-filter="Calendar">
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Dialog
description: A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Dropdown Menu
description: Displays a menu to the user — such as a set of actions or functions — triggered by a button.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Empty
description: Display empty states with icons, titles, descriptions, and actions.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Field
description: Combine labels, controls, and help text to compose accessible form fields.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Input Group
description: Add icons, text, buttons, and helper content to inputs and textareas.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Input
description: A text input component for forms and user data entry with built-in styling and accessibility features.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Item
description: A versatile component that you can use to display any content.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Kbd
description: Used to display textual user input from keyboard.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Label
description: Renders an accessible label associated with controls.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Native Select
description: A styled native HTML select element with consistent design system integration.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Pagination
description: Pagination with page navigation, next and previous links.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Popover
description: Displays rich content triggered by a button.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Progress
description: Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Radio Group
description: A set of checkable buttons where only one option can be selected at a time.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Select
description: Displays a list of options for the user to pick from—triggered by a button.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Sidebar
description: A composable, themeable and customizable sidebar component.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Skeleton
description: Used to show a placeholder while content is loading.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Slider
description: An input where the user selects a value from within a given range.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Spinner
description: An indicator that can be used to show a loading state.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Switch
description: A control that allows the user to toggle between checked and not checked.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Table
description: A responsive table component.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Tabs
description: A set of layered sections of content that are displayed one at a time.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Textarea
description: Displays a form textarea or a component that looks like a textarea.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Theme Switcher
description: A component that allows the user to switch between light and dark mode.
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Toast
description: A succinct message that is displayed temporarily.
@@ -32,6 +33,7 @@ toc:
{% set code_server_side %}<button
class="btn-outline"
hx-trigger="click"
hx-disinherit="hx-select hx-target hx-swap"
hx-get="/fragments/toast/success"
hx-target="#toaster"
hx-swap="beforeend"
@@ -1,4 +1,5 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Tooltip
description: A popup that displays information related to an element when the mouse hovers over it.
@@ -1,7 +1,9 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Customization
description: Configure icons, themes, fonts, and style overrides.
icon: settings-2
toc:
- label: Icons
id: icons
@@ -9,6 +11,8 @@ toc:
id: theming
- label: Fonts
id: fonts
- label: Custom styles
id: custom-styles
- label: Style overrides
id: style-overrides
---
@@ -69,17 +73,24 @@ toc:
<h2 id="fonts"><a href="#fonts">Fonts</a></h2>
<div class="prose">
<p>Basecoat does not ship web fonts by default. The Basecoat docs site uses the same font choices as the current shadcn/ui site: <a href="https://fonts.google.com/specimen/Geist" target="_blank">Geist</a> for sans and heading text, and <a href="https://fonts.google.com/specimen/Geist+Mono" target="_blank">Geist Mono</a> for monospace text.</p>
<p>Basecoat does not ship web fonts by default. The Basecoat docs site uses the same font choices as the current shadcn/ui site: <a href="https://fonts.google.com/specimen/Geist" target="_blank">Geist Sans</a> for sans and heading text, and <a href="https://fonts.google.com/specimen/Geist+Mono" target="_blank">Geist Mono</a> for monospace text.</p>
<p>To use the same setup, load the fonts and override the font tokens after importing Basecoat:</p>
</div>
{% set code %}@import "tailwindcss";
@import "basecoat-css/sera";
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Geist+Mono:wght@400&display=swap");
@import "@fontsource/geist-sans/400.css";
@import "@fontsource/geist-sans/500.css";
@import "@fontsource/geist-sans/600.css";
@import "@fontsource/geist-sans/700.css";
@import "@fontsource/geist-mono/400.css";
@import "@fontsource/geist-mono/500.css";
@import "@fontsource/geist-mono/600.css";
@import "@fontsource/geist-mono/700.css";
:root {
--font-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
--font-heading: "Geist", ui-sans-serif, system-ui, sans-serif;
--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;
}{% endset %}
{{ code_block(code, "css") }}
@@ -88,6 +99,32 @@ toc:
<p>Keep font overrides in a small CSS file imported after Basecoat. This makes fonts a project-level customization instead of a Basecoat package default.</p>
</div>
<h2 id="custom-styles"><a href="#custom-styles">Custom styles</a></h2>
<div class="prose">
<p>Basecoat style packs combine shared Basecoat structure with visual rules for each component. If you want to write a full custom style, import the styleless Basecoat base and then your own style file.</p>
</div>
{% set code %}@import "tailwindcss";
@import "basecoat-css/base";
@import "./style-acme.css";{% endset %}
{{ code_block(code, "css") }}
<div class="prose">
<p><code>basecoat-css/base</code> includes Basecoat tokens, semantic utilities, and component structure, but no Vega/Nova/Maia/etc. visual style pack. Your style file is responsible for component visuals such as colors, radius, shadows, focus rings, spacing, and variant treatment.</p>
<p>The practical way to start is to copy one existing style pack, rename it, and edit from there. The style files live in <code>src/css/styles/</code> and are also published under <code>basecoat-css/styles/*</code>.</p>
</div>
{% set code %}@import "basecoat-css/base";
@import "./style-acme.css";
/* style-acme.css can start as a copy of basecoat-css/styles/nova. */{% endset %}
{{ code_block(code, "css") }}
<div class="prose">
<p>Do not load a full Basecoat style bundle and then override it wholesale. For example, avoid importing <code>basecoat-css/nova</code> before your custom style. That loads Nova component visuals and forces your file to undo them. Use <code>basecoat-css/base</code> instead.</p>
</div>
<h2 id="style-overrides"><a href="#style-overrides">Style overrides</a></h2>
<div class="prose">
@@ -1,7 +1,9 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Installation
description: How to install and use Basecoat in your app.
icon: download
toc:
- label: Install using the CDN
id: install-cdn
@@ -1,7 +1,9 @@
---
templateEngineOverride: njk
layout: layouts/page.njk
title: Introduction
description: Basecoat is a set of components built with Tailwind CSS. It is designed to be used with any traditional web stack
icon: info
toc:
- label: Why Basecoat?
id: why
-506
View File
@@ -1,506 +0,0 @@
Basecoat – LLMs Guide
About
- Basecoat is a Tailwind‑first component library using semantic HTML and small vanilla JS for interactivity. No React required. Framework‑agnostic.
Install
- Tailwind project: `npm i basecoat-css`
- In CSS: `@import "tailwindcss";` then `@import "basecoat-css";`
CDN
- CSS: `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/basecoat.cdn.min.css">`
- JS (all): `<script src="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/js/all.min.js" defer></script>`
- JS (per component): include `basecoat.min.js` then `tabs.min.js`, `select.min.js`, etc.
JavaScript
- Auto‑init on `DOMContentLoaded`; observes DOM for new nodes.
- Manual: `window.basecoat.init('<name>')` or `window.basecoat.initAll()`.
- Packages to import with ESM: `import 'basecoat-css/all'` or `import 'basecoat-css/tabs'`.
Usage
- CSS‑only components: use classes like `btn`, `card`, `input`, etc.
- JS components: include the JS and use required roles/attributes (see below).
Theming and customization
- Tailwind projects can override design tokens (e.g., `primary`, `muted-foreground`) and font tokens (`--font-sans`, `--font-mono`).
- Without Tailwind, use CDN CSS and add overrides after the link tag.
- See `/customization` for icons, themes, fonts, and style overrides.
Common pitfalls
- Do not invent class names; use documented classes only.
- The package name is `basecoat-css` (not `basecoat`, not `@basecoat/css`).
- Interactive components require specific roles/attributes; include them.
- Basecoat is framework‑agnostic; do not mention React/Radix dependencies.
Components
- Button (CSS): `.btn`, variants like `.btn-secondary`, sizes `.btn-sm`, `.btn-lg`.
- Button Group (CSS): `.button-group` to group adjacent buttons; supports horizontal/vertical.
- Input (CSS): `.input` for text inputs; pairs with `.label` and `.field` patterns.
- Textarea (CSS): `.textarea` styled multiline input.
- Label (CSS): `.label` for associated form labels.
- Field (CSS): `.field` wrapper for label, input, hint, error.
- Checkbox (CSS): styled native checkbox; use `input[type='checkbox']`.
- Radio Group (CSS): `.radio-group` pattern using native radios.
- Switch (CSS): styled checkbox with `role="switch"` if needed.
- Select (JS): `.select` custom select; trigger button + popover + `[role=listbox]` and `[role=option]`. Dispatches `change` with `detail.value`.
- Combobox (JS): `.combobox` typeahead; similar roles to Select.
- Command (JS): `.command` with search input and `[role=menu]`/`[role=menuitem]`; keyboard nav.
- Tabs (JS): `.tabs` with `[role=tablist]`, `[role=tab]`, `aria-controls` panels; arrow keys switch.
- Popover (JS): `.popover` wraps a button and `[data-popover]` content; toggles `aria-hidden`.
- Dropdown Menu (JS): `.dropdown-menu` trigger + menu with `[role=menu]`/`[role=menuitem]`.
- Dialog (CSS/Native): `<dialog>` styled; `.command-dialog` variant wraps `.command`.
- Alert Dialog (CSS/Native): confirm/cancel flow using `<dialog>` semantics.
- Drawer (CSS/Native): off‑canvas using `<dialog>`/popover; class names documented in examples.
- Tooltip (CSS/JS-lite): `.tooltip` small info overlays.
- Toast (JS): global notifications container `#toaster`; dispatch `basecoat:toast` with `{ detail: { config } }`.
- Sidebar (JS): `.sidebar` with collapsible items; tracks current page via `aria-current`.
- Breadcrumb (CSS): `.breadcrumb` list of links.
- Pagination (CSS): `.pagination` navigation controls.
- Card (CSS): `.card` container with header/section/footer slots.
- Alert (CSS): `.alert` contextual banners.
- Badge (CSS): `.badge` and `.badge-outline`.
- Avatar (CSS): `.avatar` image + fallback.
- Skeleton (CSS): `.skeleton` loading placeholder.
- Progress (CSS): `.progress` bar.
- Slider (CSS): `.slider` styled input range.
- Spinner (CSS): `.spinner` loading indicator.
- Kbd (CSS): `.kbd` keyboard key style.
- Table (CSS): `.table` basic and striped tables.
- Item (CSS): `.item` list rows with icons and actions.
- Empty State (CSS): `.empty` placeholder content.
- Theme Switcher (JS-lite): `.theme-switcher` toggles themes (example in docs).
Events
- `basecoat:toast` (document): show a toast by dispatching `{ detail: { config } }`.
- `basecoat:initialized` (component root): emitted after a component initializes.
- `basecoat:popover` (document): coordination event to close other popovers.
Accessibility
- Use semantic roles/attributes from examples (tabs, menus, listbox/options).
- Disabled items: use `disabled` or `aria-disabled="true"`; JS will ignore them.
Troubleshooting
- If a component doesn’t work, ensure its JS is loaded and required roles/IDs match examples.
- For dynamic content, call `window.basecoat.init('<name>')` or `initAll()` after insertion.
License
- MIT (see LICENSE.md)
---
description: Basecoat
alwaysApply: true
applyTo: "**"
downloadedFrom: https://basecoatui.com/llms.txt
version: 0.3.x
---
# Basecoat
Basecoat is a Tailwind CSS v4‑first UI library using semantic HTML and small vanilla JS for interactivity. No React required; framework‑agnostic.
- Basecoat docs: https://basecoatui.com
- Package CSS: `basecoat-css`
## Basecoat install notes
1. Basecoat targets Tailwind CSS v4.
2. Import Tailwind in your CSS, then import Basecoat:
```css
@import "tailwindcss";
@import "basecoat-css";
```
3. For interactive components, include JS. With ESM you can load all or cherry‑pick:
```js
import 'basecoat-css/all';
// or cherry‑pick
import 'basecoat-css/tabs';
import 'basecoat-css/select';
import 'basecoat-css/popover';
import 'basecoat-css/dropdown-menu';
import 'basecoat-css/sidebar';
import 'basecoat-css/toast';
```
4. CDN usage (no build tool):
```html
<link href="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/basecoat.cdn.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/js/all.min.js" defer></script>
```
5. JS auto‑initializes on `DOMContentLoaded` and observes the DOM for added nodes. You can manually re‑init:
```js
window.basecoat.init('tabs');
window.basecoat.initAll();
```
## Basecoat usage rules
1. Add Basecoat class names to semantic HTML. Combine with Tailwind utilities for layout/spacing.
2. Interactive components require specific roles/attributes (see each component). Keep `aria-*` for semantics and `data-*` for visual states.
3. Disabled elements must use `disabled` or `aria-disabled="true"`. Basecoat JS filters them from interactions.
4. Only use documented Basecoat classes or Tailwind utilities. Don’t invent new class names.
5. Prefer native elements (button, input, dialog) and keep markup minimal.
Basecoat class name categories (for reference)
- `component`: the main component class
- `part`: a child element/slot of a component
- `style`: variants changing appearance
- `size`: size variants
- `behavior`: classes that change behavior
- `modifier`: boolean/conditional changes
## Config
Basecoat has no Tailwind plugin config. Import the CSS after Tailwind. Customize design tokens via your Tailwind theme or overrides.
Minimal CSS setup:
```css
@import "tailwindcss";
@import "basecoat-css";
```
## Basecoat tokens (commonly used)
These semantic tokens are referenced by classes (examples): `background`, `foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `muted`, `muted-foreground`, `card`, `card-foreground`, `popover`, `popover-foreground`, `destructive`, `ring`, `input`, `border`.
Rules
- Tokens should be defined in your Tailwind theme or be available via your design system. You can override colors using Tailwind utilities (e.g., `bg-primary`).
- Prefer utilities for one‑off overrides; keep semantic tokens consistent.
## Basecoat components
### alert
Alerts inform users about important events.
#### Class names
- component: `alert`, `alert-destructive`
#### Syntax
```html
<div role="alert" class="alert">{CONTENT}</div>
```
#### Rules
- Use `alert-destructive` for destructive context.
### avatar
Avatar shows a user thumbnail.
#### Class names
- component: `avatar`
#### Syntax
```html
<div class="avatar"><img src="{image-url}" alt="{alt}" /></div>
```
#### Rules
- Provide meaningful `alt` text or decorative as appropriate.
### badge
Badges display statuses or counts.
#### Class names
- component: `badge`
- style: `badge-outline`, `badge-destructive`
#### Syntax
```html
<span class="badge {MODIFIER}">{TEXT}</span>
```
#### Rules
- {MODIFIER} is optional; combine with utilities for spacing.
### button
Buttons trigger actions.
#### Class names
- component: `btn`
- style: `btn-primary`, `btn-secondary`, `btn-outline`, `btn-ghost`, `btn-link`, `btn-destructive`
- size: `btn-sm`, `btn-lg`
- icon: `btn-icon`, `btn-sm-icon`, `btn-lg-icon`
#### Syntax
```html
<button class="btn {MODIFIER}">{CONTENT}</button>
```
#### Rules
- Use `disabled` attribute for disabled state. Use `aria-pressed="true"` for toggle buttons.
### button-group
Groups adjacent buttons into a single control.
#### Class names
- component: `button-group`
#### Syntax
```html
<div class="button-group" data-orientation="{horizontal|vertical}">{BUTTONS}</div>
```
#### Rules
- Use separators with `<hr role="separator">` inside the group if needed.
### card
Card container with optional header/section/footer.
#### Class names
- component: `card`
- part: `> header`, `> section`, `> footer`
#### Syntax
```html
<article class="card">
<header><h2>{TITLE}</h2><p>{SUBTITLE}</p></header>
<section>{CONTENT}</section>
<footer>{ACTIONS}</footer>
</article>
```
#### Rules
- Parts are optional; structure with semantic elements.
### checkbox (CSS)
Styled native checkbox.
#### Class names
- component: `.input[type='checkbox']:not([role='switch'])`
#### Syntax
```html
<input type="checkbox" class="input" />
```
#### Rules
- Use standard checkbox semantics; pair with `<label>`.
### command (JS)
Command palette with search and results.
#### Class names
- component: `command`, wrapper variant: `command-dialog`
- part: `> header input`, `[role='menu']`, `[role='menuitem']`
#### Syntax
```html
<div class="command">
<header><input type="text" placeholder="Search" /></header>
<div role="menu">
<button role="menuitem">{ITEM}</button>
</div>
</div>
```
#### Rules
- Requires JS. Keyboard navigation supported. Disabled items must use `aria-disabled="true"` or `disabled` and are ignored.
### dropdown-menu (JS)
Contextual menu triggered by a button.
#### Class names
- component: `dropdown-menu`
- part: `> button` (trigger), `[role='menu']`, `[role='menuitem']`
#### Syntax
```html
<div class="dropdown-menu">
<button type="button">{TRIGGER}</button>
<div role="menu"> <button role="menuitem">{ITEM}</button> </div>
</div>
```
#### Rules
- Requires JS. Uses `basecoat:popover` to coordinate with other popovers.
### input (CSS)
Styled text input fields.
#### Class names
- component: `input` (applied to native `<input>` types)
#### Syntax
```html
<input type="text" class="input" placeholder="{Placeholder}" />
```
#### Rules
- Supports common input types; pair with `.label` and `.field` patterns.
### kbd (CSS)
Keyboard key visual.
#### Class names
- component: `kbd`
#### Syntax
```html
<kbd class="kbd">⌘K</kbd>
```
### label (CSS)
Styled form labels.
#### Class names
- component: `label`
#### Syntax
```html
<label class="label" for="{id}">{TEXT}</label>
```
### popover (JS)
Small overlay anchored to a trigger.
#### Class names
- component: `popover`
- part: `> button` (trigger), `[data-popover]` (content)
#### Syntax
```html
<div class="popover">
<button type="button">{TRIGGER}</button>
<section data-popover aria-hidden="true">{CONTENT}</section>
</div>
```
#### Rules
- Requires JS. Toggling sets `aria-hidden` on content and `aria-expanded` on trigger.
### radio-group (CSS)
Styled native radio inputs.
#### Class names
- component: `.input[type='radio']`
#### Syntax
```html
<label><input type="radio" class="input" name="{name}" /> {LABEL}</label>
```
### select (JS)
Custom select with searchable popover and listbox.
#### Class names
- component: `select`
- part: `> button` (trigger with label span), `[data-popover]` (panel), `[role='listbox']`, `[role='option']`, `> input[type='hidden']`, optional `header input[type='text']` filter
#### Syntax
```html
<div class="select">
<button type="button" aria-expanded="false"><span>{LABEL}</span></button>
<input type="hidden" name="{name}" value="{value}" />
<section data-popover aria-hidden="true">
<header><input type="text" placeholder="Filter" /></header>
<div role="listbox">
<div role="option" data-value="{value}" data-label="{label}">{label}</div>
</div>
</section>
</div>
```
#### Rules
- Requires JS. Dispatches `change` on root with `detail.value`. Disabled options use `aria-disabled="true"`.
### sidebar (JS)
Collapsible navigation sidebar.
#### Class names
- component: `sidebar`
#### Rules
- Requires JS. Uses `aria-current="page"` for active link styling.
### switch (CSS)
Styled switch using checkbox with `role="switch"`.
#### Class names
- component: `.input[type='checkbox'][role='switch']`
#### Syntax
```html
<input type="checkbox" role="switch" class="input" />
```
### table (CSS)
Styled tables.
#### Class names
- component: `table`
#### Syntax
```html
<table class="table">{ROWS}</table>
```
### tabs (JS)
Tabbed interface with keyboard navigation.
#### Class names
- component: `tabs`
- part: `[role='tablist']`, `[role='tab']`, `[role='tabpanel']`
#### Syntax
```html
<div class="tabs">
<div role="tablist">
<button role="tab" aria-controls="panel-1" aria-selected="true" tabindex="0">Tab 1</button>
<button role="tab" aria-controls="panel-2" aria-selected="false" tabindex="-1">Tab 2</button>
</div>
<section id="panel-1" role="tabpanel">...</section>
<section id="panel-2" role="tabpanel" hidden>...</section>
</div>
```
#### Rules
- Requires JS. Arrow keys move focus; keep `aria-controls` and `aria-selected` accurate.
### textarea (CSS)
Styled multiline text input.
#### Class names
- component: `textarea`
#### Syntax
```html
<textarea class="textarea" rows="{n}"></textarea>
```
### toast (JS)
Global notifications.
#### Class names
- component: `#toaster` (container), `toast` (item)
#### Syntax
```html
<section id="toaster"></section>
```
Show a toast:
```js
document.dispatchEvent(new CustomEvent('basecoat:toast', { detail: { config: { title: 'Saved', description: 'Changes persisted', category: 'success' } } }))
```
#### Rules
- Requires JS. Uses timeouts; hovering pauses timers.
### popover/dialog variants (CSS/JS‑lite)
Dialog and drawer patterns built on native elements, styled by Basecoat.
#### Class names
- wrapper: `command-dialog` (for Command palette in a dialog)
#### Rules
- Use native `<dialog>` or popover attributes where applicable.
### other CSS components
Additional CSS components include: `progress`, `skeleton`, `slider`, `spinner`, `pagination`, `breadcrumb`, `item`, `empty`, `theme-switcher`, `form`, `field`, `input-group`, `radio-group`, `switch`, `avatar`, `badge`, `alert`, `card`, `kbd`, `table`.
## Events
- `basecoat:initialized` (component root): emitted after a component initializes.
- `basecoat:popover` (document): used to close other popovers when one opens.
- `basecoat:toast` (document): creates a toast using `{ detail: { config } }`.
## Accessibility
- Use semantic roles/labels per component. Keep `aria-*` for semantics and `data-*` for visual states.
- Disabled items must use `disabled` or `aria-disabled="true"` to be filtered from interactions.
## Troubleshooting
- If interactivity doesn’t work, ensure the component’s JS is included and required roles/IDs match the examples.
- For dynamic content, call `window.basecoat.init('<name>')` or `window.basecoat.initAll()` after insertion.
## License
MIT (see LICENSE.md)
+2 -2
View File
@@ -27,7 +27,7 @@ The earlier five-style assumption was wrong. Luma, Sera, and Rhea are current up
- Basecoat component CSS: `src/css/components/<component>.css`
- Basecoat templates/macros if relevant: `src/nunjucks/*.njk`, `src/jinja/*.html.jinja`
- Basecoat docs page: `docs/src/components/<component>.njk`
- Basecoat docs page: `docs/src/components/<component>.md`
- Upstream base component: `../_sandbox/shadcn-ui/apps/v4/registry/bases/base/ui/<component>.tsx`
- Upstream styles: `../_sandbox/shadcn-ui/apps/v4/registry/styles/style-{vega,nova,maia,lyra,mira,luma,sera,rhea}.css`
- Basecoat styles: `src/css/styles/style-{vega,nova,maia,lyra,mira,luma,sera,rhea}.css`
@@ -95,7 +95,7 @@ Files involved:
- `src/css/styles/style-luma.css`
- `src/css/styles/style-sera.css`
- `src/css/styles/style-rhea.css`
- `docs/src/components/button.njk`
- `docs/src/components/button.md`
Current button work includes:
+1193 -10
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -45,9 +45,17 @@
},
"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",
"htmx.org": "^2.0.10",
"lucide-static": "^1.17.0",
"markdown-it": "^14.2.0",
"markdown-it-anchor": "^9.2.0",
"prettier": "^3.5.3",
"tailwindcss": "^4.1.3",
"terser": "^5.42.0",
+10
View File
@@ -59,6 +59,14 @@
"./toast": "./dist/js/toast.js",
"./toast.min": "./dist/js/toast.min.js",
"./all.css": "./dist/basecoat.all.css",
"./base": "./dist/basecoat-base.css",
"./base.css": "./dist/basecoat-base.css",
"./base.cdn.css": "./dist/basecoat-base.cdn.css",
"./components": "./dist/basecoat-components.css",
"./components.css": "./dist/basecoat-components.css",
"./components/*": "./dist/components/*.css",
"./components/*.css": "./dist/components/*.css",
"./base/base.css": "./dist/base/base.css",
"./vega": "./dist/basecoat-vega.css",
"./vega.css": "./dist/basecoat-vega.css",
"./nova": "./dist/basecoat-nova.css",
@@ -83,6 +91,8 @@
"./styles/luma": "./dist/styles/style-luma.css",
"./styles/sera": "./dist/styles/style-sera.css",
"./styles/rhea": "./dist/styles/style-rhea.css",
"./styles/*": "./dist/styles/style-*.css",
"./styles/*.css": "./dist/styles/style-*.css",
"./package.json": "./package.json"
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ For every component, check these files before editing:
- Basecoat component CSS: `src/css/components/<component>.css`
- Basecoat templates/macros if relevant: `src/nunjucks/*.njk`, `src/jinja/*.html.jinja`
- Basecoat docs examples: `docs/src/components/<component>.njk`
- Basecoat docs examples: `docs/src/components/<component>.md`
- Upstream base component: `../_sandbox/shadcn-ui/apps/v4/registry/bases/base/ui/<component>.tsx`
- Upstream docs page: `../_sandbox/shadcn-ui/apps/v4/content/docs/components/{base,radix}/<component>.mdx`
- Upstream style packs: `../_sandbox/shadcn-ui/apps/v4/registry/styles/style-{vega,nova,maia,lyra,mira,luma,sera,rhea}.css`
+26
View File
@@ -1,10 +1,22 @@
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];
@@ -13,6 +25,20 @@ function tailwindArgs(input, output) {
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`), {
+6 -1
View File
@@ -3,6 +3,7 @@ import path from 'path';
import { fileURLToPath } from 'url';
import { exec } from 'child_process';
import { promisify } from 'util';
import { generateCssEntrypoints } from './generate-css-entrypoints.js';
const execPromise = promisify(exec);
@@ -66,6 +67,7 @@ async function copyDirRecursive(src, dest) {
// Main build function to prepare packages for publishing.
async function build() {
console.log('Starting build process...');
await generateCssEntrypoints();
// Define all necessary paths
const cliPackageDir = path.join(projectRoot, 'packages', 'cli');
@@ -152,6 +154,9 @@ async function build() {
const styles = ['vega', 'nova', 'maia', 'lyra', 'mira', 'luma', 'sera', 'rhea'];
await fs.copyFile(path.join(srcCssDir, 'basecoat.css'), path.join(cssDistDir, 'basecoat.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat.all.css'), path.join(cssDistDir, 'basecoat.all.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat-base.css'), path.join(cssDistDir, 'basecoat-base.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat-base.cdn.css'), path.join(cssDistDir, 'basecoat-base.cdn.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat-components.css'), path.join(cssDistDir, 'basecoat-components.css'));
for (const style of styles) {
await fs.copyFile(path.join(srcCssDir, `basecoat-${style}.css`), path.join(cssDistDir, `basecoat-${style}.css`));
await fs.copyFile(path.join(srcCssDir, `basecoat-${style}.cdn.css`), path.join(cssDistDir, `basecoat-${style}.cdn.css`));
@@ -170,7 +175,7 @@ async function build() {
console.log(`Copied split CSS folders to ${cssDistDir}`);
// Create Tailwind CSS builds for the CSS package.
const cdnEntries = ['basecoat.cdn.css', ...styles.map((style) => `basecoat-${style}.cdn.css`)];
const cdnEntries = ['basecoat.cdn.css', 'basecoat-base.cdn.css', ...styles.map((style) => `basecoat-${style}.cdn.css`)];
for (const entry of cdnEntries) {
const cdnCssSrc = path.join(srcCssDir, entry);
const baseName = path.basename(entry, '.css');
+100
View File
@@ -0,0 +1,100 @@
import fs from 'fs/promises';
import path from 'path';
const styles = ['vega', 'nova', 'maia', 'lyra', 'mira', 'luma', 'sera', 'rhea'];
const excludedComponents = new Set(['form']);
// Keep cascade stable for components whose selectors intentionally compose.
const componentOrder = [
'alert',
'badge',
'button',
'button-group',
'card',
'collapsible',
'command',
'combobox',
'dialog',
'dropdown-menu',
'empty',
'field',
'checkbox',
'input',
'input-group',
'item',
'kbd',
'label',
'native-select',
'popover',
'radio',
'range',
'select',
'sidebar',
'skeleton',
'switch',
'table',
'tabs',
'textarea',
'toast',
'tooltip',
];
async function existingComponentNames(cssDir) {
const componentDir = path.join(cssDir, 'components');
const files = await fs.readdir(componentDir);
const names = files
.filter((file) => file.endsWith('.css'))
.map((file) => path.basename(file, '.css'))
.filter((name) => !excludedComponents.has(name));
const known = componentOrder.filter((name) => names.includes(name));
const extra = names.filter((name) => !componentOrder.includes(name)).sort();
return [...known, ...extra];
}
function componentImports(names) {
return [
'/* Components */',
...names.map((name) => `@import "./components/${name}.css";`),
].join('\n');
}
async function writeIfChanged(filePath, content) {
let current = null;
try {
current = await fs.readFile(filePath, 'utf8');
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
if (current !== content) await fs.writeFile(filePath, content);
}
export async function generateCssEntrypoints({ cssDir = path.resolve('src/css') } = {}) {
const components = await existingComponentNames(cssDir);
const componentsCss = `${componentImports(components)}\n`;
const baseCss = `@import "./base/base.css";\n@import "./basecoat-components.css";\n`;
await writeIfChanged(path.join(cssDir, 'basecoat-components.css'), componentsCss);
await writeIfChanged(path.join(cssDir, 'basecoat-base.css'), baseCss);
await writeIfChanged(path.join(cssDir, 'basecoat-base.cdn.css'), '@import "tailwindcss" source(none);\n@import "./basecoat-base.css";\n');
for (const style of styles) {
await writeIfChanged(
path.join(cssDir, `basecoat-${style}.css`),
`@import "./basecoat-base.css";\n@import "./styles/style-${style}.css";\n`,
);
await writeIfChanged(
path.join(cssDir, `basecoat-${style}.cdn.css`),
`@import "tailwindcss" source(none);\n@import "./basecoat-${style}.css";\n`,
);
}
await writeIfChanged(path.join(cssDir, 'basecoat.css'), '@import "./basecoat-vega.css";\n');
await writeIfChanged(path.join(cssDir, 'basecoat.all.css'), '@import "./basecoat.css";\n');
await writeIfChanged(path.join(cssDir, 'basecoat.cdn.css'), '@import "tailwindcss" source(none);\n@import "./basecoat.css";\n');
return { components, styles };
}
if (import.meta.url === `file://${process.argv[1]}`) {
await generateCssEntrypoints();
}
+2
View File
@@ -0,0 +1,2 @@
@import "tailwindcss" source(none);
@import "./basecoat-base.css";
+2
View File
@@ -0,0 +1,2 @@
@import "./base/base.css";
@import "./basecoat-components.css";
+32
View File
@@ -0,0 +1,32 @@
/* Components */
@import "./components/alert.css";
@import "./components/badge.css";
@import "./components/button.css";
@import "./components/button-group.css";
@import "./components/card.css";
@import "./components/collapsible.css";
@import "./components/command.css";
@import "./components/combobox.css";
@import "./components/dialog.css";
@import "./components/dropdown-menu.css";
@import "./components/empty.css";
@import "./components/field.css";
@import "./components/checkbox.css";
@import "./components/input.css";
@import "./components/input-group.css";
@import "./components/item.css";
@import "./components/kbd.css";
@import "./components/label.css";
@import "./components/native-select.css";
@import "./components/popover.css";
@import "./components/radio.css";
@import "./components/range.css";
@import "./components/select.css";
@import "./components/sidebar.css";
@import "./components/skeleton.css";
@import "./components/switch.css";
@import "./components/table.css";
@import "./components/tabs.css";
@import "./components/textarea.css";
@import "./components/toast.css";
@import "./components/tooltip.css";
+1 -33
View File
@@ -1,34 +1,2 @@
@import "./base/base.css";
/* Components */
@import "./components/alert.css";
@import "./components/badge.css";
@import "./components/button.css";
@import "./components/button-group.css";
@import "./components/card.css";
@import "./components/collapsible.css";
@import "./components/command.css";
@import "./components/combobox.css";
@import "./components/dialog.css";
@import "./components/dropdown-menu.css";
@import "./components/empty.css";
@import "./components/field.css";
@import "./components/checkbox.css";
@import "./components/input.css";
@import "./components/input-group.css";
@import "./components/item.css";
@import "./components/kbd.css";
@import "./components/label.css";
@import "./components/native-select.css";
@import "./components/popover.css";
@import "./components/radio.css";
@import "./components/range.css";
@import "./components/select.css";
@import "./components/sidebar.css";
@import "./components/switch.css";
@import "./components/table.css";
@import "./components/tabs.css";
@import "./components/textarea.css";
@import "./components/toast.css";
@import "./components/tooltip.css";
@import "./basecoat-base.css";
@import "./styles/style-luma.css";
+1 -33
View File
@@ -1,34 +1,2 @@
@import "./base/base.css";
/* Components */
@import "./components/alert.css";
@import "./components/badge.css";
@import "./components/button.css";
@import "./components/button-group.css";
@import "./components/card.css";
@import "./components/collapsible.css";
@import "./components/command.css";
@import "./components/combobox.css";
@import "./components/dialog.css";
@import "./components/dropdown-menu.css";
@import "./components/empty.css";
@import "./components/field.css";
@import "./components/checkbox.css";
@import "./components/input.css";
@import "./components/input-group.css";
@import "./components/item.css";
@import "./components/kbd.css";
@import "./components/label.css";
@import "./components/native-select.css";
@import "./components/popover.css";
@import "./components/radio.css";
@import "./components/range.css";
@import "./components/select.css";
@import "./components/sidebar.css";
@import "./components/switch.css";
@import "./components/table.css";
@import "./components/tabs.css";
@import "./components/textarea.css";
@import "./components/toast.css";
@import "./components/tooltip.css";
@import "./basecoat-base.css";
@import "./styles/style-lyra.css";
+1 -33
View File
@@ -1,34 +1,2 @@
@import "./base/base.css";
/* Components */
@import "./components/alert.css";
@import "./components/badge.css";
@import "./components/button.css";
@import "./components/button-group.css";
@import "./components/card.css";
@import "./components/collapsible.css";
@import "./components/command.css";
@import "./components/combobox.css";
@import "./components/dialog.css";
@import "./components/dropdown-menu.css";
@import "./components/empty.css";
@import "./components/field.css";
@import "./components/checkbox.css";
@import "./components/input.css";
@import "./components/input-group.css";
@import "./components/item.css";
@import "./components/kbd.css";
@import "./components/label.css";
@import "./components/native-select.css";
@import "./components/popover.css";
@import "./components/radio.css";
@import "./components/range.css";
@import "./components/select.css";
@import "./components/sidebar.css";
@import "./components/switch.css";
@import "./components/table.css";
@import "./components/tabs.css";
@import "./components/textarea.css";
@import "./components/toast.css";
@import "./components/tooltip.css";
@import "./basecoat-base.css";
@import "./styles/style-maia.css";
+1 -33
View File
@@ -1,34 +1,2 @@
@import "./base/base.css";
/* Components */
@import "./components/alert.css";
@import "./components/badge.css";
@import "./components/button.css";
@import "./components/button-group.css";
@import "./components/card.css";
@import "./components/collapsible.css";
@import "./components/command.css";
@import "./components/combobox.css";
@import "./components/dialog.css";
@import "./components/dropdown-menu.css";
@import "./components/empty.css";
@import "./components/field.css";
@import "./components/checkbox.css";
@import "./components/input.css";
@import "./components/input-group.css";
@import "./components/item.css";
@import "./components/kbd.css";
@import "./components/label.css";
@import "./components/native-select.css";
@import "./components/popover.css";
@import "./components/radio.css";
@import "./components/range.css";
@import "./components/select.css";
@import "./components/sidebar.css";
@import "./components/switch.css";
@import "./components/table.css";
@import "./components/tabs.css";
@import "./components/textarea.css";
@import "./components/toast.css";
@import "./components/tooltip.css";
@import "./basecoat-base.css";
@import "./styles/style-mira.css";
+1 -33
View File
@@ -1,34 +1,2 @@
@import "./base/base.css";
/* Components */
@import "./components/alert.css";
@import "./components/badge.css";
@import "./components/button.css";
@import "./components/button-group.css";
@import "./components/card.css";
@import "./components/collapsible.css";
@import "./components/command.css";
@import "./components/combobox.css";
@import "./components/dialog.css";
@import "./components/dropdown-menu.css";
@import "./components/empty.css";
@import "./components/field.css";
@import "./components/checkbox.css";
@import "./components/input.css";
@import "./components/input-group.css";
@import "./components/item.css";
@import "./components/kbd.css";
@import "./components/label.css";
@import "./components/native-select.css";
@import "./components/popover.css";
@import "./components/radio.css";
@import "./components/range.css";
@import "./components/select.css";
@import "./components/sidebar.css";
@import "./components/switch.css";
@import "./components/table.css";
@import "./components/tabs.css";
@import "./components/textarea.css";
@import "./components/toast.css";
@import "./components/tooltip.css";
@import "./basecoat-base.css";
@import "./styles/style-nova.css";

Some files were not shown because too many files have changed in this diff Show More