Merge branch 'main' into development

This commit is contained in:
Ronan Berder
2025-05-19 00:15:39 -07:00
79 changed files with 1173 additions and 460 deletions
+1
View File
@@ -0,0 +1 @@
github: [hunvreus]
+5 -6
View File
@@ -2,11 +2,10 @@
!.gitignore
!.eleventy.js
/node_modules
/_site
_site/
node_modules/
packages/cli/dist/
packages/css/dist/
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
npm-debug.log*
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) [year] [fullname]
Copyright (c) 2025 Ronan Berder
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+2
View File
@@ -2,6 +2,8 @@
A collection of modern UI components built with Tailwind CSS that works with any web stack. Basecoat brings the magic of [shadcn/ui](https://ui.shadcn.com) to traditional web applications, no React required.
![screenshot](docs/src/assets/images/screenshot.png)
## Features
- **Lightweight**: no runtime JS, just CSS and tiny Alpine.js scripts for the more interactive components.
-175
View File
@@ -1,175 +0,0 @@
#!/usr/bin/env node
import { Command } from 'commander';
import inquirer from 'inquirer';
import fs from 'fs-extra';
import path from 'path';
const program = new Command();
let packageVersion = 'unknown';
try {
const cliScriptDir = path.dirname(new URL(import.meta.url).pathname);
const packageJsonPath = path.resolve(cliScriptDir, '..', 'package.json');
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
packageVersion = JSON.parse(packageJsonContent).version;
} catch (error) {
console.warn('Could not read package version:', error.message);
}
const DEFAULT_CONFIG = {
templateEngine: null,
templateDest: './components/basecoat',
scriptDest: './static/js/basecoat'
};
let config = { ...DEFAULT_CONFIG };
async function getAvailableComponents() {
try {
const packageRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
const jsSourceDir = path.join(packageRoot, 'src', 'js');
const jsFiles = await fs.readdir(jsSourceDir);
return jsFiles
.filter(file => file.endsWith('.js'))
.map(file => path.basename(file, '.js'));
} catch (error) {
console.error("Error reading component source directories:", error);
return [];
}
}
async function ensureConfiguration() {
console.log('\nChecking configuration...');
if (!config.templateEngine) {
const engineChoice = await inquirer.prompt([
{
type: 'list',
name: 'engine',
message: 'Which template engine are you using?',
choices: ['nunjucks', 'jinja'],
}
]);
config.templateEngine = engineChoice.engine;
}
const pathChoices = await inquirer.prompt([
{
type: 'input',
name: 'templateDest',
message: 'Where should template files be placed?',
default: './',
},
{
type: 'input',
name: 'scriptDest',
message: 'Where should script files be placed?',
default: './',
}
]);
config.templateDest = pathChoices.templateDest;
config.scriptDest = pathChoices.scriptDest;
console.log(`Configuration set: Engine=${config.templateEngine}, Templates=${config.templateDest}, Scripts=${config.scriptDest}`);
}
console.log('Basecoat CLI');
program
.name('basecoat')
.description('Add Basecoat components to your project')
.version(packageVersion);
program
.command('add')
.description('Add one or more components to your project')
.argument('[components...]', 'Names of components to add')
.action(async (componentsArg) => {
let componentsToAdd = componentsArg;
try {
if (!componentsToAdd || componentsToAdd.length === 0) {
const availableComponents = await getAvailableComponents();
if (availableComponents.length === 0) {
console.error('Error: Could not find any components.');
return;
}
const answers = await inquirer.prompt([
{
type: 'checkbox',
name: 'selectedComponents',
message: 'Which component(s) would you like to add?',
choices: availableComponents,
validate: (input) => input.length > 0 ? true : 'Please select at least one component.'
}
]);
componentsToAdd = answers.selectedComponents;
}
if (!componentsToAdd || componentsToAdd.length === 0) {
console.log('No components selected. Exiting.');
return;
}
console.log(`Attempting to add: ${componentsToAdd.join(', ')}`);
await ensureConfiguration();
for (const componentName of componentsToAdd) {
await addComponent(componentName);
}
console.log('\nComponent addition process finished.');
} catch (error) {
if (error.isTtyError || error.constructor.name === 'ExitPromptError') {
console.log('\nOperation cancelled.');
} else {
console.error('\nAn unexpected error occurred:', error);
}
process.exit(1);
}
});
async function addComponent(componentName) {
console.log(`\nProcessing component: ${componentName}...`);
// 1. Determine source paths (relative to package root)
const cliScriptDir = path.dirname(new URL(import.meta.url).pathname); // Get directory of the current script file
// Go UP from cli/, then DOWN into src/, then the specific folder
const templateExt = config.templateEngine === 'jinja' ? '.html.jinja' : '.njk';
const templateSource = path.resolve(cliScriptDir, '..', 'src', config.templateEngine, `${componentName}${templateExt}`);
const scriptSource = path.resolve(cliScriptDir, '..', 'src', 'js', `${componentName}.js`);
// 2. Determine destination paths (relative to user's current working directory)
const templateDestPath = path.resolve(process.cwd(), config.templateDest, `${componentName}${templateExt}`);
const scriptDestPath = path.resolve(process.cwd(), config.scriptDest, `${componentName}.js`);
try {
// 3. Check if source files exist
const templateExists = await fs.pathExists(templateSource);
const scriptExists = await fs.pathExists(scriptSource);
if (!templateExists || !scriptExists) {
console.error(` Error: Source files for component '${componentName}' not found. Searched:`);
if (!templateExists) console.error(` - ${templateSource}`);
if (!scriptExists) console.error(` - ${scriptSource}`);
return; // Skip this component
}
// 4. Ensure destination directories exist
await fs.ensureDir(path.dirname(templateDestPath));
await fs.ensureDir(path.dirname(scriptDestPath));
// 5. Copy files
await fs.copyFile(templateSource, templateDestPath);
console.log(` -> Copied template to: ${templateDestPath}`);
await fs.copyFile(scriptSource, scriptDestPath);
console.log(` -> Copied script to: ${scriptDestPath}`);
} catch (error) {
console.error(` Error processing component ${componentName}:`, error);
}
}
program.parse(process.argv);
+4 -7
View File
@@ -1,16 +1,13 @@
@import url("https://fonts.googleapis.com/css2?family=Poppins&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Lora&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Fira+Code&display=swap");
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Geist+Mono&display=swap");
@import "tailwindcss";
@import "../../src/css/basecoat.css";
@import "./custom.css";
@theme {
--font-sans: 'Inter';
--font-mono: 'JetBrains Mono';
--font-sans: 'Geist';
--font-mono: 'Geist Mono';
}
@import "./themes/claude.css";
+10
View File
@@ -0,0 +1,10 @@
export default {
title: "Basecoat",
description: "A components library built with Tailwind CSS that works with any web stack.",
keywords: ["components", "component library", "component system", "UI", "UI kit", "shadcn", "shadcn/ui", "Tailwind CSS", "Tailwind", "CSS", "HTML", "Jinja", "Nunjucks", "Alpine.js"],
author: {
name: "Ronan Berder",
x: "@hunvreus"
},
url: process.env.ELEVENTY_ENV === 'development' ? "http://localhost:8080" : "https://basecoatui.com"
};
-4
View File
@@ -1,4 +0,0 @@
{
"title": "Basecoat",
"description": "shadcn/ui for the rest of us"
}
+24 -6
View File
@@ -45,18 +45,36 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Title & Description -->
<title>{{ site.title + ": " + title if page.url === "/" else title + " - " + site.title }}</title>
<meta name="description" content="{{ description or site.description }}"/>
<meta name="title" content="{{ title }} | Basecoat">
<meta name="description" content="{{ description }}">
{% set meta_title = title ~ ' | ' ~ site.title if title else site.title %}
{% set meta_description = description or site.description %}
<title>{{ meta_title }}</title>
<meta name="description" content="{{ meta_description }}"/>
<meta name="title" content="{{ meta_title }}">
{% if site.keywords %}<meta name="keywords" content="{{ site.keywords | join(',') }}" />{% endif %}
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="{{ site.url }}{{ page.url }}">
<meta property="og:title" content="{{ meta_title }}">
<meta property="og:description" content="{{ meta_description }}">
<meta property="og:image" content="{{ site.url }}/assets/social-screenshot.png">
<meta property="og:site_name" content="{{ site.title }}">
<meta property="og:locale" content="en_US">
<meta property="og:author" content="{{ site.author.name }}">
<!-- X/Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="{{ site.url }}{{ page.url }}">
<meta name="twitter:title" content="{{ title if title else site.title }}">
<meta name="twitter:description" content="{{ meta_description }}">
<meta name="twitter:image" content="{{ site.url }}/assets/social-screenshot.png">
<meta name="twitter:creator" content="{{ site.author.x }}">
<!-- CSS -->
<link rel="preconnect" href="https://rsms.me/">
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="stylesheet" href="/assets/styles.css">
<!-- JS -->
+1 -1
View File
@@ -9,7 +9,7 @@ layout: layouts/base.njk
{% include "partials/header.njk" %}
</header>
<div class="p-6 md:p-8 xl:p-12">
<div class="p-4 md:p-6 xl:p-12">
{{ content | safe }}
</div>
</main>
+1 -1
View File
@@ -5,7 +5,7 @@ layout: layouts/layout.njk
<main class="mx-auto relative flex w-full max-w-screen-lg gap-10">
<div class="mx-auto w-full max-w-screen-md">
<header class="space-y-2">
<h1 class="text-3xl font-semibold tracking-tight">{{ title }}</h1>
<h1 class="text-3xl font-bold tracking-tight">{{ title }}</h1>
<p class="text-muted-foreground">{{ description }}</p>
</header>
<article class="pb-12 mt-8 content">
+11 -1
View File
@@ -32,7 +32,6 @@
:aria-label="$store.darkMode.isDark ? 'Switch to light mode' : 'Switch to dark mode'"
:data-tooltip="$store.darkMode.isDark ? 'Light mode' : 'Dark mode'"
data-side="bottom"
data-align="end"
class="btn-icon-outline size-8"
x-init
@click="$store.darkMode.toggle()"
@@ -40,4 +39,15 @@
<span x-show="$store.darkMode.isDark">{% lucide "sun" %}</span>
<span x-show="!$store.darkMode.isDark">{% lucide "moon" %}</span>
</button>
<a
href="https://github.com/hunvreus/basecoat"
class="btn-icon size-8"
target="_blank"
rel="noopener noreferrer"
data-tooltip="GitHub repository"
data-side="bottom"
data-align="end"
>
{% lucide "github" %}
</a>
</div>
@@ -1,7 +1,7 @@
<section id="accordion" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Accordion</h2>
<a href="/components/accordion" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/accordion" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -3,7 +3,7 @@
<section id="alert-dialog" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Alert Dialog</h2>
<a href="/components/alert-dialog" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/alert-dialog" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="alert" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Alert</h2>
<a href="/components/alert" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/alert" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="avatar" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Avatar</h2>
<a href="/components/avatar" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/avatar" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="badge" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Badge</h2>
<a href="/components/badge" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/badge" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -3,7 +3,7 @@
<section id="breadcrumb" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Breadcrumb</h2>
<a href="/components/breadcrumb" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/breadcrumb" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="button" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Button</h2>
<a href="/components/button" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/button" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="card" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Card</h2>
<a href="/components/card" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/card" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="checkbox" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Checkbox</h2>
<a href="/components/checkbox" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/checkbox" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -3,7 +3,7 @@
<section id="combobox" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Combobox</h2>
<a href="/components/combobox" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/combobox" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -3,7 +3,7 @@
<section id="dialog" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Dialog</h2>
<a href="/components/dialog" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/dialog" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -3,7 +3,7 @@
<section id="dropdown-menu" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Dropdown Menu</h2>
<a href="/components/dropdown-menu" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/dropdown-menu" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -258,7 +258,8 @@
{% call dropdown_menu(
id="dropdown-checkboxes",
trigger=trigger,
trigger_attrs={"class": "btn-icon-ghost rounded-full size-8"}
trigger_attrs={"class": "btn-icon-ghost rounded-full size-8"},
content_attrs={"data-align": "end"}
) %}
{{ profile_menu | safe }}
{% endcall %}
@@ -1,7 +1,7 @@
<section id="input" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Input</h2>
<a href="/components/input" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/input" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="label" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Label</h2>
<a href="/components/label" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/label" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="pagination" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Pagination</h2>
<a href="/components/pagination" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/pagination" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -3,7 +3,7 @@
<section id="popover" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Popover</h2>
<a href="/components/popover" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/popover" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="radio-group" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Radio Group</h2>
<a href="/components/radio-group" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/radio-group" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -3,7 +3,7 @@
<section id="select" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Select</h2>
<a href="/components/select" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/select" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="skeleton" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Skeleton</h2>
<a href="/components/skeleton" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/skeleton" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -15,7 +15,7 @@
</div>
</div>
<div class="flex gap-4 w-full">
<div class="flex max-sm:flex-col gap-4 w-full">
<div class="card w-full @md:w-auto @md:min-w-sm">
<header>
<div class="bg-accent animate-pulse rounded-md h-4 w-2/3"></div>
@@ -1,7 +1,7 @@
<section id="slider" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Slider</h2>
<a href="/components/slider" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/slider" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="switch" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Switch</h2>
<a href="/components/switch" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/switch" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="table" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Table</h2>
<a href="/components/table" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/table" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -3,7 +3,7 @@
<section id="tabs" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Tabs</h2>
<a href="/components/tabs" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/tabs" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="textarea" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Textarea</h2>
<a href="/components/textarea" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/textarea" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="toast" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Toast</h2>
<a href="/components/toast" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/toast" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
@@ -1,7 +1,7 @@
<section id="tooltip" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Tooltip</h2>
<a href="/components/tooltip" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation">
<a href="/components/tooltip" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
+19 -3
View File
@@ -13,7 +13,18 @@
{% call popover(
trigger=trigger,
trigger_attrs={"class": "btn-ghost p-2 h-12 w-full flex items-center justify-start"},
content_attrs={"data-side": "right", "data-align": "end", "class": "w-80"}
content_attrs={
"data-side": "right",
"data-align": "end",
"class": "w-80",
"x-init": "
if (window.innerWidth < 768) {
$el.setAttribute('data-side', 'top');
$el.setAttribute('data-align', 'start');
$el.classList.add('w-full');
}
"
}
) %}
<div class="grid gap-4 p-4">
<header class="grid gap-1.5">
@@ -51,7 +62,8 @@
"hx-boost": "true",
"hx-select": "#content",
"hx-target": "#content",
"hx-swap": "outerHTML"
"hx-swap": "outerHTML",
"@click": "if (window.innerWidth < 768) { open = false }"
} %}
{% set icon_home %}{% lucide "house" %}{% endset %}
@@ -65,7 +77,11 @@
{ label: "Introduction", url: "/introduction", icon: icon_info, attrs: link_attrs },
{ label: "Installation", url: "/installation", 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: "GitHub", url: "https://github.com/hunvreus/basecoat", icon: icon_github, attrs: {
"target": "_blank",
"@click": "if (window.innerWidth < 768) { open = false }"
}
}
]},
{ type: 'group', label: 'Components', items: [
{ label: "Accordion", url: "/components/accordion", attrs: link_attrs },
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+26 -17
View File
@@ -1,15 +1,12 @@
/*! tailwindcss v4.1.4 | MIT License | https://tailwindcss.com */
@import url("https://fonts.googleapis.com/css2?family=Poppins&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Lora&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Fira+Code&display=swap");
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Geist+Mono&display=swap");
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--font-sans: 'Inter';
--font-mono: 'JetBrains Mono';
--font-sans: 'Geist';
--font-mono: 'Geist Mono';
--color-amber-50: oklch(98.7% 0.022 95.277);
--color-amber-100: oklch(96.2% 0.059 95.617);
--color-amber-900: oklch(41.4% 0.112 45.904);
@@ -56,7 +53,7 @@
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--tracking-tighter: -0.05em;
--font-weight-bold: 700;
--tracking-tight: -0.025em;
--tracking-normal: 0em;
--tracking-widest: 0.1em;
@@ -352,6 +349,9 @@
.mb-6 {
margin-bottom: calc(var(--spacing) * 6);
}
.mb-8 {
margin-bottom: calc(var(--spacing) * 8);
}
.mb-12 {
margin-bottom: calc(var(--spacing) * 12);
}
@@ -364,6 +364,9 @@
.block {
display: block;
}
.contents {
display: contents;
}
.flex {
display: flex;
}
@@ -772,9 +775,6 @@
.p-4 {
padding: calc(var(--spacing) * 4);
}
.p-6 {
padding: calc(var(--spacing) * 6);
}
.p-10 {
padding: calc(var(--spacing) * 10);
}
@@ -881,6 +881,10 @@
--tw-leading: var(--leading-tight);
line-height: var(--leading-tight);
}
.font-bold {
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
@@ -897,10 +901,6 @@
--tw-tracking: var(--tracking-tight);
letter-spacing: var(--tracking-tight);
}
.tracking-tighter {
--tw-tracking: var(--tracking-tighter);
letter-spacing: var(--tracking-tighter);
}
.tracking-widest {
--tw-tracking: var(--tracking-widest);
letter-spacing: var(--tracking-widest);
@@ -1218,6 +1218,11 @@
background-color: var(--color-green-50);
}
}
.max-sm\:flex-col {
@media (width < 40rem) {
flex-direction: column;
}
}
.sm\:max-w-\[425px\] {
@media (width >= 40rem) {
max-width: 425px;
@@ -1261,9 +1266,9 @@
border-left-width: 1px;
}
}
.md\:p-8 {
.md\:p-6 {
@media (width >= 48rem) {
padding: calc(var(--spacing) * 8);
padding: calc(var(--spacing) * 6);
}
}
.md\:pl-8 {
@@ -3171,6 +3176,10 @@
background-size: 1rem;
background-position: center right 0.75rem;
background-repeat: no-repeat;
option, optgroup {
background-color: var(--color-popover);
color: var(--color-popover-foreground);
}
}
}
.sidebar {
+2 -2
View File
@@ -61,7 +61,7 @@ document.addEventListener('alpine:init', () => {
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with Javascript
{% lucide "arrow-right" %}
@@ -89,7 +89,7 @@ document.addEventListener('alpine:init', () => {
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">dialog()</code> Nunjucks or Jinja macro for this component. If you use one of the macros, make sure to set <code>close_on_overlay_click</code> and <code>close_button</code> to <code>False</code> (or <code>false</code> for Nunjucks).</p>
</div>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
+1 -1
View File
@@ -12,7 +12,7 @@ toc:
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated accordion component in Basecoat.</h2>
<h2>There is no dedicated avatar component in Basecoat.</h2>
</div>
{% set code %}<img class="size-8 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">{% endset %}
+1 -1
View File
@@ -75,7 +75,7 @@ document.addEventListener('alpine:init', () => {
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with Javascript
{% lucide "arrow-right" %}
+2 -2
View File
@@ -71,7 +71,7 @@ document.addEventListener('alpine:init', () => {
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</div>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with Javascript
{% lucide "arrow-right" %}
@@ -99,7 +99,7 @@ document.addEventListener('alpine:init', () => {
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">select()</code> Nunjucks or Jinja macro for this component. If you use one of the macros, make sure to set <code>is_combobox</code> to <code>True</code> (or <code>true</code> for Nunjucks).</p>
</div>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
+11 -2
View File
@@ -89,7 +89,7 @@ document.addEventListener('alpine:init', () => {
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with Javascript
{% lucide "arrow-right" %}
@@ -111,13 +111,22 @@ document.addEventListener('alpine:init', () => {
] %}
{{ tabs(id="combobox", tabsets=tabsets) }}
<section class="prose">
<p>The component will dispatch <code>dialog:opened</code> and <code>dialog:closed</code> events when the dialog is opened or closed. You can also dispatch <code>dialog:open</code> and <code>dialog:close</code> events to open or close the dialog programmatically. You will need the element to have an <code>id</code> to be able to target it (e.g. <code>&lt;div id="my-dialog" class="dialog"&gt;</code>):</p>
</section>
{% set code_dispatch %}<button type="button" x-init @click="$dispatch('dialog:open', { id: 'my-dialog' })">
Open #my-dialog
</button>{% endset %}
{{ code_block(code_dispatch | prettyHtml, "html") }}
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">dialog()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
+2 -2
View File
@@ -114,7 +114,7 @@ document.addEventListener('alpine:init', () => {
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with Javascript
{% lucide "arrow-right" %}
@@ -142,7 +142,7 @@ document.addEventListener('alpine:init', () => {
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">dropdown_menu()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
+2 -2
View File
@@ -91,7 +91,7 @@ document.addEventListener('alpine:init', () => {
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with Javascript
{% lucide "arrow-right" %}
@@ -119,7 +119,7 @@ document.addEventListener('alpine:init', () => {
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">popover()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
+6 -2
View File
@@ -106,7 +106,7 @@ document.addEventListener('alpine:init', () => {
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with Javascript
{% lucide "arrow-right" %}
@@ -128,13 +128,17 @@ document.addEventListener('alpine:init', () => {
] %}
{{ tabs(id="select", tabsets=tabsets) }}
<section class="prose">
<p>The component will dispatch a <code>select:change</code> event when a value is selected, along with the value and label of the selected option in the <code>detail</code> object.</p>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">dropdown_menu()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
+2 -2
View File
@@ -43,7 +43,7 @@ toc:
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with Javascript
{% lucide "arrow-right" %}
@@ -112,7 +112,7 @@ document.addEventListener('alpine:init', () => {
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">sidebar()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
+2 -2
View File
@@ -109,7 +109,7 @@ document.addEventListener('alpine:init', () => {
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with Javascript
{% lucide "arrow-right" %}
@@ -137,7 +137,7 @@ document.addEventListener('alpine:init', () => {
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">tabs()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
+2 -2
View File
@@ -67,7 +67,7 @@ toc:
<p>You will also need to include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with Javascript
{% lucide "arrow-right" %}
@@ -205,7 +205,7 @@ document.addEventListener('alpine:init', () => {
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">toaster()</code> and <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">toast()</code> Nunjucks or Jinja macros for this component.</p>
</div>
<div class="flex gap-2 my-6">
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
+3 -3
View File
@@ -7,7 +7,7 @@ layout: layouts/layout.njk
<div class="max-w-screen-lg mx-auto">
<header class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<h1 class="text-2xl font-semibold tracking-tighter sm:text-3xl md:text-4xl">All of the shadcn/ui magic, none of the React</h1>
<h1 class="text-2xl font-bold tracking-tight sm:text-3xl md:text-4xl">All of the shadcn/ui magic, none of the React</h1>
<p class="sm:text-lg text-muted-foreground">A components library built with Tailwind CSS that works with any web stack.</p>
</div>
<div class="flex w-full items-center justify-start gap-2 pt-2">
@@ -92,7 +92,7 @@ layout: layouts/layout.njk
<div class="font-medium">Strictly Necessary</div>
<div class="text-muted-foreground">These cookies are essential in order to use the website and use its features.</div>
</div>
<input type="checkbox" role="switch" class="input" />
<input type="checkbox" role="switch" class="input" checked />
</label>
<label class="flex items-center justify-between gap-2">
<div class="flex flex-col gap-0.5">
@@ -192,7 +192,7 @@ layout: layouts/layout.njk
</div>
<div class="grid gap-2 flex-1">
<label for="payment-method-cvv">CVV</label>
<input type="text" id="payment-method-cvv" placeholder="123" maxlength="3" class="w-full" />
<input type="text" id="payment-method-cvv" placeholder="123" maxlength="4" class="w-full" />
</div>
</div>
<button type="button" class="btn w-full">Continue</button>
+53 -14
View File
@@ -25,14 +25,10 @@ toc:
---
{% from "macros/code_block.njk" import code_block %}
{% from "tabs.njk" import tabs %}
<h2 id="install-css">Install the CSS</h2>
<div class="alert my-6">
{% lucide "circle-alert" %}
<h2>If you don't want to use npm, you can follow the <a href="#install-manual" class="underline underline-offset-4">manual installation instructions</a>.</h2>
</div>
<ol class="[&>h3]:step steps mb-12 md:ml-4 md:border-l md:pl-8">
<li class="step">
<h4 class="mb-4 scroll-m-20 font-semibold tracking-tight" id="install-css-tailwind"><a href="#install-css-tailwind">Add Tailwind CSS</a></h4>
@@ -46,8 +42,17 @@ toc:
<div class="prose">
<p>Add Basecoat to your Tailwind CSS file.</p>
</div>
{% set code %}npm install hunvreus/basecoat{% endset %}
{{ code_block(code, "bash") }}
{% set code_npm %}npm install basecoat-css{% endset %}
{% set code_pnpm %}pnpm add basecoat-css{% endset %}
{% set code_bun %}bun add basecoat-css{% endset %}
{% set code_yarn %}yarn add basecoat-css{% endset %}
{% set tabsets = [
{ tab: "npm", panel: code_block(code_npm, class="") },
{ tab: "pnpm", panel: code_block(code_pnpm, class="") },
{ tab: "bun", panel: code_block(code_bun, class="") },
{ tab: "yarn", panel: code_block(code_yarn, class="") }
] %}
{{ tabs(id="npm-install", tabsets=tabsets) }}
<div class="prose">
<p>Alternatively, you can directly <a href="https://github.com/hunvreus/basecoat/blob/main/src/css/basecoat.css" target="_blank">copy the <code>basecoat.css</code> file</a> into your project.</p>
</div>
@@ -55,7 +60,7 @@ toc:
<li class="step mt-8">
<h3 class="mb-4 scroll-m-20 font-semibold tracking-tight" id="install-css-import"><a href="#install-css-import">Import basecoat in your CSS</a></h3>
{% set code %}@import "tailwindcss";
@import "basecoat";{% endset %}
@import "basecoat-css";{% endset %}
{{ code_block(code, "css") }}
</li>
<li class="step mt-8">
@@ -95,10 +100,23 @@ toc:
<h2 id="install-cli"><a href="#install-cli">Use the CLI</a></h2>
<div class="prose">
<p>If you're using Nunjucks or Jinja, you can use the CLI to install the CSS and macros for any of the complex components(e.g. <a href="/components/dialog">Dialog</a>). It will copy the JS and `<code>.html.jinja</code> or <code>.njk</code> macros into your code.</p>
<p>For example, to add the Dialog component, run:</p>
{% set code %}npx basecoatui add dialog{% endset %}
{{ code_block(code, "bash") }}
<p>If you're using Nunjucks or Jinja, you can use the CLI to install the CSS and macros for any of the complex components(e.g. <a href="/components/dialog">Dialog</a>). It will copy the JS and <code>.html.jinja</code> or <code>.njk</code> macros into your code.</p>
<p>For example, to add the Dialog component, run one of the following commands:</p>
</div>
{% set code_npm_exec %}npx basecoat-cli add dialog{% endset %}
{% set code_pnpm_exec %}pnpm dlx basecoat-cli add dialog{% endset %}
{% set code_bun_exec %}bunx basecoat-cli add dialog{% endset %}
{% set code_yarn_exec %}yarn dlx basecoat-cli add dialog{% endset %}
{% set exec_tabsets = [
{ tab: "npm", panel: code_block(code_npm_exec, class="") },
{ tab: "pnpm", panel: code_block(code_pnpm_exec, class="") },
{ tab: "bun", panel: code_block(code_bun_exec, class="") },
{ tab: "yarn", panel: code_block(code_yarn_exec, class="") }
] %}
{{ tabs(id="cli-execution", tabsets=exec_tabsets, label="Execute CLI commands to add components:") }}
<div class="prose my-6">
<p>You will be asked to confirm the template engine and destination directory for the Javascript and macros.</p>
</div>
@@ -140,10 +158,31 @@ toc:
<li>
Import the theme in your CSS file:
{% set code %}@import "tailwindcss";
@import "basecoat";
@import "basecoat-css";
@import "theme.css";{% endset %}
{{ code_block(code, "css") }}
</li>
</ul>
<p><a href="https://ui.shadcn.com/docs/theming" target="_blank">Read more about theming on the shadcn/ui website.</a></p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="https://ui.shadcn.com/docs/theming" target="_blank">
About shadcn/ui theming
{% lucide "arrow-right" %}
</a>
</div>
<h2 id="install-customization"><a href="#install-manual">Customization</a></h2>
<div class="prose">
<p>You can override default styles using Tailwind:</p>
{% set code %}<button class="btn font-normal ">Click me</button>{% endset %}
{{ code_block(code, "html") }}
<p>You can also obviously extend and/or override the existing styles with your own Tailwind files:</p>
{% set code %}@import "tailwindcss";
@import "basecoat-css";
@import "theme.css";{% endset %}
{{ code_block(code, "css") }}
<p>You can also use use the <a href="#install-theming">theme variables</a> to customize many aspects of the components (colors, fonts, sizes, etc).</p>
<p><b>If you want to make more drastic changes</b>, I recommend <a href="https://github.com/hunvreus/basecoat/blob/main/src/css/basecoat.css" target="_blank">copying the <code>basecoat.css</code> file</a> into your project and making your changes there.</p>
</div>
+5 -5
View File
@@ -46,7 +46,7 @@ toc:
<p>There's also a CLI to help you scaffold components and get set up fast.</p>
<p><a href="/docs/installation">Check out the installation guide</a> to get started.</p>
<p><a href="/installation">Check out the installation guide</a> to get started.</p>
</div>
<h2 id="how-can-i-help"><a href="#how-can-i-help">How can I help?</a></h2>
@@ -55,9 +55,9 @@ toc:
<p>Basecoat is 100% open source and free.</p>
<ul>
<li><a href="https://github.com/basecoat-ui/basecoat">Star it on GitHub</a></li>
<li><a href="https://github.com/basecoat-ui/basecoat/issues">Report bugs or request features</a></li>
<li><a href="https://github.com/basecoat-ui/basecoat/pulls">Submit a pull request</a></li>
<li><a href="https://github.com/hunvreus/basecoat">Star it on GitHub</a></li>
<li><a href="https://github.com/hunvreus/basecoat/issues">Report bugs or request features</a></li>
<li><a href="https://github.com/hunvreus/basecoat/pulls">Submit a pull request</a></li>
<li><a href="https://github.com/sponsors/hunvreus">Sponsor the project</a></li>
</ul>
</div>
</div>
+13 -5
View File
@@ -1,5 +1,7 @@
---
layout: layouts/layout.njk
title: Kitchen Sink
description: A collection of all the components available in Basecoat.
---
{% set components = [
{ label: "Accordion", id: "accordion" },
@@ -31,11 +33,17 @@ layout: layouts/layout.njk
{ label: "Tooltip", id: "tooltip" }
] %}
<div class="flex gap-x-10">
<div class="grid gap-4 flex-1">
{% for component in components %}
{% include "partials/kitchen-sink/" ~ component.id ~ ".njk" %}
{% endfor %}
<div class="flex gap-x-10 justify-center">
<div>
<header class="space-y-2 mb-8">
<h1 class="text-3xl font-semibold tracking-tight">{{ title }}</h1>
<p class="text-muted-foreground">{{ description }}</p>
</header>
<div class="grid gap-4 flex-1">
{% for component in components %}
{% include "partials/kitchen-sink/" ~ component.id ~ ".njk" %}
{% endfor %}
</div>
</div>
<div class="hidden text-sm xl:block w-full max-w-[200px]">
{% from "macros/toc.njk" import toc_nav %}
+7
View File
@@ -0,0 +1,7 @@
---
permalink: /robots.txt
eleventyExcludeFromCollections: true
---
Sitemap: {{ site.url }}/sitemap.xml
User-agent: *
Disallow:
+15
View File
@@ -0,0 +1,15 @@
---
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 not page.url.startsWith('/fragments/') %}
<url>
<loc>{{ site.url }}{{ page.url | url }}</loc>
<lastmod>{{ page.date.toISOString() }}</lastmod>
</url>
{% endif %}
{% endfor %}
</urlset>
+31 -52
View File
@@ -1,24 +1,21 @@
{
"name": "basecoat",
"version": "0.1.0",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "basecoat",
"version": "0.1.0",
"version": "0.0.1",
"license": "MIT",
"bin": {
"basecoat": "cli/index.js"
},
"workspaces": [
"packages/*"
],
"devDependencies": {
"@11ty/eleventy": "^3.0.0",
"@grimlink/eleventy-plugin-lucide-icons": "^2.1.4",
"@tailwindcss/cli": "^4.1.3",
"commander": "^13.1.0",
"concurrently": "^9.1.2",
"fs-extra": "^11.3.0",
"inquirer": "^12.6.0",
"prettier": "^3.5.3",
"tailwindcss": "^4.1.3"
}
@@ -260,7 +257,6 @@
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.5.tgz",
"integrity": "sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
@@ -285,7 +281,6 @@
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz",
"integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
@@ -307,7 +302,6 @@
"version": "10.1.10",
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz",
"integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/figures": "^1.0.11",
@@ -335,7 +329,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -345,14 +338,12 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/@inquirer/core/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
@@ -367,7 +358,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
@@ -380,7 +370,6 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
@@ -395,7 +384,6 @@
"version": "4.2.10",
"resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.10.tgz",
"integrity": "sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
@@ -418,7 +406,6 @@
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.12.tgz",
"integrity": "sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
@@ -441,7 +428,6 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz",
"integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -451,7 +437,6 @@
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.9.tgz",
"integrity": "sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
@@ -473,7 +458,6 @@
"version": "3.0.12",
"resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.12.tgz",
"integrity": "sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
@@ -495,7 +479,6 @@
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.12.tgz",
"integrity": "sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
@@ -518,7 +501,6 @@
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.5.0.tgz",
"integrity": "sha512-tk8Bx7l5AX/CR0sVfGj3Xg6v7cYlFBkEahH+EgBB+cZib6Fc83dwerTbzj7f2+qKckjIUGsviWRI1d7lx6nqQA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/checkbox": "^4.1.5",
@@ -548,7 +530,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.0.tgz",
"integrity": "sha512-6ob45Oh9pXmfprKqUiEeMz/tjtVTFQTgDDz1xAMKMrIvyrYjAmRbQZjMJfsictlL4phgjLhdLu27IkHNnNjB7g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
@@ -571,7 +552,6 @@
"version": "3.0.12",
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.12.tgz",
"integrity": "sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
@@ -595,7 +575,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.0.tgz",
"integrity": "sha512-KkXQ4aSySWimpV4V/TUJWdB3tdfENZUU765GjOIZ0uPwdbGIG6jrxD4dDf1w68uP+DVtfNhr1A92B+0mbTZ8FA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
@@ -620,7 +599,6 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz",
"integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -1353,7 +1331,6 @@
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"type-fest": "^0.21.3"
@@ -1382,7 +1359,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -1472,6 +1448,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/basecoat-cli": {
"resolved": "packages/cli",
"link": true
},
"node_modules/basecoat-css": {
"resolved": "packages/css",
"link": true
},
"node_modules/bcp-47": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz",
@@ -1617,7 +1601,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 12"
@@ -1705,7 +1688,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -1718,14 +1700,12 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/commander": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -2061,7 +2041,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
"integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
"dev": true,
"license": "MIT",
"dependencies": {
"chardet": "^0.7.0",
@@ -2076,7 +2055,6 @@
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
@@ -2196,7 +2174,6 @@
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
@@ -2296,7 +2273,6 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/gray-matter": {
@@ -2413,7 +2389,6 @@
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
@@ -2433,7 +2408,6 @@
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.6.0.tgz",
"integrity": "sha512-3zmmccQd/8o65nPOZJZ+2wqt76Ghw3+LaMrmc6JE/IzcvQhJ1st+QLCOo/iLS85/tILU0myG31a2TAZX0ysAvg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
@@ -2530,7 +2504,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -2626,7 +2599,6 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
@@ -3191,7 +3163,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
"integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^18.17.0 || >=20.5.0"
@@ -3279,7 +3250,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -3557,7 +3527,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
"integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -3591,7 +3560,6 @@
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
@@ -3601,7 +3569,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"license": "MIT"
},
"node_modules/section-matter": {
@@ -3708,7 +3675,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
@@ -3918,7 +3884,6 @@
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"os-tmpdir": "~1.0.2"
@@ -3964,14 +3929,12 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/type-fest": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
@@ -3991,7 +3954,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
@@ -4235,7 +4197,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz",
"integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -4243,6 +4204,24 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/cli": {
"name": "basecoat-cli",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"commander": "^13.1.0",
"fs-extra": "^11.3.0",
"inquirer": "^12.6.0"
},
"bin": {
"basecoat-cli": "dist/index.js"
}
},
"packages/css": {
"name": "basecoat-css",
"version": "0.1.0",
"license": "MIT"
}
}
}
+15 -16
View File
@@ -2,19 +2,20 @@
"name": "basecoat",
"version": "0.1.0",
"description": "shadcn/ui for the rest of us",
"author": "hunvreus",
"type": "module",
"private": true,
"workspaces": [
"packages/*"
],
"files": [
"css",
"js",
"nunjucks",
"jinja",
"cli"
"src/css",
"src/js",
"src/nunjucks",
"src/jinja"
],
"bin": {
"basecoat": "./cli/index.js"
},
"exports": {
".": "./css/basecoat.css"
".": "./src/css/basecoat.css"
},
"keywords": [
"components",
@@ -42,19 +43,17 @@
},
"homepage": "https://github.com/hunvreus/basecoat#readme",
"scripts": {
"css:dev": "tailwindcss -i ./docs/css/styles.css -o ./docs/src/assets/styles.css --watch",
"eleventy:serve": "eleventy --serve",
"dev": "concurrently \"npm:css:dev\" \"npm:eleventy:serve\"",
"build": "tailwindcss -i ./docs/css/styles.css -o ./docs/src/assets/styles.css --minify && eleventy"
"build": "node ./scripts/build.js",
"docs:build": "tailwindcss -i ./docs/css/styles.css -o ./docs/src/assets/styles.css --minify && eleventy",
"docs:dev:css": "tailwindcss -i ./docs/css/styles.css -o ./docs/src/assets/styles.css --watch",
"docs:dev:eleventy": "ELEVENTY_ENV=prod eleventy --serve",
"docs:dev": "concurrently \"npm:docs:dev:css\" \"npm:docs:dev:eleventy\""
},
"devDependencies": {
"@11ty/eleventy": "^3.0.0",
"@grimlink/eleventy-plugin-lucide-icons": "^2.1.4",
"@tailwindcss/cli": "^4.1.3",
"commander": "^13.1.0",
"concurrently": "^9.1.2",
"fs-extra": "^11.3.0",
"inquirer": "^12.6.0",
"prettier": "^3.5.3",
"tailwindcss": "^4.1.3"
}
+56
View File
@@ -0,0 +1,56 @@
# basecoat-cli
This package provides a Command Line Interface (CLI) to help you quickly scaffold [Basecoat](https://basecoatui.com) components into your project.
## Prerequisites
- Ensure you have Node.js and npm (or your preferred package manager) installed.
- For the components to function correctly, your project should have:
- [Tailwind CSS](https://tailwindcss.com/docs/installation) installed and configured.
- The `basecoat-css` package installed and imported (or `basecoat.css` manually added).
- [Alpine.js](https://alpinejs.dev/docs/installation) installed and initialized, as Basecoat components rely on it for interactivity.
## Usage
Use the `add` command to add components to your project:
```bash
# npx
npx basecoat-cli add [component]
# pnpm
pnpm dlx basecoat-cli add [component]
# bun
bunx basecoat-cli add [component]
# yarn
yarn dlx basecoat-cli add [component]
```
Replace `[component]` with the name of the component you wish to add (e.g., `dialog`, `select`) or leave it out if you want to pick from the list of available components
or install all of them at once.
The CLI will prompt you for:
1. **Template Engine:** Choose between Nunjucks (`.njk`) or Jinja (`.html.jinja`).
2. **Assets Directory:** The path where component assets (like JavaScript files) should be placed (e.g., `src/assets/js/components`).
3. **Macros Directory:** The path where component macros should be placed (e.g., `src/_includes/macros/components`).
The CLI will then copy the necessary JavaScript files and Nunjucks/Jinja macros for the selected component into the specified directories.
### Example
To add the `dialog` component:
```bash
npx basecoat-cli add dialog
```
## Documentation
For more detailed information on components, their usage, and customization options, please refer to the [Basecoat documentation](https://basecoatui.com/installation/#install-cli).
## License
[MIT](https://github.com/hunvreus/basecoat/blob/main/LICENSE.md)
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env node
import { Command } from 'commander';
import inquirer from 'inquirer';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
const program = new Command();
// Attempt to read version from the CLI's own package.json
let packageVersion = 'unknown';
try {
const cliScriptRunningDir = path.dirname(fileURLToPath(import.meta.url)); // .../packages/cli/dist
const cliPackageRootDir = path.resolve(cliScriptRunningDir, '..'); // .../packages/cli
const packageJsonPath = path.join(cliPackageRootDir, 'package.json');
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
packageVersion = JSON.parse(packageJsonContent).version;
} catch (error) {
console.warn('Could not read CLI package version:', error.message);
}
const DEFAULT_CONFIG = {
templateEngine: null,
templateDest: './components/basecoat',
scriptDest: './static/js/basecoat'
};
let config = { ...DEFAULT_CONFIG }; // User-specific config, potentially updated by prompts
// Discovers available components from the CLI's bundled assets.
async function getAvailableComponents() {
try {
const cliScriptRunningDir = path.dirname(fileURLToPath(import.meta.url));
const assetsDir = path.resolve(cliScriptRunningDir, 'assets');
const jsSourceDir = path.join(assetsDir, 'js');
const jsFiles = await fs.readdir(jsSourceDir);
return jsFiles
.filter(file => file.endsWith('.js'))
.map(file => path.basename(file, '.js'));
} catch (error) {
console.error("Error reading component source directories from CLI assets:", error);
return [];
}
}
// Ensures necessary configuration (template engine, destination paths) is set, prompting the user if needed.
async function ensureConfiguration() {
if (!config.templateEngine) {
const engineChoice = await inquirer.prompt([
{
type: 'list',
name: 'engine',
message: 'Which template engine are you using?',
choices: ['nunjucks', 'jinja'],
}
]);
config.templateEngine = engineChoice.engine;
}
const pathPrompts = [];
if (!config.templateDest || config.templateDest === DEFAULT_CONFIG.templateDest) {
pathPrompts.push({
type: 'input',
name: 'templateDest',
message: 'Where should template files be placed?',
default: DEFAULT_CONFIG.templateDest,
});
}
if (!config.scriptDest || config.scriptDest === DEFAULT_CONFIG.scriptDest) {
pathPrompts.push({
type: 'input',
name: 'scriptDest',
message: 'Where should script files be placed?',
default: DEFAULT_CONFIG.scriptDest,
});
}
if (pathPrompts.length > 0) {
const pathChoices = await inquirer.prompt(pathPrompts);
if (pathChoices.templateDest) config.templateDest = pathChoices.templateDest;
if (pathChoices.scriptDest) config.scriptDest = pathChoices.scriptDest;
}
}
// Copies a component's template and script files to the user's project.
async function addComponent(componentName) {
console.log(`\nProcessing component: ${componentName}...`);
const cliScriptRunningDir = path.dirname(fileURLToPath(import.meta.url));
const assetsDir = path.resolve(cliScriptRunningDir, 'assets');
const templateExt = config.templateEngine === 'jinja' ? '.html.jinja' : '.njk';
const templateSource = path.join(assetsDir, config.templateEngine, `${componentName}${templateExt}`);
const scriptSource = path.join(assetsDir, 'js', `${componentName}.js`);
const templateDestPath = path.resolve(process.cwd(), config.templateDest, `${componentName}${templateExt}`);
const scriptDestPath = path.resolve(process.cwd(), config.scriptDest, `${componentName}.js`);
try {
const templateExists = await fs.pathExists(templateSource);
const scriptExists = await fs.pathExists(scriptSource);
if (!templateExists) {
console.error(` Error: Template file for component '${componentName}' not found in CLI assets. Searched: ${templateSource}`);
// Optionally, list available template engines or files for that component if helpful
}
if (!scriptExists) {
console.error(` Error: Script file for component '${componentName}' not found in CLI assets. Searched: ${scriptSource}`);
}
if (!templateExists || !scriptExists) return; // Skip this component if sources are missing
await fs.ensureDir(path.dirname(templateDestPath));
await fs.ensureDir(path.dirname(scriptDestPath));
await fs.copyFile(templateSource, templateDestPath);
console.log(` -> Copied template to: ${templateDestPath}`);
await fs.copyFile(scriptSource, scriptDestPath);
console.log(` -> Copied script to: ${scriptDestPath}`);
} catch (error) {
console.error(` Error processing component ${componentName}:`, error);
}
}
// Setup main CLI program
program
.name('basecoat-cli')
.description('Add Basecoat components to your project')
.version(packageVersion);
// Define the 'add' command
program
.command('add')
.description('Add one or more Basecoat components to your project')
.argument('[components...]', 'Names of components to add (e.g., dialog select)')
.action(async (componentsArg) => {
let componentsToAdd = []; // Initialize as an empty array
try {
if (!componentsArg || componentsArg.length === 0) {
const availableComponents = await getAvailableComponents();
if (availableComponents.length === 0) {
console.error('Error: No components available in the CLI. Build might be corrupted.');
return;
}
const allComponentsChoice = 'All components';
const choices = [allComponentsChoice, ...availableComponents];
const answers = await inquirer.prompt([
{
type: 'list', // Changed from checkbox to list
name: 'selectedComponent', // Changed name for clarity
message: 'Which component(s) would you like to add?',
choices: choices,
}
]);
if (answers.selectedComponent === allComponentsChoice) {
componentsToAdd = availableComponents; // Add all
} else {
componentsToAdd = [answers.selectedComponent]; // Add the single selected one
}
} else {
// If components were provided as arguments, use them directly
// (This part of the logic remains, ensuring componentsArg is an array)
componentsToAdd = Array.isArray(componentsArg) ? componentsArg : [componentsArg];
}
if (!componentsToAdd || componentsToAdd.length === 0) {
console.log('No components selected. Exiting.');
return;
}
await ensureConfiguration(); // Confirm/gather destination paths
for (const componentName of componentsToAdd) {
await addComponent(componentName);
}
console.log('\nComponent addition process finished.');
} catch (error) {
if (error.isTtyError || error.constructor?.name === 'ExitPromptError') { // Handle inquirer cancellation
console.log('\nOperation cancelled by user.');
} else {
console.error('\nAn unexpected error occurred during the add command:', error);
}
process.exit(1); // Exit with error code for unexpected errors
}
});
program.parse(process.argv);
+45
View File
@@ -0,0 +1,45 @@
{
"name": "basecoat-cli",
"version": "0.1.1",
"description": "Add Basecoat components to your project",
"author": "hunvreus",
"license": "MIT",
"type": "module",
"bin": {
"basecoat-cli": "./dist/index.js"
},
"files": [
"dist/"
],
"dependencies": {
"commander": "^13.1.0",
"fs-extra": "^11.3.0",
"inquirer": "^12.6.0"
},
"keywords": [
"cli",
"components",
"component library",
"component system",
"ui",
"ui kit",
"shadcn",
"shadcn/ui",
"tailwind",
"tailwindcss",
"css",
"html",
"jinja",
"nunjucks",
"alpinejs"
],
"repository": {
"type": "git",
"url": "git+https://github.com/hunvreus/basecoat.git",
"directory": "packages/cli"
},
"bugs": {
"url": "https://github.com/hunvreus/basecoat/issues"
},
"homepage": "https://basecoatui.com/installation#install-cli"
}
+54
View File
@@ -0,0 +1,54 @@
# basecoat-css
This package provides the core CSS styles for [Basecoat](https://basecoatui.com), a component library built with Tailwind CSS.
## Prerequisites
Your project must have [Tailwind CSS](https://tailwindcss.com/docs/installation) installed and configured, as Basecoat relies on Tailwind utility classes and theming.
## Installation
You can install `basecoat-css` using your preferred package manager:
```bash
# npm
npm install basecoat-css
# yarn
yarn add basecoat-css
# pnpm
pnpm add basecoat-css
# bun
bun add basecoat-css
```
Alternatively, you can download the `basecoat.css` file directly from the [GitHub repository](https://github.com/hunvreus/basecoat/blob/main/src/css/basecoat.css) (or the `dist/basecoat.css` file from this package after installation) and include it in your project.
## Usage
After installation, import `basecoat-css` into your main CSS file. Make sure to import it **after** Tailwind CSS and **before** any custom theme overrides.
```css
/* Your main CSS file (e.g., styles.css) */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "basecoat-css"; /* Or the path to basecoat.css if manually added */
/* Your custom styles or theme overrides (optional) */
/* @import "your-theme.css"; */
```
Once imported, you can start using Basecoat's component classes in your HTML.
## Documentation
For more detailed information on components, their usage, and customization options, please refer to the [Basecoat documentation](https://basecoatui.com/installation/#install-css).
## License
[MIT](https://github.com/hunvreus/basecoat/blob/main/LICENSE.md)
+40
View File
@@ -0,0 +1,40 @@
{
"name": "basecoat-css",
"version": "0.1.2",
"description": "Tailwind CSS for Basecoat components",
"author": {
"name": "hunvreus",
"url": "https://x.com/hunvreus"
},
"license": "MIT",
"main": "dist/basecoat.css",
"style": "dist/basecoat.css",
"files": [
"dist/"
],
"keywords": [
"components",
"component library",
"component system",
"ui",
"ui kit",
"shadcn",
"shadcn/ui",
"tailwind",
"tailwindcss",
"css",
"html",
"jinja",
"nunjucks",
"alpinejs"
],
"repository": {
"type": "git",
"url": "git+https://github.com/hunvreus/basecoat.git",
"directory": "packages/css"
},
"bugs": {
"url": "https://github.com/hunvreus/basecoat/issues"
},
"homepage": "https://basecoatui.com/installation#install-css"
}
+112
View File
@@ -0,0 +1,112 @@
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
// Resolve project root, assuming this script is in <project_root>/scripts/
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..');
// Ensures a directory exists. If it doesn't, it's created recursively.
async function ensureDir(dirPath) {
try {
await fs.access(dirPath);
} catch (error) {
if (error.code === 'ENOENT') {
await fs.mkdir(dirPath, { recursive: true });
} else {
throw error;
}
}
}
// Cleans a directory by removing all its contents (files and subdirectories).
async function cleanDir(directory) {
try {
await fs.access(directory);
const entries = await fs.readdir(directory, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
await fs.rm(entryPath, { recursive: true, force: true });
} else {
await fs.unlink(entryPath);
}
}
console.log(`Cleaned directory: ${directory}`);
} catch (error) {
if (error.code === 'ENOENT') {
console.log(`Directory not found, no need to clean: ${directory}`);
} else {
console.error(`Error cleaning directory ${directory}:`, error);
throw error;
}
}
}
// Recursively copies a directory.
async function copyDirRecursive(src, dest) {
await ensureDir(dest);
const entries = await fs.readdir(src, { withFileTypes: true });
for (let entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
await copyDirRecursive(srcPath, destPath);
} else {
await fs.copyFile(srcPath, destPath);
}
}
}
// Main build function to prepare packages for publishing.
async function build() {
console.log('Starting build process...');
// Define all necessary paths
const cliPackageDir = path.join(projectRoot, 'packages', 'cli');
const cliDistDir = path.join(cliPackageDir, 'dist');
const cliDistAssetsDir = path.join(cliDistDir, 'assets');
const cssPackageDir = path.join(projectRoot, 'packages', 'css');
const cssDistDir = path.join(cssPackageDir, 'dist');
const srcDir = path.join(projectRoot, 'src');
const srcCssDir = path.join(srcDir, 'css');
const srcJsDir = path.join(srcDir, 'js');
const srcNunjucksDir = path.join(srcDir, 'nunjucks');
const srcJinjaDir = path.join(srcDir, 'jinja');
// Clean previous build artifacts
console.log('Cleaning distribution directories...');
await cleanDir(cliDistDir);
await cleanDir(cssDistDir);
// Build CLI package
console.log('Building CLI package...');
await ensureDir(cliDistDir); // Ensure base dist dir exists
await ensureDir(cliDistAssetsDir); // Ensure assets dir within dist exists
await fs.copyFile(path.join(cliPackageDir, 'index.js'), path.join(cliDistDir, 'index.js'));
console.log(`Copied CLI index.js to ${cliDistDir}`);
await copyDirRecursive(srcJsDir, path.join(cliDistAssetsDir, 'js'));
console.log(`Copied JS assets to ${path.join(cliDistAssetsDir, 'js')}`);
await copyDirRecursive(srcNunjucksDir, path.join(cliDistAssetsDir, 'nunjucks'));
console.log(`Copied Nunjucks assets to ${path.join(cliDistAssetsDir, 'nunjucks')}`);
await copyDirRecursive(srcJinjaDir, path.join(cliDistAssetsDir, 'jinja'));
console.log(`Copied Jinja assets to ${path.join(cliDistAssetsDir, 'jinja')}`);
// Build CSS package
console.log('Building CSS package...');
await ensureDir(cssDistDir); // Ensure dist dir exists for css package
await fs.copyFile(path.join(srcCssDir, 'basecoat.css'), path.join(cssDistDir, 'basecoat.css'));
console.log(`Copied basecoat.css to ${cssDistDir}`);
console.log('Build process finished successfully!');
}
build().catch(error => {
console.error('Build failed:', error);
process.exit(1);
});
+5
View File
@@ -716,6 +716,11 @@
.select {
@apply appearance-none border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent pl-3 pr-9 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 h-9;
@apply bg-[image:var(--chevron-down-icon-50)] bg-no-repeat bg-position-[center_right_0.75rem] bg-size-[1rem];
option,
optgroup {
@apply bg-popover text-popover-foreground;
}
}
}
+61 -5
View File
@@ -3,6 +3,7 @@
@param id {string} [optional] - Unique identifier for the dropdown component.
@param trigger {string} [optional] - HTML content for the button that triggers the dropdown.
@param menu {array} [optional] - Array of menu items for the dropdown.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
@param content_attrs {object} [optional] - Additional HTML attributes for the dropdown content div.
@@ -10,6 +11,7 @@
{% macro dropdown_menu(
id=None,
trigger=None,
menu=None,
main_attrs={},
trigger_attrs={},
content_attrs={}
@@ -19,8 +21,8 @@
x-data="dropdownMenu"
@click.away="open = false"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in main_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% for key, value in main_attrs %}
{% if key != "class" %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% if trigger %}
@@ -33,7 +35,7 @@
id="{{ id }}-trigger"
aria-controls="{{ id }}-menu"
{% endif %}
{% for key, value in trigger_attrs.items() %}
{% for key, value in trigger_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
@@ -45,13 +47,67 @@
aria-hidden="true"
x-bind="$content"
{% if id %}id="{{ id }}-menu"{% endif %}
{% for key, value in content_attrs.items() %}
{% for key, value in content_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
<nav role="menu">
{{ caller() if caller }}
{% if menu %}
{{ render_dropdown_items(menu, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{% endif %}
</nav>
</div>
</div>
{% endmacro %}
{#
Renders dropdown menu items recursively.
@param items {array} - The array of items to render.
@param parent_id_prefix {string} [optional] - Prefix for generating element IDs.
#}
{% macro render_dropdown_items(items, parent_id_prefix="items") %}
{% for item in items.items() %}
{% set item_id = parent_id_prefix ~ "-" ~ loop.index %}
{% if item.type == "group" %}
{% set group_label_id = item.id if item.id else "group-label-" + item_id %}
<div
role="group"
aria-labelledby="{{ group_label_id }}"
{% for key, value in item.attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
{{ render_dropdown_items(item.items, item_id) if item.items }}
</div>
{% elif item.type == "separator" %}
<hr role="separator" />
{% elif item.type == "item" or not item.type %}
{% if item.url %}
<a
role="menuitem"
href="{{ item.url }}"
{% for key, value in item.attrs.items() %}
{% if key != "url" %} {{ key }}="{{ value }}" {% endif %}
{% endfor %}
>
{{ item.label | safe }}
</a>
{% else %}
<button
role="menuitem"
type="button"
{% for key, value in item.attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ item.label | safe }}
</button>
{% endif %}
{% endif %}
{% endfor %}
{% endmacro %}
+45 -2
View File
@@ -10,12 +10,13 @@
@param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
@param input_attrs {object} [optional] - Additional HTML attributes for the hidden input.
@param search_placeholder {string} [optional] [default="Search entries..."] - Placeholder text for the search input (combobox only).
@param is_combobox {boolean} [optional] [default=False] - Renders a combobox with search functionality if true.
@param is_combobox {boolean} [optional] [default=False] - Renders a combobox with search functionality if True.
#}
{% macro select(
id=None,
selected=None,
name=None,
items=None,
main_attrs={},
trigger_attrs={},
content_attrs={},
@@ -91,7 +92,11 @@
{{ key }}="{{ value }}"
{% endfor %}
>
{{ caller() if caller }}
{% if items %}
{{ render_select_items(items, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{% endif %}
</div>
</div>
{% if name is defined %}
@@ -105,4 +110,42 @@
>
{% endif %}
</div>
{% endmacro %}
{#
Renders a list of items for the select component.
@param items {array} - The array of items to render.
@param parent_id_prefix {string} [optional] - The prefix for the item id.
#}
{% macro render_select_items(items, parent_id_prefix="items") %}
{% for item in items.items() %}
{% set item_id = parent_id_prefix ~ "-" ~ loop.index %}
{% if item.type == "group" %}
{% set group_label_id = item.id if item.id else "group-label-" + item_id %}
<div
role="group"
aria-labelledby="{{ group_label_id }}"
{% for key, value in item.attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
{{ render_select_items(item.items, item_id) if item.items }}
</div>
{% elif item.type == "separator" %}
<hr role="separator" />
{% elif item.type == "item" or not item.type %}
<div
role="option"
data-value="{{ item.value }}"
{% for key, value in item.attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ item.label | safe }}
</div>
{% endif %}
{% endfor %}
{% endmacro %}
+138
View File
@@ -0,0 +1,138 @@
{#
Renders a sidebar component.
@param id {string} [optional] - Unique identifier for the sidebar component.
@param label {string} [optional] [default="Sidebar navigation"] - Label for the sidebar navigation.
@param open {boolean} [optional] [default=True] - Whether the sidebar is open.
@param side {string} [optional] - Side of the sidebar to display.
@param header {string} [optional] - Header content for the sidebar.
@param footer {string} [optional] - Footer content for the sidebar.
@param menu {array} [optional] - Array of menu items for the sidebar.
#}
{% macro sidebar(
id=None,
label="Sidebar navigation",
open=True,
side=None,
header=None,
footer=None,
menu=None,
main_attrs={},
header_attrs={},
content_attrs={},
footer_attrs={},
content_wrapper_attrs=None
) %}
<div
{% if id %}id="{{ id }}"{% endif %}
class="sidebar {{ main_attrs.class }}"
data-uninitialized
data-side="{{ side if side else "left" }}"
aria-hidden="{{ "true" if not open else "false" }}"
{{ "inert" if not open }}
{% for key, value in main_attrs.items() %}
{% if key != "class" %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
x-data="sidebar({{ "true" if open else "false" }})"
x-bind="$main"
>
<nav
aria-label="{{ label }}"
>
{% if header %}
<header
{% for key, value in header_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ header | safe }}
</header>
{% endif %}
<section
{% for key, value in content_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if menu %}
{{ render_sidebar_content(menu, id ~ "-content" if id else "content") }}
{% else %}
{{ caller() if caller }}
{% endif %}
</section>
{% if footer %}
<footer
{% for key, value in footer_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ footer | safe }}
</footer>
{% endif %}
</nav>
</div>
{% endmacro %}
{#
Renders sidebar content recursively (groups, items, submenus, separators).
@param items {array} - The array of items to render.
@param parent_id_prefix {string} [optional] - Prefix for generating element IDs.
#}
{% macro render_sidebar_content(items, parent_id_prefix="content") %}
{% for item in items.items() %}
{% set item_id = parent_id_prefix ~ "-" ~ loop.index %}
{% if item.type == "group" %}
{% set group_label_id = item.id if item.id else "group-label-" + item_id %}
<div
role="group"
{% if item.label %}aria-labelledby="{{ group_label_id }}"{% endif %}
{% for key, value in item.attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if item.label %}
<h3 id="{{ group_label_id }}">{{ item.label }}</h3>
{% endif %}
<ul>
{{ render_sidebar_content(item.items, item_id) if item.items }}
</ul>
</div>
{% elif item.type == "separator" %}
<hr role="separator"/>
{% elif item.type == "submenu" %}
<li>
<details
id="submenu-{{ item_id }}"
{{ "open" if item.open }}
{% for key, value in item.attrs.items() %}
{% if key != "open" %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<summary aria-controls="submenu-{{ item_id }}-content">
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label }}
</summary>
<ul id="submenu-{{ item_id }}-content">
{{ render_sidebar_content(item.items, item_id) if item.items }}
</ul>
</details>
</li>
{% elif item.type == "item" or not item.type %}
<li>
<a
href="{{ item.url }}"
{{ 'aria-current="page"' if item.current }}
{% for key, value in item.attrs.items() %}
{% if key != "href" and key != "aria-current" %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
<span>{{ item.label }}</span>
</a>
</li>
{% endif %}
{% endfor %}
{% endmacro %}
+2 -2
View File
@@ -37,7 +37,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
{% for tabset in tabsets %}
{% for tabset in tabsets.items() %}
<button
type="button"
role="tab"
@@ -54,7 +54,7 @@
{% endfor %}
</nav>
{% for tabset in tabsets %}
{% for tabset in tabsets.items() %}
{% if tabset.panel %}
<div
role="tabpanel"
+109 -84
View File
@@ -3,19 +3,9 @@
Can render the initial container or be used with htmx OOB swaps to add toasts dynamically.
@param id {string} [optional] [default="toaster"] - Unique identifier for the toaster container.
@param toasts {array} [optional] - An array of toast objects to render initially.
Each toast object can have:
- category {string} [optional]: Type of toast ('success', 'error', 'info', 'warning'). Determines icon and ARIA role.
- title {string} [optional]: The main title text of the toast.
- description {string} [optional]: The secondary description text.
- duration {number} [optional] [default=5000]: Duration in milliseconds before the toast auto-dismisses.
- action {object} [optional]: Defines an action button.
- label {string}: Button text.
- click {string}: JavaScript code to execute on click (e.g., '$dispatch(\'custom-event\')').
- url {string}: URL for an anchor link button.
- cancel {object} [optional]: Defines a cancel/dismiss button (similar structure to action).
@param toasts {array} [optional] - An array of toast objects to render initially. See the <code>toast()</code> macro for more details.
@param main_attrs {object} [optional] - Additional HTML attributes for the main toaster container div.
@param is_fragment {boolean} [optional] [default=False] - If true, renders only the toast elements with hx-swap-oob="beforeend", suitable for htmx responses. Skips script and template inclusion.
@param is_fragment {boolean} [optional] [default=False] - If True, renders only the toast elements with hx-swap-oob="beforeend", suitable for htmx responses. Skips script and template inclusion.
#}
{% macro toaster(
id="toaster",
@@ -23,13 +13,6 @@
main_attrs=None,
is_fragment=False
) %}
{% set icons = {
'success': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check-icon lucide-circle-check"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>',
'error': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x-icon lucide-circle-x"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>',
'info': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info-icon lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
'warning': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert-icon lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>'
} %}
<div
id="{{ id }}"
class="toaster"
@@ -38,64 +21,14 @@
{% endfor %}
{% if is_fragment %}hx-swap-oob="beforeend"{% endif %}
>
{% for toast in toasts %}
<div
class="toast"
role="{{ 'alert' if toast.category == 'error' else 'status' }}"
aria-atomic="true"
aria-hidden="false"
{% if toast.category %}data-category="{{ toast.category }}"{% endif %}
x-data="toast('{{ toast.category }}', {{ toast.duration }})"
x-bind="$toastBindings"
>
<div class="toast-content">
<div class="flex items-center justify-between gap-x-3 p-4 [&>svg]:size-4 [&>svg]:shrink-0 [&>[role=img]]:size-4 [&>[role=img]]:shrink-0 [&>[role=img]>svg]:size-4">
{% if toast.category in ["error", "success", "info", "warning"] %}
{{ icons[toast.category] | safe }}
{% endif %}
<section class="flex-1 flex flex-col gap-0.5 items-start">
{% if toast.title %}
<h2 class="font-medium">{{ toast.title }}</h2>
{% endif %}
{% if toast.description %}
<p class="text-muted-foreground">{{ toast.description }}</p>
{% endif %}
</section>
{% if toast.action or toast.cancel %}
<footer class="flex flex-col gap-1 self-start">
{% if toast.action %}
{% if toast.action.click %}
<button
type="button"
class="btn h-6 text-xs px-2.5 rounded-sm"
@click="{{ toast.action.click }}"
>{{ toast.action.label }}</button>
{% elif toast.action.url %}
<a
href="{{ toast.action.url }}"
class="btn h-6 text-xs px-2.5 rounded-sm"
>{{ toast.action.label }}</a>
{% endif %}
{% endif %}
{% if toast.cancel %}
{% if toast.cancel.click %}
<button
type="button"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
@click="{{ toast.cancel.click }}"
>{{ toast.cancel.label }}</button>
{% elif toast.cancel.url %}
<a
href="{{ toast.cancel.url }}"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
>{{ toast.cancel.label }}</a>
{% endif %}
{% endif %}
</footer>
{% endif %}
</div>
</div>
</div>
{% for item in toasts.items() %}
{{ toast(
category=item.category,
title=item.title,
description=item.description,
action=item.action,
cancel=item.cancel
) }}
{% endfor %}
</div>
@@ -113,16 +46,16 @@
<span aria-hidden="true" role="img" x-html="config.icon"></span>
</template>
<template x-if="!config.icon && config.category === 'success'">
{{ icons.success | safe }}
{{ toast_icons.success | safe }}
</template>
<template x-if="!config.icon && config.category === 'error'">
{{ icons.error | safe }}
{{ toast_icons.error | safe }}
</template>
<template x-if="!config.icon && config.category === 'info'">
{{ icons.info | safe }}
{{ toast_icons.info | safe }}
</template>
<template x-if="!config.icon && config.category === 'warning'">
{{ icons.warning | safe }}
{{ toast_icons.warning | safe }}
</template>
<section class="flex-1 flex flex-col gap-0.5 items-start">
<template x-if="config.title">
@@ -138,7 +71,7 @@
<button
type="button"
class="btn h-6 text-xs px-2.5 rounded-sm"
@click="$evaluate(config.action.click)"
@click="executeAction(config.action.click)"
x-text="config.action.label"
></button>
</template>
@@ -153,7 +86,7 @@
<button
type="button"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
@click="$evaluate(config.cancel.click)"
@click="executeAction(config.cancel.click)"
x-text="config.cancel.label"
></button>
</template>
@@ -165,9 +98,101 @@
></a>
</template>
</footer>
</template>
</div>
</div>
</div>
</template>
{% endif %}
{% endmacro %}
{% endmacro %}
{#
Renders a single toast message.
@param category {string} [optional] [default="success"] - Type of toast ('success', 'error', 'info', 'warning'). Determines icon and ARIA role.
@param title {string} [optional] - The main title text of the toast.
@param description {string} [optional] - The secondary description text.
@param action {object} [optional] - Defines an action button.
- label {string}: Button text.
- click {string}: JavaScript code to execute on click (e.g., '$dispatch(\'custom-event\')').
- url {string}: URL for an anchor link button.
@param cancel {object} [optional] - Defines a cancel/dismiss button (similar structure to action).
- label {string}: Button text.
- click {string}: JavaScript code to execute on click (e.g., '$dispatch(\'custom-event\')').
- url {string}: URL for an anchor link button.
#}
{% macro toast(
category="success",
title="",
description="",
action=None,
cancel=None
) %}
<div
class="toast"
role="{{ 'alert' if category == 'error' else 'status' }}"
aria-atomic="true"
aria-hidden="false"
{% if category %}data-category="{{ category }}"{% endif %}
x-data="toast({
category: '{{ category }}',
duration: {{ duration or 'null' }}
})"
x-bind="$toastBindings"
>
<div class="toast-content">
<div class="flex items-center justify-between gap-x-3 p-4 [&>svg]:size-4 [&>svg]:shrink-0 [&>[role=img]]:size-4 [&>[role=img]]:shrink-0 [&>[role=img]>svg]:size-4">
{% if category in ["error", "success", "info", "warning"] %}
{{ toast_icons[category] | safe }}
{% endif %}
<section class="flex-1 flex flex-col gap-0.5 items-start">
{% if title %}
<h2 class="font-medium">{{ title }}</h2>
{% endif %}
{% if description %}
<p class="text-muted-foreground">{{ description }}</p>
{% endif %}
</section>
{% if action or cancel %}
<footer class="flex flex-col gap-1 self-start">
{% if action %}
{% if action.click %}
<button
type="button"
class="btn h-6 text-xs px-2.5 rounded-sm"
@click="{{ action.click }}"
>{{ action.label }}</button>
{% elif action.url %}
<a
href="{{ action.url }}"
class="btn h-6 text-xs px-2.5 rounded-sm"
>{{ action.label }}</a>
{% endif %}
{% endif %}
{% if cancel %}
{% if cancel.click %}
<button
type="button"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
@click="{{ cancel.click }}"
>{{ cancel.label }}</button>
{% elif cancel.url %}
<a
href="{{ cancel.url }}"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
>{{ toast.cancel.label }}</a>
{% endif %}
{% endif %}
</footer>
{% endif %}
</div>
</div>
</div>
{% endmacro %}
{% set toast_icons = {
'success': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check-icon lucide-circle-check"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>',
'error': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x-icon lucide-circle-x"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>',
'info': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info-icon lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
'warning': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert-icon lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>'
} %}
+4 -2
View File
@@ -10,8 +10,10 @@ window.basecoat.registerToast = function(Alpine) {
timeoutId: null,
init() {
this.timeoutDuration = config.duration || (config.category === 'error' ? 5000 : 3000);
this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
if (config.duration !== -1) {
this.timeoutDuration = config.duration || (config.category === 'error' ? 5000 : 3000);
this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
}
this.open = true;
this.$watch('$store.toaster.isPaused', (isPaused) => {
if (!this.open) return;