Compare commits

...
35 Commits
Author SHA1 Message Date
Ronan Berder c771c8256b Compiling CSS for site 2025-12-21 14:43:07 +08:00
Ronan Berder 2adb4c4667 Bumping up version unmber to 0.3.9 2025-12-21 14:39:44 +08:00
Ronan Berder cbb1119699 Addressing regression bug 2025-12-21 14:38:47 +08:00
Ronan Berder 88db2e84ce Bumping up version unmber to 0.3.8 2025-12-21 14:36:58 +08:00
Ronan Berder a7f6f717d1 Fix #112 and #61 2025-12-21 14:20:09 +08:00
Ronan Berder 91c3932fe7 Cleaning up 2025-12-18 09:00:05 +08:00
Ronan Berder f977492bf6 Removing FUNDING.yml 2025-12-18 08:57:48 +08:00
Ronan Berder 21ba8ecb9e Bumping version number 2025-12-10 15:00:14 +08:00
Ronan Berder c0bf373890 Fixing edge case for command and select with data-keywords filtering, updating macros for command and sidebar 2025-12-10 14:49:32 +08:00
Ronan Berder 77680f9e2b Bumping version number 2025-11-17 10:53:40 +08:00
Ronan Berder 2afa2ae02e Fixing and extending combobox and command 2025-11-17 10:45:39 +08:00
Ronan Berder 23585e3161 Fixing #107 2025-11-14 23:58:35 +08:00
Ronan Berder 7e3324edf2 Bumping up the version number 2025-11-14 19:51:01 +08:00
Ronan Berder cda08f3c95 Merge branch 'main' of github.com:hunvreus/basecoat 2025-11-14 19:38:29 +08:00
Ronan Berder 0b46e05a29 Draft llms.txt 2025-11-14 19:38:11 +08:00
Ronan Berder 7322220788 Merge pull request #106 from volfpeter/fix/add-command-to-combined-js-bundle
Fix: include command in combined JS bundle + fix command examples
2025-11-14 18:45:28 +08:00
Peter Volf 4c86ce9bfc fix: include command in combined JS bundle + fix command examples 2025-11-13 10:09:58 +01:00
Ronan Berder bff89833d4 Attempting to fix #84 2025-11-12 09:56:02 +08:00
Ronan Berder 770b12f82f 0.3.3 2025-11-05 11:40:56 +08:00
Ronan Berder b8ac469755 Merge branch 'development' 2025-11-05 11:37:34 +08:00
Ronan Berder 14abbdc013 Accepting theirs changes on docs/src/assets/styles.css to avoid conflicts from Tailwind 2025-11-05 11:37:28 +08:00
Ronan Berder f367ebac2a Finishing Field 2025-11-05 11:25:13 +08:00
Ronan Berder b8d8b1741c Fixing issues with field 2025-11-04 20:41:23 +08:00
Ronan Berder f82b1d79d4 Cleaning up Button Group 2025-11-04 12:46:00 +08:00
Ronan Berder eed1938d2f Adding Icons section 2025-11-03 14:14:56 +08:00
Ronan Berder e5aed697c8 Adding spinner 2025-11-03 13:51:40 +08:00
Ronan Berder 0c16664e4b Adding Input Group, cleaning up other new components 2025-11-03 11:51:39 +08:00
Ronan Berder ab44d6d48a Adding empty, field and item, working on input group 2025-11-02 11:56:40 +08:00
Ronan Berder 15473865b4 Cleaning up button group 2025-10-31 08:56:44 +08:00
Ronan Berder 6148716f68 Adding kbd and draft command 2025-10-30 19:09:21 +08:00
Ronan Berder 045d13cd49 Added kbd and command 2025-10-30 10:03:34 +07:00
Ronan Berder 2962e980eb Minor fixes 2025-10-29 10:37:49 +07:00
Ronan Berder 382b8f6fa2 Update popover.njk 2025-08-31 20:29:09 +08:00
Ronan Berder d644cb9efd Merge pull request #90 from dmjio/patch-1
Update to sidebar.njk
2025-08-21 22:59:31 +08:00
David M. Johnson 1c1a413114 Update sidebar.njk
alphabetize
2025-08-21 08:56:35 -05:00
50 changed files with 5612 additions and 709 deletions
+2 -1
View File
@@ -9,6 +9,7 @@ export default async function(eleventyConfig) {
eleventyConfig.setInputDirectory("./docs/src");
eleventyConfig.addPassthroughCopy({"docs/src/assets": "assets"});
eleventyConfig.addPassthroughCopy({ "src/js": "assets/js" });
eleventyConfig.addPassthroughCopy({ "docs/src/llms.txt": "llms.txt" });
eleventyConfig.addPlugin(eleventyLucideicons);
eleventyConfig.addShortcode("fetchCode", function(filePath) {
const absolutePath = path.resolve(process.cwd(), filePath);
@@ -43,4 +44,4 @@ export default async function(eleventyConfig) {
{ autoescape: true }
);
eleventyConfig.setLibrary("njk", nunjucksEnvironment);
}
}
+1
View File
@@ -0,0 +1 @@
docs/src/assets/styles.css merge=theirs
-1
View File
@@ -1 +0,0 @@
github: [hunvreus]
+1
View File
@@ -1,5 +1,6 @@
.*
!.gitignore
!.gitattributes
!.eleventy.js
_site/
+112
View File
@@ -0,0 +1,112 @@
# Basecoat – Agent Guide
## What is Basecoat?
Basecoat is a vanilla JavaScript/HTML/CSS port of shadcn/ui components. It reuses shadcn/ui's design patterns and Tailwind CSS classes while maintaining semantic HTML, native browser elements, and full accessibility (ARIA attributes, proper hierarchies, keyboard navigation). The goal is to provide shadcn-quality components without React dependencies.
## Quickstart
- Install deps: `npm i`
- Dev docs server: `npm run docs:dev`
- Build CSS/JS: `npm run build`
- Build static docs: `npm run docs:build`
## What to edit (and what not)
- Edit sources only:
- `src/css/basecoat.css` – All component CSS classes (Tailwind + custom)
- `src/js/*.js` – Individual component JS files (kebab-case, ESM) + `basecoat.js` (component registry)
- `src/nunjucks/*.njk` and `src/jinja/*.html.jinja` – Component template macros
- `docs/src/components/*.njk` – Component documentation pages with examples
- `docs/src/_includes/` – Layout templates, partials, and navigation
- Do not edit build outputs:
- `packages/*/dist` – built CSS/JS bundles
- `_site/` – generated docs site
## Project layout
- Workspaces: root npm workspace with publishable packages in `packages/*`.
- Packages:
- `packages/css` – built CSS and JS bundles under `dist/`.
- `packages/cli` – CLI; distributed assets live under `dist/`.
- Docs site: `docs/src` (Eleventy); output goes to `_site/`.
## Conventions
- ESM everywhere (`import`/`export`).
- Prettier defaults (2 spaces; single quotes per file preference).
- Filenames for assets: kebab-case (e.g., `dropdown-menu.js`, `basecoat.cdn.css`).
- JS naming: `camelCase` for vars/functions; `PascalCase` for DOM classes and exported component names when applicable.
## Design Principles
- **Minimal code**: Prefer native HTML elements and browser behavior over JavaScript abstractions
- **Accessibility-first**: Semantic HTML, proper ARIA attributes, keyboard navigation
- **CSS patterns**:
- Use `border` for lines/separators (not `background` on `<hr>`)
- Use `:is()` and `:has()` for compact selectors
- Use `isolation: isolate` to contain z-index stacking contexts
- Use `data-*` attributes for styling states, `aria-*` for accessibility semantics
- **Component patterns**:
- Dialog variants use wrapper classes (e.g., `.command-dialog` wraps `.command`)
- JavaScript components register with `window.basecoat.register()`
- Emit custom events for user actions (e.g., `command:select`)
- Filter disabled options (`aria-disabled="true"`) from all interactions
- **When NOT to create a component**:
- If it's just composition of existing elements (users can do it with standard HTML/Tailwind)
- If behavior is fully native (e.g., fieldset, legend)
## Adding new components
Before creating a component, ask:
1. Does shadcn/ui have it? → Port it
2. Is it pure composition? → Document pattern, no component
3. Does it need JS behavior? → Create component
4. Is it just CSS? → Add to basecoat.css, document usage
Examples:
- Field/Fieldset: CSS-only patterns (no JS component)
- Empty/Item/Input Group: Pure composition (docs only)
- Command/Select: JS behavior required (full component)
## Common tasks
- Update a component's CSS/JS: Edit `src/css/basecoat.css` and/or `src/js/*.js`.
- Update a component template: Edit `src/nunjucks/*.njk` and/or `src/jinja/*.html.jinja`.
- Add a new component: Create JS, CSS, templates, and docs page under `docs/src/components/*.njk`.
- Update navigation: Edit files in `docs/src/_includes/partials/`.
## Testing
No automated testing configured. User validates manually via the docs site (`npm run docs:dev`).
## Commits and PRs
- Commits: concise, imperative (≤72 chars), optional scope.
- Example: `select: prevent change event on init`.
- PRs: include description, linked issues (e.g., `Closes #123`), and before/after screenshots or code snippets for visual/behavioral changes.
- Keep PRs focused; update docs examples when components or APIs change.
## Environment and tooling
- Node 18+ recommended.
- Eleventy config: `.eleventy.js`.
- Tailwind via `@tailwindcss/cli` v4.
- Build uses Tailwind CLI and `terser`; both are provided via devDependencies.
## Git workflow
- `.gitattributes` configures merge strategies for generated files
- `docs/src/assets/styles.css` auto-resolves to incoming changes (regenerated by Tailwind)
- Always run `npm run build` before publishing packages
## Publishing packages
1. Bump version in root `package.json`
2. Run `npm run build` to generate dist files
3. Run `npm publish --workspaces` to publish both packages
## Package references
- `packages/css` usage and details: see `packages/css/README.md`.
- `packages/cli` usage and details: see `packages/cli/README.md`.
+1 -1
View File
@@ -27,7 +27,7 @@
@apply font-medium underline underline-offset-4;
}
code:not(pre > code) {
@apply relative rounded-md bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm;
@apply relative rounded-md bg-muted/50 px-[0.3rem] py-[0.2rem] font-mono text-sm;
}
blockquote {
@apply border-l-4 border-border pl-4;
+1 -1
View File
@@ -70,6 +70,7 @@
<link rel="stylesheet" href="/assets/styles.css">
{# JS #}
<script src="/assets/js/basecoat.js" defer></script>
<script src="/assets/js/command.js" defer></script>
<script src="/assets/js/dropdown-menu.js" defer></script>
<script src="/assets/js/popover.js" defer></script>
<script src="/assets/js/select.js" defer></script>
@@ -98,7 +99,6 @@
<script>
document.addEventListener('htmx:historyRestore', () => {
if (window.basecoat) window.basecoat.initAll();
if (window.Alpine) Alpine.initTree(document.body);
});
</script>
{% if head %}{{ head | safe }}{% endif %}
+2 -2
View File
@@ -1,6 +1,6 @@
{% macro code_block(code, language="html", class="my-4") %}
<div class="relative {{ class }}">
<pre class="grid text-sm max-h-[650px] overflow-y-auto rounded-xl scrollbar"><code class="language-{{ language }} !bg-muted !p-3.5">{{- code | escape -}}</code></pre>
<pre class="grid text-sm max-h-[650px] overflow-y-auto rounded-xl scrollbar"><code class="language-{{ language }} !bg-muted/40 !p-3.5">{{- code | escape -}}</code></pre>
<button
onclick="
@@ -16,7 +16,7 @@
"
class="btn-icon-ghost size-8 absolute right-2.5 top-2 text-muted-foreground hover:text-foreground group"
>
{% lucide "clipboard", {"class": "group-[.copied]:hidden" } %}
{% lucide "copy", {"class": "group-[.copied]:hidden" } %}
{% lucide "check", {"class": "hidden group-[.copied]:block" } %}
</button>
</div>
+19 -2
View File
@@ -61,6 +61,14 @@
"hx-swap": "outerHTML"
} %}
{% set new_link_attrs = {
"hx-boost": "true",
"hx-select": "#content",
"hx-target": "#content",
"hx-swap": "outerHTML",
"data-new-link": "true"
} %}
{% set icon_home %}{% lucide "house" %}{% endset %}
{% set icon_info %}{% lucide "info" %}{% endset %}
{% set icon_square_terminal %}{% lucide "square-terminal" %}{% endset %}
@@ -86,21 +94,30 @@
{ label: "Badge", url: "/components/badge", attrs: link_attrs },
{ label: "Breadcrumb", url: "/components/breadcrumb", attrs: link_attrs },
{ label: "Button", url: "/components/button", attrs: link_attrs },
{ label: "Button Group", url: "/components/button-group", attrs: new_link_attrs },
{ label: "Card", url: "/components/card", attrs: link_attrs },
{ label: "Checkbox", url: "/components/checkbox", attrs: link_attrs },
{ label: "Command", url: "/components/command", attrs: new_link_attrs },
{ label: "Combobox", url: "/components/combobox", attrs: link_attrs },
{ label: "Dialog", url: "/components/dialog", attrs: link_attrs },
{ label: "Dropdown Menu", url: "/components/dropdown-menu", attrs: link_attrs },
{ label: "Empty", url: "/components/empty", attrs: new_link_attrs },
{ label: "Field", url: "/components/field", attrs: new_link_attrs },
{ label: "Form", url: "/components/form", attrs: link_attrs },
{ label: "Input", url: "/components/input", attrs: link_attrs },
{ label: "Input Group", url: "/components/input-group", attrs: new_link_attrs },
{ label: "Item", url: "/components/item", attrs: new_link_attrs },
{ label: "Kbd", url: "/components/kbd", attrs: new_link_attrs },
{ label: "Label", url: "/components/label", attrs: link_attrs },
{ label: "Pagination", url: "/components/pagination", attrs: link_attrs },
{ label: "Popover", url: "/components/popover", attrs: link_attrs },
{ label: "Progress", url: "/components/progress", attrs: new_link_attrs },
{ label: "Radio Group", url: "/components/radio-group", attrs: link_attrs },
{ label: "Select", url: "/components/select", attrs: link_attrs },
{ label: "Skeleton", url: "/components/skeleton", attrs: link_attrs },
{ label: "Sidebar", url: "/components/sidebar", attrs: link_attrs },
{ label: "Skeleton", url: "/components/skeleton", attrs: link_attrs },
{ label: "Slider", url: "/components/slider", attrs: link_attrs },
{ label: "Spinner", url: "/components/spinner", attrs: new_link_attrs },
{ label: "Switch", url: "/components/switch", attrs: link_attrs },
{ label: "Table", url: "/components/table", attrs: link_attrs },
{ label: "Tabs", url: "/components/tabs", attrs: link_attrs },
@@ -117,6 +134,6 @@
label="Sidebar navigation",
header=header,
footer=footer,
content_attrs={ "class": "scrollbar" },
content_attrs={ "class": "scrollbar [&_[data-new-link]::after]:content-['New'] [&_[data-new-link]::after]:ml-auto [&_[data-new-link]::after]:text-xs [&_[data-new-link]::after]:font-medium [&_[data-new-link]::after]:bg-sidebar-primary [&_[data-new-link]::after]:text-sidebar-primary-foreground [&_[data-new-link]::after]:px-2 [&_[data-new-link]::after]:py-0.5 [&_[data-new-link]::after]:rounded-md" },
menu=menu
) }}
+1580 -471
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -11,7 +11,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 Accordion component in Basecoat.</h2>
</div>
{% set code %}<section class="accordion">
@@ -72,4 +72,4 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<p class="prose">Basecoat animates <code>&lt;details&gt;</code> elements by default. The example above uses Alpine.js to handle the open/close state along with some Tailwind CSS classes.</p>
<p class="prose">Basecoat already animates <code>&lt;details&gt;</code> elements by default. The example add some Tailwind CSS utility classes for style and a bit of vanilla JavaScript to handle the open/close state.</p>
+1 -1
View File
@@ -43,7 +43,7 @@ toc:
<p>The Alert Dialog component is identical to <a href="/components/dialog" class="font-medium underline underline-offset-4">Dialog</a> except for two small differences: we do not display a close button and we do not let the user close the dialog when clicking on the backdrop (no <code>onclick</code> attribute on the <code>&lt;dialog&gt;</code> or the <code>&lt;article&gt;</code> elements).</p>
</section>
{{ code_block(code_html | prettyHtml, class="") }}
{{ code_block(code_html | prettyHtml) }}
<section class="prose">
<p>The component has the following HTML structure:</p>
+47 -14
View File
@@ -19,30 +19,63 @@ toc:
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<div class="alert">
{% lucide "circle-check" %}
<h2>Success! Your changes have been saved</h2>
<section>This is an alert with icon, title and description.</section>
</div>{% endset %}
{{ code_preview("alert", code) }}
{% set code %}
<div class="grid w-full max-w-xl items-start gap-4">
<div class="alert">
{% lucide "circle-check" %}
<h2>Success! Your changes have been saved</h2>
<section>This is an alert with icon, title and description.</section>
</div>
<div class="alert">
{% lucide "popcorn" %}
<h2>This Alert has a title and an icon. No description.</h2>
<section>This is an alert with icon, title and description.</section>
</div>
<div class="alert-destructive">
{% lucide "circle-check" %}
<h2>Unable to process your payment.</h2>
<section>
<p>Please verify your billing information and try again.</p>
<ul class="list-inside list-disc text-sm">
<li>Check your card details</li>
<li>Ensure sufficient funds</li>
<li>Verify billing address</li>
</ul>
</section>
</div>
</div>
{% endset %}
{{ code_preview("alert", code | prettyHtml) }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>You can use one of the two classes: <code>alert</code> or <code>alert-dialog</code>.</p>
<p>Use the <code>alert</code> or <code>alert-destructive</code>.</p>
</section>
{{ code_block(code) }}
{% set code_simple %}
<div class="alert">
{% lucide "circle-check" %}
<h2>Success! Your changes have been saved</h2>
<section>This is an alert with icon, title and description.</section>
</div>
{% endset %}
{{ code_block(code_simple | prettyHtml) }}
<section class="prose">
<p>The component has the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;svg&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The icon.</dd>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dd>The title.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The description.</dd>
<dt><code class="highlight language-html">&lt;div class="alert"&gt;</code></dt>
<dd>Main container. Use <code>alert</code> for default styling or <code>alert-destructive</code> for error states.
<dl>
<dt><code class="highlight language-html">&lt;svg&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The icon.</dd>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dd>The title.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The description.</dd>
</dl>
</dd>
</dl>
</section>
+21 -4
View File
@@ -12,12 +12,29 @@ toc:
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated avatar 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 %}
{{ code_preview("avatar", code, "max-w-md") }}
{% set code %}
<div class="flex flex-row flex-wrap items-center gap-12">
<img class="size-8 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">
<img class="size-8 shrink-0 object-cover rounded-lg" alt="@shadcn" src="https://github.com/shadcn.png">
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-8 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
</div>
{% endset %}
{{ code_preview("avatar", code | prettyHtml, "w-full max-w-md") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<p class="prose">Avatars are fairly simple to implement with Tailwind. The example above shows how to implement one with Tailwind and an <code>&lt;img&gt;</code> element. More examples can be found in the <a href="/kitchen-sink#avatar">kitchen sink</a>.</p>
<section class="prose">
<p>Avatars are just <code>&lt;img&gt;</code> elements styled with Tailwind utility classes.</p>
</section>
{% set code_simple %}
<img class="size-8 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">
{% endset %}
{{ code_block(code_simple | prettyHtml, "html") }}
+19 -1
View File
@@ -25,7 +25,25 @@ toc:
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<span class="badge">Badge</span>{% endset %}
{% set code %}
<div class="flex flex-col items-center gap-2">
<div class="flex w-full flex-wrap gap-2">
<span class="badge">Badge</span>
<span class="badge-secondary">Secondary</span>
<span class="badge-destructive">Destructive</span>
<span class="badge-outline">Outline</span>
</div>
<div class="flex w-full flex-wrap gap-2">
<span class="badge-secondary bg-blue-500 text-white dark:bg-blue-600">
{% lucide "badge-check" %}
Verified
</span>
<span class="badge rounded-full h-5 min-w-5 px-1 font-mono tabular-nums">8</span>
<span class="badge-destructive rounded-full h-5 min-w-5 px-1 font-mono tabular-nums">99</span>
<span class="badge-outline rounded-full h-5 min-w-5 px-1 font-mono tabular-nums">20+</span>
</div>
</div>
{% endset %}
{{ code_preview("badge", code) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
+14 -3
View File
@@ -13,7 +13,7 @@ toc:
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated breadcrumb component in Basecoat.</h2>
<h2>There is no dedicated Breadcrumb component in Basecoat.</h2>
</div>
{% set code_html %}
@@ -54,7 +54,18 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>This example uses some simple Tailwind CSS styles along with the <a href="/components/dropdown-menu">Dropdown Menu</a> component (which may require JavaScript).</p>
<p>You can style an ordered list of links combined with chevron icons as show below. The example above demonstrates a more complex use case using the <a href="/components/dropdown-menu">Dropdown Menu</a> component.</p>
</section>
{{ code_block(code_html | prettyHtml, class="") }}
{% set code_simple %}
<ol class="text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5">
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Home</a>
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Components</a>
</li>
</ol>
{% endset %}
{{ code_block(code_simple | prettyHtml, "html") }}
+290
View File
@@ -0,0 +1,290 @@
---
layout: layouts/page.njk
title: Button Group
description: A container that groups related buttons together with consistent styling.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Orientation
id: example-orientation
- label: Nested
id: example-nested
- label: Separator
id: example-separator
- label: Input
id: example-input
- label: Dropdown Menu
id: example-dropdown-menu
- label: Select
id: example-select
- label: Popover
id: example-popover
---
{% from "dropdown-menu.njk" import dropdown_menu %}
{% from "popover.njk" import popover %}
{% from "select.njk" import select %}
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code_default %}
<div class="flex w-fit items-stretch gap-2">
<button type="button" class="btn-icon-outline" aria-label="Go Back">{% lucide "arrow-left" %}</button>
<div role="group" class="button-group">
<button type="button" class="btn-outline">Archive</button>
<button type="button" class="btn-outline">Report</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn-outline">Snooze</button>
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{% call dropdown_menu(
trigger=trigger,
trigger_attrs={"class": "btn-icon-outline"},
popover_attrs={"data-align": "end"}
) %}
<div role="menuitem">
{% lucide "mail-check" %}
Mark as Read
</div>
<div role="menuitem">
{% lucide "archive" %}
Archive
</div>
<hr role="separator">
<div role="menuitem">
{% lucide "clock" %}
Snooze
</div>
<div role="menuitem">
{% lucide "calendar" %}
Add to Calendar
</div>
<div role="menuitem">
{% lucide "list-filter-plus" %}
Add to List
</div>
<hr role="separator">
<div role="menuitem" class="text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/20 focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive [&_svg]:!text-destructive">
{% lucide "trash-2" %}
Trash
</div>
{% endcall %}
</div>
{% endset %}
{{ code_preview("button-group-default", code_default | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<p class="prose">Simply wrap buttons you want to group together in a <code>&lt;div role="group" class="button-group"&gt;</code> element. You can also use the <code>data-orientation="vertical"</code> attribute to stack buttons vertically. You can also group buttons with <a href="#example-dropdown-menu">dropdown menus</a> or <a href="#example-popover">popovers</a> (provided the trigger is a button). Finally, you can use a <a href="#example-separator">separator</a> to visually divide buttons within a group (for secondary buttons for example).</p>
{% set code_simple %}
<div role="group" class="button-group">
<button type="button" class="btn-outline">Archive</button>
<button type="button" class="btn-outline">Report</button>
</div>
{% endset %}
{{ code_block(code_simple | prettyHtml, "html") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-orientation"><a href="#example-orientation">Orientation</a></h3>
<section class="prose">
<p>Use <code>data-orientation="vertical"</code> to stack buttons vertically.</p>
</section>
{% set code_orientation %}
<div role="group" class="button-group" data-orientation="vertical">
<button type="button" class="btn-icon-outline" aria-label="Zoom in">
{% lucide "plus" %}
</button>
<button type="button" class="btn-icon-outline" aria-label="Zoom out">
{% lucide "minus" %}
</button>
</div>
{% endset %}
{{ code_preview("button-group-orientation", code_orientation | prettyHtml) }}
<h3 id="example-size"><a href="#example-size">Size</a></h3>
{% set code_size %}
<div class="flex flex-col items-start gap-8">
<div role="group" class="button-group">
<button type="button" class="btn-sm-outline">Small</button>
<button type="button" class="btn-sm-outline">Button</button>
<button type="button" class="btn-sm-outline">Group</button>
<button type="button" class="btn-sm-icon-outline">{% lucide "plus" %}</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn-outline">Default</button>
<button type="button" class="btn-outline">Button</button>
<button type="button" class="btn-outline">Group</button>
<button type="button" class="btn-icon-outline">{% lucide "plus" %}</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn-lg-outline">Small</button>
<button type="button" class="btn-lg-outline">Button</button>
<button type="button" class="btn-lg-outline">Group</button>
<button type="button" class="btn-lg-icon-outline">{% lucide "plus" %}</button>
</div>
</div>
{% endset %}
{{ code_preview("button-group-size", code_size | prettyHtml) }}
<h3 id="example-nested"><a href="#example-nested">Nested</a></h3>
{% set code_nested %}
<div role="group" aria-label="Pagination controls" class="flex w-fit items-stretch gap-2">
<div role="group" class="button-group">
<button type="button" class="btn-sm-outline">1</button>
<button type="button" class="btn-sm-outline">2</button>
<button type="button" class="btn-sm-outline">3</button>
<button type="button" class="btn-sm-outline">4</button>
<button type="button" class="btn-sm-outline">5</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn-sm-icon-outline" aria-label="Previous page">
{% lucide "chevron-left" %}
</button>
<button type="button" class="btn-sm-icon-outline" aria-label="Next page">
{% lucide "chevron-right" %}
</button>
</div>
</div>
{% endset %}
{{ code_preview("button-group-nested", code_nested | prettyHtml) }}
<h3 id="example-separator"><a href="#example-separator">Separator</a></h3>
<section class="prose">
<p>Use <code>&lt;hr role="separator"&gt;</code> to visually divide buttons within a group.</p>
</section>
{% set code_separator %}
<div role="group" class="button-group">
<button type="button" class="btn-secondary">Copy</button>
<hr role="separator">
<button type="button" class="btn-secondary">Paste</button>
</div>
{% endset %}
{{ code_preview("button-group-separator", code_separator | prettyHtml) }}
<h3 id="example-input"><a href="#example-input">Input</a></h3>
{% set code_input %}
<div class="button-group">
<input type="text" class="input" placeholder="Search...">
<button type="button" class="btn-icon-outline">{% lucide "search" %}</button>
</div>
{% endset %}
{{ code_preview("button-group-input", code_input | prettyHtml) }}
<h3 id="example-dropdown-menu"><a href="#example-dropdown-menu">Dropdown Menu</a></h3>
{% set code_dropdown_menu %}
<div role="group" class="button-group">
<button type="button" class="btn-outline">Follow</button>
{% set trigger %}
{% lucide "chevron-down" %}
{% endset %}
{% call dropdown_menu(
trigger=trigger,
trigger_attrs={"class": "btn-icon-outline"},
popover_attrs={"data-align": "end"}
) %}
<div role="menuitem">
{% lucide "volume-off" %}
Mute Conversation
</div>
<div role="menuitem">
{% lucide "check" %}
Mark as Read
</div>
<div role="menuitem">
{% lucide "triangle-alert" %}
Report Conversation
</div>
<div role="menuitem">
{% lucide "user-round-x" %}
Block User
</div>
<div role="menuitem">
{% lucide "share" %}
Share Conversation
</div>
<div role="menuitem">
{% lucide "copy" %}
Copy Conversation
</div>
<hr role="separator">
<div role="menuitem" class="text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/20 focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive [&_svg]:!text-destructive">
{% lucide "trash-2" %}
Delete Conversation
</div>
{% endcall %}
</div>
{% endset %}
{{ code_preview("button-group-dropdown-menu", code_dropdown_menu | prettyHtml) }}
<h3 id="example-select"><a href="#example-select">Select</a></h3>
{% set code_select %}
<div class="flex w-fit items-stretch gap-2">
<div class="button-group">
{% call select(trigger_attrs={"class": "[&_[data-name]]:hidden"}) %}
<div role="option" data-value="$">$ <span class="text-muted-foreground" data-name>US Dollar</span></div>
<div role="option" data-value="€">€ <span class="text-muted-foreground" data-name>Euro</span></div>
<div role="option" data-value="£">£ <span class="text-muted-foreground" data-name>British Pound</span></div>
{% endcall %}
<input type="text" class="input" placeholder="10.00">
</div>
<button type="button" class="btn-icon-outline">{% lucide "arrow-right" %}</button>
</div>
{% endset %}
{{ code_preview("button-group-select", code_select | prettyHtml) }}
<h3 id="example-popover"><a href="#example-popover">Popover</a></h3>
{% set code_popover %}
<div role="group" class="button-group">
<button type="button" class="btn-outline">
{% lucide "bot" %}
Copilot
</button>
{% call popover(
trigger=trigger,
trigger_attrs={"class": "btn-icon-outline"},
popover_attrs={"class": "w-72 p-0", "data-align": "end"}
) %}
<header class="px-4 py-3 text-sm font-medium border-b">Agent Tasks</header>
<section class="p-4 text-sm space-y-2">
<textarea class="textarea w-full mb-4" placeholder="Describe your taks in natural language"></textarea>
<p class="font-medium">Start a new task with Copilot</p>
<p class="text-muted-foreground">
Describe your task in natural language. Copilot will work in the
background and open a pull request for your review.
</p>
</section>
{% endcall %}
</div>
{% endset %}
{{ code_preview("button-group-popover", code_popover | prettyHtml) }}
+8 -1
View File
@@ -118,7 +118,14 @@ toc:
</ul>
<dl>
<dt><code class="highlight language-html">&lt;div role="option" data-value="{ VALUE }"&gt;</code></dt>
<dd>Option that can be selected.Should have a unique id if you use the <code>aria-activedescendant</code> attribute on the trigger.</dd>
<dd>Option that can be selected. Should have a unique id if you use the <code>aria-activedescendant</code> attribute on the trigger. Optional attributes:
<ul>
<li><code>data-filter</code>: custom text used for filtering (if different from displayed content, e.g., <code>data-filter="united states"</code>)</li>
<li><code>data-keywords</code>: additional keywords for enhanced filtering (e.g., <code>data-keywords="bread fruit"</code>)</li>
<li><code>data-force</code>: forces the option to always display, regardless of filter (e.g., <code>data-force</code>)</li>
<li><code>aria-disabled="true"</code>: marks option as disabled (excluded from selection and keyboard navigation)</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Separator between groups/options.</dd>
<dt><code class="highlight language-html">&lt;div role="group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
+288
View File
@@ -0,0 +1,288 @@
---
layout: layouts/page.njk
title: Command
description: Fast, composable command menu for quick navigation and actions.
toc:
- label: Usage
id: usage
children:
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1: Include the JavaScript files"
id: usage-html-js-1
- label: "Step 2: Add your command HTML"
id: usage-html-js-2
- label: HTML structure
id: usage-html-js-3
- label: JavaScript events
id: usage-html-js-4
- label: Jinja and Nunjucks
id: usage-macro
- label: Examples
id: examples
children:
- label: Dialog
id: example-dialog
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "command.njk" import command, command_dialog %}
{% macro command_items(suggestions_id="suggestions", settings_id="settings") %}
<div role="group" aria-labelledby="{{ suggestions_id }}">
<span role="heading" id="{{ suggestions_id }}">Suggestions</span>
<div role="menuitem" onclick="console.log('Open calendar')">
{% lucide "calendar" %}
<span>Calendar</span>
</div>
<div role="menuitem" onclick="console.log('Search emoji')">
{% lucide "smile" %}
<span>Search Emoji</span>
</div>
<div role="menuitem" aria-disabled="true">
{% lucide "calculator" %}
<span>Calculator</span>
</div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="{{ settings_id }}">
<span role="heading" id="{{ settings_id }}">Settings</span>
<div role="menuitem" onclick="console.log('Open profile')" data-filter="Profile">
{% lucide "user" %}
<span>Profile</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘P</kbd>
</div>
<div role="menuitem" onclick="console.log('Open billing')" data-filter="Billing">
{% lucide "credit-card" %}
<span>Billing</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘B</kbd>
</div>
<div role="menuitem" onclick="console.log('Open settings')" data-filter="Settings">
{% lucide "settings" %}
<span>Settings</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘S</kbd>
</div>
</div>
{% endmacro %}
{% set code_standalone %}
{% call command(
id="demo-command-standalone",
main_attrs={ "class": "rounded-lg border shadow-md" }
) %}
{{ command_items("suggestions", "settings") }}
{% endcall %}
{% endset %}
{{ code_preview("command-standalone", code_standalone | prettyHtml, class=" w-full max-w-[450px]") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript files</a></h4>
<section class="prose">
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/command.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/command.js" target="_blank">
command.js
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your command HTML</a></h4>
<section class="prose">
<p>You can use the command menu standalone:</p>
</section>
{{ code_block(code_standalone | prettyHtml, "html") }}
<section class="prose">
<p>Wrap the command menu in a <code>&lt;dialog class="command-dialog"&gt;</code> to display in a dialog:</p>
</section>
{% set code_dialog %}
<dialog class="command-dialog" aria-label="Command menu">
<div class="command">
<!-- Regular command content: header, menu, etc. -->
</div>
</dialog>
{% endset %}
{{ code_block(code_dialog | prettyHtml, "html") }}
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;dialog class="command-dialog"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>For dialog variant, wraps a <code>&lt;div class="command"&gt;</code> to display as a modal. Uses native <code>&lt;dialog&gt;</code> behavior and requires <code>aria-label</code> or <code>aria-labelledby</code> for accessibility.
<dl>
<dt><code class="highlight language-html">&lt;div class="command"&gt;</code></dt>
<dd>Wraps around the entire command component. Can be used standalone or inside a dialog.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Contains the search input to filter command options.
<dl>
<dt><code class="highlight language-html">&lt;input type="text"&gt;</code></dt>
<dd>The search input. Should have:
<ul>
<li><code>role="combobox"</code>: indicates the input controls a menu with filtering</li>
<li><code>aria-expanded="true"</code>: the menu is always expanded</li>
<li><code>aria-controls="{ MENU_ID }"</code>: points to the menu's id</li>
</ul>
</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;div role="menu"&gt;</code></dt>
<dd>The menu containing the command items. Should have <code>id="{ MENU_ID }"</code> refered to by the input's <code>aria-controls</code> attribute.
<dl>
<dt><code class="highlight language-html">&lt;div role="menuitem"&gt;</code> or <code class="highlight language-html">&lt;a role="menuitem"&gt;</code></dt>
<dd>Command item that triggers an action when clicked. Use <code>&lt;a&gt;</code> for navigation links, or <code>&lt;div&gt;</code> for actions. In a command-dialog, clicking a menuitem automatically closes the dialog unless the <code>data-keep-command-open</code> attribute is present. Optional attributes:
<ul>
<li><code>href</code>: If present on an <code>&lt;a&gt;</code>, the item navigates to that URL</li>
<li><code>data-filter</code>: custom text used for filtering (if different from displayed content, e.g., <code>data-filter="calendar event"</code>)</li>
<li><code>data-keywords</code>: additional keywords for enhanced filtering (e.g., <code>data-keywords="calendar event date"</code>)</li>
<li><code>data-force</code>: forces the item to always display, regardless of filter (e.g., <code>data-force</code>)</li>
<li><code>aria-disabled="true"</code>: marks item as disabled (excluded from keyboard navigation and filtering)</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Separator between groups/items. Hidden when filtering is active.</dd>
<dt><code class="highlight language-html">&lt;div role="group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Group of items, can have <code>aria-labelledby</code> attribute to link to a heading.</dd>
<dt><code class="highlight language-html">&lt;span role="heading"&gt;</code></dt>
<dd>Group heading, must have an <code>id</code> attribute if you use the <code>aria-labelledby</code> attribute on the group.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</section>
<h4 id="usage-html-js-4"><a href="#usage-html-js-4">JavaScript events</a></h4>
<section class="prose">
<dl>
<dt><code>basecoat:initialized</code></dt>
<dd>Once the component is fully initialized, it dispatches a custom (non-bubbling) <code>basecoat:initialized</code> event on itself.</dd>
</dl>
</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">command()</code> or <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">command_dialog()</code> Nunjucks or Jinja macros for this component.</p>
</div>
<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" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/command.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/command.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% call command() %}
<div role="menuitem">
<svg>...</svg>
<span>Calendar</span>
</div>
<div role="menuitem" data-keywords="config preferences">
<svg>...</svg>
<span>Settings</span>
</div>
{% endcall %}{% endraw %}{% endset %}
{{ code_block(raw_code, "jinja") }}
<section class="prose">
<p>You can also use the <code>items</code> parameter to pass an array of items:</p>
</section>
{% set icon_calendar %}{% lucide "calendar" %}{% endset %}
{% set icon_settings %}{% lucide "settings" %}{% endset %}
{% set raw_code_items %}{% raw %}{% set icon_calendar %}{% lucide "calendar" %}{% endset %}
{% set icon_settings %}{% lucide "settings" %}{% endset %}
{% set items = [
{ type: "group", label: "Suggestions", items: [
{ label: "Calendar", icon: icon_calendar, keywords: "date event" },
{ label: "Search Emoji", icon: icon_settings },
{ label: "Calculator", disabled: true }
]},
{ type: "separator" },
{ type: "group", label: "Settings", items: [
{ label: "Profile", url: "/profile", icon: icon_settings, keywords: "user account" },
{ label: "Billing", url: "/billing", icon: icon_settings },
{ label: "Settings", url: "/settings", icon: icon_settings }
]}
] %}
{% call command(items=items) %}{% endcall %}{% endraw %}{% endset %}
{{ code_block(raw_code_items, "jinja") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-dialog"><a href="#example-dialog">Dialog</a></h3>
<section class="prose">
<p>You can use some simple JavaScript to open the command menu with a keyboard shortcut (in this case <kbd class="kbd">⌘J</kbd>).</p>
</section>
{% set code_dialog %}
<button
type="button"
class="btn-outline"
onclick="document.getElementById('demo-command-dialog').showModal()"
>
Open command menu
<kbd class="kbd">⌘J</kbd>
</button>
{% call command_dialog(id="demo-command-dialog") %}
{{ command_items("cmd-suggestions", "cmd-settings") }}
{% endcall %}
<script>
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'j') {
e.preventDefault();
const dialog = document.getElementById('demo-command-dialog');
if (dialog.open) {
dialog.close();
} else {
dialog.showModal();
dialog.querySelector('header input')?.focus();
}
}
});
</script>
{% endset %}
{{ code_preview("command-dialog", code_dialog | prettyHtml) }}
+2 -2
View File
@@ -69,8 +69,8 @@ toc:
<li><code>onclick</code>: closes the dialog when the backdrop is clicked.</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;article&gt;</code></dt>
<dd>Wraps the content of the dialog. It includes an <code>onclick</code> attribute to prevent from closing the dialog if we have an <code>onclick</code> event on <code>&lt;dialog&gt;</code>.
<dt><code class="highlight language-html">&lt;div&gt;</code></dt>
<dd>Wraps the content of the dialog.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Contains the header for the dialog:
+157
View File
@@ -0,0 +1,157 @@
---
layout: layouts/page.njk
title: Empty
description: Display empty states with icons, titles, descriptions, and actions.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Outline
id: example-outline
- label: Avatar
id: example-avatar
- label: Avatar Group
id: example-avatar-group
- label: Input Group
id: example-input-group
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated Empty component in Basecoat.</h2>
</div>
{% set code_default %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300">
<header class="flex max-w-sm flex-col items-center gap-2 text-center">
<div class="mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6">
{% lucide "folder-code" %}
</div>
<h3 class="text-lg font-medium tracking-tight">No Projects Yet</h3>
<p class="text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4">
You haven't created any projects yet. Get started by creating your first project.
</p>
</header>
<section class="flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance">
<div class="flex gap-2">
<button class="btn">Create Project</button>
<button class="btn-outline">Import Project</button>
</div>
</section>
<a href="#" class="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive underline-offset-4 hover:underline h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5 text-muted-foreground">
Learn More
{% lucide "arrow-up-right" %}
</a>
</div>
{% endset %}
{{ code_preview("empty-default", code_default) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Empty states are pure HTML composition using Tailwind utility classes. Use flex layout utilities to center content, and compose with semantic elements like <code>&lt;header&gt;</code>, <code>&lt;h3&gt;</code>, <code>&lt;p&gt;</code>, and <code>&lt;button&gt;</code>.</p>
<p>Browse the examples below and copy the pattern that fits your needs.</p>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-outline"><a href="#example-outline">Outline</a></h3>
{% set code_outline %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300 border">
<header class="flex max-w-sm flex-col items-center gap-3 text-center">
<div class="mb-2 bg-muted text-foreground flex size-12 shrink-0 items-center justify-center rounded-lg [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6">
{% lucide "cloud" %}
</div>
<h3 class="text-lg font-semibold tracking-tight">Cloud Storage Empty</h3>
<p class="text-muted-foreground text-sm/relaxed">
Upload files to your cloud storage to access them anywhere.
</p>
</header>
<section class="flex w-full max-w-sm min-w-0 flex-col items-center gap-3">
<button class="btn-sm-outline">Upload Files</button>
</section>
</div>
{% endset %}
{{ code_preview("empty-outline", code_outline) }}
<h3 id="example-avatar"><a href="#example-avatar">Avatar</a></h3>
{% set code_avatar %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300">
<header class="flex max-w-sm flex-col items-center gap-3 text-center">
<div class="mb-2 flex items-center justify-center">
<img alt="@shadcn" src="https://github.com/shadcn.png" class="grayscale size-12 shrink-0 object-cover rounded-full">
</div>
<h3 class="text-lg font-semibold tracking-tight">User Offline</h3>
<p class="text-muted-foreground text-sm/relaxed">
This user is currently offline. You can leave a message to notify them or try again later.
</p>
</header>
<section class="flex w-full max-w-sm min-w-0 flex-col items-center gap-3">
<button class="btn-sm">Leave Message</button>
</section>
</div>
{% endset %}
{{ code_preview("empty-avatar", code_avatar) }}
<h3 id="example-avatar-group"><a href="#example-avatar-group">Avatar Group</a></h3>
{% set code_avatar_group %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300">
<header class="flex max-w-sm flex-col items-center gap-3 text-center">
<div class="mb-2 flex items-center justify-center">
<div class="flex -space-x-2 [&amp;_img]:ring-background [&amp;_img]:ring-2 [&amp;_img]:grayscale [&amp;_img]:size-12 [&amp;_img]:shrink-0 [&amp;_img]:object-cover [&amp;_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
</div>
<h3 class="text-lg font-semibold tracking-tight">No Team Members</h3>
<p class="text-muted-foreground text-sm/relaxed">
Invite your team to collaborate on this project.
</p>
</header>
<section class="flex w-full max-w-sm min-w-0 flex-col items-center gap-3">
<button class="btn-sm inline-flex items-center gap-2">
{% lucide "plus" %}
Invite Members
</button>
</section>
</div>
{% endset %}
{{ code_preview("empty-avatar-group", code_avatar_group) }}
<h3 id="example-input-group"><a href="#example-input-group">Input Group</a></h3>
{% set code_input_group %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300">
<header class="flex max-w-sm flex-col items-center gap-3 text-center">
<h3 class="text-lg font-semibold tracking-tight">404 - Not Found</h3>
<p class="text-muted-foreground text-sm/relaxed">
The page you're looking for doesn't exist. Try searching for what you need below.
</p>
</header>
<section class="flex w-full max-w-md min-w-0 flex-col items-center gap-3">
<div class="relative w-3/4">
<input type="text" class="input pl-8.5 pr-10" placeholder="Try searching for pages...">
{% lucide "search" , { "class": "absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground pointer-events-none shrink-0 size-4" } %}
<kbd class="kbd absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">/</kbd>
</div>
<p class="text-muted-foreground text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary">
Need help? <a href="#">Contact support</a>
</p>
</section>
</div>
{% endset %}
{{ code_preview("empty-input-group", code_input_group) }}
+459
View File
@@ -0,0 +1,459 @@
---
layout: layouts/page.njk
title: Field
description: Combine labels, controls, and help text to compose accessible form fields.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Input
id: example-input
- label: Textarea
id: example-textarea
- label: Select
id: example-select
- label: Fieldset
id: example-fieldset
- label: Checkbox
id: example-checkbox
- label: Radio Group
id: example-radio
- label: Switch
id: example-switch
- label: Responsive Layout
id: example-responsive
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code_demo %}
<form class="w-full max-w-md space-y-6">
<fieldset class="fieldset">
<legend>Payment Method</legend>
<p>All transactions are secure and encrypted</p>
<div role="group" class="field">
<label for="card-name">Name on Card</label>
<input id="card-name" type="text" placeholder="Evil Rabbit" required>
</div>
<div role="group" class="field">
<label for="card-number">Card Number</label>
<input id="card-number" type="text" placeholder="1234 5678 9012 3456" aria-describedby="card-number-desc" required>
<p id="card-number-desc">Enter your 16-digit card number</p>
</div>
<div class="grid grid-cols-3 gap-4">
<div role="group" class="field">
<label for="exp-month">Month</label>
<select id="exp-month" class="select w-full">
<option value="">MM</option>
<option value="01">01</option>
<option value="02">02</option>
<option value="03">03</option>
<option value="04">04</option>
<option value="05">05</option>
<option value="06">06</option>
<option value="07">07</option>
<option value="08">08</option>
<option value="09">09</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
</select>
</div>
<div role="group" class="field">
<label for="exp-year">Year</label>
<select id="exp-year" class="select w-full">
<option value="">YYYY</option>
<option value="2024">2024</option>
<option value="2025">2025</option>
<option value="2026">2026</option>
<option value="2027">2027</option>
<option value="2028">2028</option>
<option value="2029">2029</option>
</select>
</div>
<div role="group" class="field">
<label for="cvv">CVV</label>
<input id="cvv" type="text" placeholder="123" required>
</div>
</div>
</fieldset>
<hr role="separator" class="my-6 border-border">
<fieldset class="fieldset">
<legend>Billing Address</legend>
<p>The billing address associated with your payment method</p>
<div role="group" class="field">
<label for="same-as-shipping" class="gap-3">
<input type="checkbox" id="same-as-shipping" checked>
Same as shipping address
</label>
</div>
</fieldset>
<fieldset class="fieldset">
<div role="group" class="field">
<label for="comments">Comments</label>
<textarea id="comments" placeholder="Add any additional comments" rows="3"></textarea>
</div>
</fieldset>
<div class="flex gap-3">
<button type="submit" class="btn">Submit</button>
<button type="button" class="btn-outline">Cancel</button>
</div>
</form>
{% endset %}
{{ code_preview("field-demo", code_demo) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Field uses semantic HTML with minimal classes. Two main patterns can be used independently or composed together:</p>
<ul>
<li><code>&lt;div role="group" class="field"&gt;</code> for individual field containers (label, input, helper text, errors)</li>
<li><code>&lt;fieldset class="fieldset"&gt;</code> for grouping multiple related fields together</li>
</ul>
<p>Elements inside these containers are styled automatically based on their purpose. Use <code>aria-describedby</code> to connect helper text and errors to inputs for accessibility.</p>
<p><strong>Fields</strong> have the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;div role="group" class="field"&gt;</code></dt>
<dd>Field container. Add <code>data-orientation="horizontal"</code> to align controls horizontally. For responsive layouts, use standard Tailwind responsive classes.
<dl>
<dt><code class="highlight language-html">&lt;label&gt;</code></dt>
<dd>Label for the input.</dd>
<dt><code class="highlight language-html">&lt;input&gt;</code>, <code>&lt;select&gt;</code> or <code>&lt;textarea&gt;</code></dt>
<dd>Form input.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Helper text. Use <code>aria-describedby</code> on the input to reference this element's <code>id</code>.</dd>
<dt><code class="highlight language-html">&lt;p role="alert"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Error message. Use <code>aria-describedby</code> on the input and <code>aria-invalid="true"</code> to reference this element's <code>id</code>.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Wraps label and description when the label sits beside the input (e.g. <code>data-orientation="horizontal"</code>). Not required if you have no description.</dd>
</dl>
</dd>
</dl>
<p><strong>Fieldsets</strong> have the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;fieldset class="fieldset"&gt;</code></dt>
<dd>Container for grouping related fields.
<dl>
<dt><code class="highlight language-html">&lt;legend&gt;</code></dt>
<dd>Heading for the fieldset.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Description text. When placed directly after <code>&lt;legend&gt;</code>, automatically styled as muted text.</dd>
<dt><code class="highlight language-html">&lt;div role="group" class="field"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Individual field containers (see structure above). You can include multiple <code>.field</code> elements, or place form controls directly in the fieldset.</dd>
</dl>
</dd>
</dl>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-input"><a href="#example-input">Input</a></h3>
{% set code_input %}
<div class="grid gap-6">
<div role="group" class="field">
<label for="username">Username</label>
<input id="username" type="text" placeholder="evilrabbit" aria-describedby="username-desc">
<p id="username-desc">Choose a unique username for your account.</p>
</div>
<div role="group" class="field">
<label for="password">Password</label>
<p id="password-desc">Must be at least 8 characters long.</p>
<input id="password" type="password" placeholder="••••••••" aria-describedby="password-desc">
</div>
</div>
{% endset %}
{{ code_preview("field-input", code_input, class="w-full max-w-md") }}
<h3 id="example-textarea"><a href="#example-textarea">Textarea</a></h3>
{% set code_textarea %}
<div role="group" class="field">
<label for="bio">Feedback</label>
<textarea id="bio" rows="4" placeholder="Your feedback helps us improve..." aria-describedby="bio-desc"></textarea>
<p id="bio-desc">Share your thoughts about our service.</p>
</div>
{% endset %}
{{ code_preview("field-textarea", code_textarea, class="w-full max-w-md") }}
<h3 id="example-select"><a href="#example-select">Select</a></h3>
{% set code_select %}
<div role="group" class="field">
<label for="country">Department</label>
<select id="country" class="select w-full" aria-describedby="country-desc">
<option value="">Choose department</option>
<option value="engineering">Engineering</option>
<option value="design">Design</option>
<option value="marketing">Marketing</option>
<option value="sales">Sales</option>
<option value="support">Customer Support</option>
<option value="hr">Human Resources</option>
<option value="finance">Finance</option>
<option value="operations">Operations</option>
</select>
<p id="country-desc">Select your department or area of work.</p>
</div>
{% endset %}
{{ code_preview("field-select", code_select, class="w-full max-w-md") }}
<h3 id="example-slider"><a href="#example-slider">Slider</a></h3>
{% set code %}
<div role="group" class="field">
<label for="price-range">Price</label>
<p id="price-range-desc">Set your budget: $<span id="price-range-value">150</span></p>
<input id="price-range" type="range" min="0" max="500" value="150">
</div>
<script>
(() => {
const sliders = document.querySelectorAll('input[type="range"]');
if (!sliders) return;
const updateSlider = (el) => {
const min = parseFloat(el.min || 0);
const max = parseFloat(el.max || 100);
const value = parseFloat(el.value);
const percent = (max === min) ? 0 : ((value - min) / (max - min)) * 100;
el.style.setProperty('--slider-value', `${percent}%`);
};
sliders.forEach(slider => {
updateSlider(slider);
slider.addEventListener('input', (event) => updateSlider(event.target));
});
const priceSlider = document.getElementById('price-range');
const priceValue = document.getElementById('price-range-value');
priceSlider.addEventListener('input', (event) => priceValue.innerHTML = event.target.value);
})();
</script>
{% endset %}
{{ code_preview("slider", code, "w-full max-w-sm") }}
<h3 id="example-fieldset"><a href="#example-fieldset">Fieldset</a></h3>
<section class="prose">
<p>Group related fields with <code>&lt;fieldset class="fieldset"&gt;</code> and <code>&lt;legend&gt;</code>.</p>
</section>
{% set code_fieldset %}
<form class="w-full max-w-md">
<fieldset class="fieldset">
<legend>Profile</legend>
<p>This information will be displayed on your profile</p>
<div role="group" class="field">
<label for="full-name">Full name</label>
<input id="full-name" type="text" placeholder="Evil Rabbit" aria-describedby="name-desc">
<p id="name-desc">Your first and last name</p>
</div>
<div role="group" class="field">
<label for="display-name">Username</label>
<input id="display-name" type="text" placeholder="@evilrabbit" aria-invalid="true" aria-describedby="username-error">
<p id="username-error" role="alert">Username is already taken</p>
</div>
<div class="flex items-center gap-3">
<input type="checkbox" id="newsletter">
<label for="newsletter" class="font-normal">Subscribe to the newsletter</label>
</div>
</fieldset>
<button type="submit" class="btn mt-4">Save changes</button>
</form>
{% endset %}
{{ code_preview("field-fieldset", code_fieldset) }}
<h3 id="example-checkbox"><a href="#example-checkbox">Checkbox</a></h3>
{% set code_checkbox %}
<fieldset class="fieldset">
<legend>Show these items on the desktop</legend>
<p>Select the items you want to show on the desktop.</p>
<div class="flex flex-col gap-3">
<div class="field">
<label class="gap-3">
<input type="checkbox">
Hard disks
</label>
</div>
<div class="field">
<label class="gap-3">
<input type="checkbox">
External disks
</label>
</div>
<div class="field">
<label class="gap-3">
<input type="checkbox">
CDs, DVDs and iPods
</label>
</div>
<div class="field">
<label class="gap-3">
<input type="checkbox">
Connected servers
</label>
</div>
</div>
<hr role="separator">
<div class="field" data-orientation="horizontal">
{# <div class="flex w-full items-center gap-3"> #}
<input id="sync-desktop-documents" type="checkbox" checked class="input self-start">
<section>
<label for="sync-desktop-documents">Sync Desktop & Documents folders</label>
<p>Your Desktop & Documents folders are being synced with iCloud Drive. You can access them from other devices.</p>
</section>
{# </div> #}
</div>
</fieldset>
{% endset %}
{{ code_preview("field-checkbox", code_checkbox, class="w-full max-w-md") }}
<h3 id="example-radio"><a href="#example-radio">Radio Group</a></h3>
{% set code_radio %}
<fieldset class="fieldset">
<legend>Subscription plan</legend>
<p>Yearly and lifetime plans offer significant savings.</p>
<div role="radiogroup" class="grid gap-3">
<div class="field">
<label class="gap-3">
<input type="radio" name="subscription-plan" checked>
Monthly ($9.99/month)
</label>
</div>
<div class="field">
<label class="gap-3">
<input type="radio" name="subscription-plan">
Yearly ($99.99/year)
</label>
</div>
<div class="field">
<label class="gap-3">
<input type="radio" name="subscription-plan">
Lifetime ($299.99)
</label>
</div>
</div>
</fieldset>
{% endset %}
{{ code_preview("field-radio", code_radio, class="w-full max-w-md") }}
<h3 id="example-switch"><a href="#example-switch">Switch</a></h3>
<section class="prose">
<p>Set <code>data-orientation="horizontal"</code> on <code>.field</code> to align labels and controls side-by-side.</p>
</section>
{% set code_switch %}
<div role="group" class="field" data-orientation="horizontal">
<section>
<label for="multi-factor-authentication">Multi-factor authentication</label>
<p>Enable multi-factor authentication. If you do not have a two-factor device, you can use a one-time code sent to your email.</p>
</section>
<input id="multi-factor-authentication" type="checkbox" role="switch">
</div>
{% endset %}
{{ code_preview("field-switch", code_switch, class="w-full max-w-md") }}
<h3 id="example-choice-card"><a href="#example-choice-card">Choice Card</a></h3>
{% set code_choice_card %}
<fieldset class="fieldset">
<legend class="text-sm">Compute Environment</legend>
<p>Select the compute environment for your cluster.</p>
<div role="group" class="field">
<label class="flex gap-3 border rounded-md p-4 has-[:checked]:border-primary has-[:checked]:bg-primary/5 dark:has-[:checked]:bg-primary/10">
<section>
<h3>Kubernetes</h3>
<p>Run GPU workloads on a K8s configured cluster.</p>
</section>
<input id="choice-card" type="radio" name="choice-card" class="self-start" checked>
</label>
</div>
<div role="group" class="field">
<label class="flex gap-3 border rounded-md p-4 has-[:checked]:border-primary has-[:checked]:bg-primary/5 dark:has-[:checked]:bg-primary/10">
<section>
<h3>Virtual Machine</h3>
<p>Access a VM configured cluster to run GPU workloads.</p>
</section>
<input id="choice-card" type="radio" name="choice-card" class="self-start">
</label>
</div>
</fieldset>
{% endset %}
{{ code_preview("field-choice-card", code_choice_card, class="w-full max-w-md") }}
<h3 id="example-responsive"><a href="#example-responsive">Responsive Layout</a></h3>
<section class="prose">
<p>For responsive layouts, use standard Tailwind responsive classes directly on <code>.field</code>. This example stacks vertically on mobile and switches to horizontal at the <code>md</code> breakpoint.</p>
</section>
{% set code_responsive %}
<fieldset class="fieldset">
<legend>Profile</legend>
<p>Keep your profile details up to date.</p>
<hr role="separator">
<div class="flex flex-col gap-7">
<div role="group" class="field flex-col md:flex-row md:items-center">
<section class="md:flex-auto">
<label for="profile-name">Name</label>
<p id="profile-name-desc">Provide your full name for identification.</p>
</section>
<input id="profile-name" type="text" placeholder="Evil Rabbit" aria-describedby="profile-name-desc" class="md:w-auto md:min-w-80">
</div>
<hr role="separator">
<div role="group" class="field flex-col md:flex-row md:items-start">
<section class="md:flex-auto">
<label for="profile-bio">Message</label>
<p id="profile-bio-desc">You can write your message here. Keep it short, preferably under 100 characters.</p>
</section>
<textarea id="profile-bio" rows="3" placeholder="Hello, world!" aria-describedby="profile-bio-desc" class="md:w-auto md:min-w-80"></textarea>
</div>
<hr role="separator">
<div role="group" class="field flex-col md:flex-row md:items-center">
<button type="submit" class="btn md:w-auto">Submit</button>
<button type="button" class="btn-outline md:w-auto">Cancel</button>
</div>
</div>
</fieldset>
{% endset %}
{{ code_preview("field-responsive", code_responsive, class="w-full max-w-3xl") }}
+372
View File
@@ -0,0 +1,372 @@
---
layout: layouts/page.njk
title: Input Group
description: Display additional information or actions to an input or textarea.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Icon
id: example-icon
- label: Text
id: example-text
- label: Button
id: example-button
- label: Tooltip
id: example-tooltip
- label: Textarea
id: example-textarea
- label: Spinner
id: example-spinner
- label: Dropdown
id: example-dropdown
- label: Group
id: example-group
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "dropdown-menu.njk" import dropdown_menu %}
{% from "popover.njk" import popover %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated Input Group component in Basecoat.</h2>
</div>
{% set code_default %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pl-9 pr-20" placeholder="Search...">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "search" %}
</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">12 results</div>
</div>
<div class="relative">
<input type="text" class="input pl-15 pr-9" placeholder="example.com">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">https://</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4" data-tooltip="This is content in a tooltip.">{% lucide "info" %}</div>
</div>
<div class="relative">
<textarea class="textarea pr-10 min-h-27 pb-12" placeholder="Ask, Search or Chat..."></textarea>
<footer role="group" class="absolute bottom-0 px-3 pb-3 pt-1.5 flex items-center w-full gap-2">
<button type="button" class="btn-icon-outline rounded-full size-6 text-muted-foreground hover:text-accent-foreground">
{% lucide "plus" %}
</button>
{{ dropdown_menu(
id="demo-dropdown-menu",
trigger="Auto",
trigger_attrs={"class": "btn-sm-ghost text-muted-foreground hover:text-accent-foreground h-6 p-2"},
popover_attrs={"data-side": "top", "class": "min-w-32"},
items=[
{ type: "item", label: "Auto" },
{ type: "item", label: "Agent" },
{ type: "item", label: "Manual" }
]
) }}
<div class="text-muted-foreground text-sm ml-auto">52% used</div>
<hr class="w-0 h-4 border-r border-border shrink-0 m-0">
<button type="button" class="btn-icon rounded-full size-6 bg-muted-foreground hover:bg-foreground" disabled>
{% lucide "arrow-up" %}
</button>
</footer>
</div>
<div class="relative">
<input type="text" class="input pr-9" placeholder="@shadcn">
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none bg-primary text-primary-foreground flex size-4 items-center justify-center rounded-full [&>svg]:size-3">{% lucide "check" %}</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-default", code_default, class="w-full max-w-sm") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Input groups are pure HTML composition using Tailwind utility classes. There is no dedicated component: use <code>relative</code> positioning on a wrapper, add your content with <code>absolute</code> positioning, and adjust input padding to make room.</p>
<p>Browse the examples below and copy the pattern that fits your needs.</p>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-icon"><a href="#example-icon">Icon</a></h3>
{% set code_icon %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pl-9" placeholder="Search...">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "search" %}
</div>
</div>
<div class="relative">
<input type="email" class="input pl-9" placeholder="Enter your email">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "mail" %}
</div>
</div>
<div class="relative">
<input type="text" class="input px-9" placeholder="Card number">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "credit-card" %}
</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "check" %}
</div>
</div>
<div class="relative">
<input type="text" class="input pr-16" placeholder="Card number">
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4 flex items-center gap-2">
{% lucide "star" %}
{% lucide "info" %}
</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-icon-prefix", code_icon, class="w-full max-w-sm") }}
<h3 id="example-text"><a href="#example-text">Text</a></h3>
{% set code_text %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pl-7 pr-12" placeholder="0.00">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">$</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">USD</div>
</div>
<div class="relative">
<input type="text" class="input pl-15 pr-12" placeholder="example.com">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">https://</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">.com</div>
</div>
<div class="relative">
<input type="text" class="input pr-30" placeholder="Enter your username">
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">@company.com</div>
</div>
<div class="relative">
<textarea class="textarea pr-10 min-h-25 pb-12" placeholder="Enter your message"></textarea>
<footer role="group" class="absolute bottom-0 px-3 pb-3 pt-1.5 flex items-center w-full gap-2">
<div class="text-muted-foreground text-sm">120 characters left</div>
</footer>
</div>
</div>
{% endset %}
{{ code_preview("input-group-text", code_text, class="w-full max-w-sm") }}
<h3 id="example-button"><a href="#example-button">Button</a></h3>
{% set code_button %}
<div class="grid gap-6">
<div class="relative">
<input type="text" readonly class="input pr-9" value="https://x.com/hunvreus">
<button
data-copied="false"
onclick="
this.dataset.copied = 'true';
setTimeout(() => this.dataset.copied = 'false', 2000);
"
class="group absolute right-1.5 top-1/2 -translate-y-1/2 btn-icon-ghost text-muted-foreground hover:text-accent-foreground size-6"
>
{% lucide "copy", { "class": "group-data-[copied=true]:hidden" } %}
{% lucide "check", { "class": "hidden group-data-[copied=true]:block" } %}
</button>
</div>
<div class="relative">
<input type="text" class="input pl-21 pr-9 rounded-full">
<button
aria-pressed="false"
onclick="this.ariaPressed = this.ariaPressed === 'true' ? 'false' : 'true'"
class="absolute right-1.5 top-1/2 -translate-y-1/2 btn-icon-ghost text-muted-foreground hover:text-accent-foreground size-6 rounded-full aria-pressed:bg-transparent aria-pressed:hover:bg-accent dark:aria-pressed:hover:bg-accent/50 aria-pressed:text-blue-600 aria-pressed:[&>svg]:fill-blue-600"
>{% lucide "star" %}</button>
<div class="absolute left-1.5 top-1/2 -translate-y-1/2 flex items-center gap-1 z-10">
{% set trigger %}
{% lucide "info" %}
{% endset %}
{% call popover(
trigger=trigger,
trigger_attrs={"class": "btn-sm-icon-ghost rounded-full size-6"},
popover_attrs={"class": "max-w-72"}
) %}
<h3 class="font-medium mb-1">Your connection is not secure.</h3>
<p>You should not enter any sensitive information on this site.</p>
{% endcall %}
<div class="text-muted-foreground text-sm pointer-events-none">https://</div>
</div>
</div>
<div class="relative">
<input type="text" readonly class="input pr-9" placeholder="Type to search...">
<button type="button" class="absolute right-1.5 top-1/2 -translate-y-1/2 btn-secondary h-6 p-2 rounded">Search</button>
</div>
</div>
{% endset %}
{{ code_preview("input-group-button", code_button, class="w-full max-w-sm") }}
<h3 id="example-tooltip"><a href="#example-tooltip">Tooltip</a></h3>
{% set code_tooltip %}
<div class="grid gap-6">
<div class="relative">
<input type="password" class="input pr-9" placeholder="Enter password">
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4" data-tooltip="Password must be at least 8 characters long.">{% lucide "info" %}</div>
</div>
<div class="relative">
<input type="email" class="input pr-9" placeholder="Your email address">
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4" data-tooltip="We'll use this to send your notifications.">{% lucide "circle-help" %}</div>
</div>
<div class="relative">
<input type="email" class="input pl-9" placeholder="Your email address">
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4" data-tooltip="Click for help with API keys." data-side="left">{% lucide "circle-help" %}</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-tooltip", code_tooltip, class="w-full max-w-sm") }}
<h3 id="example-textarea"><a href="#example-textarea">Textarea</a></h3>
{% set code_textarea %}
<div class="relative">
<textarea class="textarea pt-15 pb-17 min-h-77" placeholder="console.log('Hello, world!')."></textarea>
<header class="absolute top-0 flex items-center w-full gap-2 p-3 border-b">
<svg 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="tabler-icon tabler-icon-brand-javascript size-4 text-muted-foreground"><path d="M20 4l-2 14.5l-6 2l-6 -2l-2 -14.5z"></path><path d="M7.5 8h3v8l-2 -1"></path><path d="M16.5 8h-2.5a.5 .5 0 0 0 -.5 .5v3a.5 .5 0 0 0 .5 .5h1.423a.5 .5 0 0 1 .495 .57l-.418 2.93l-2 .5"></path></svg>
<span class="font-mono text-sm text-muted-foreground mr-auto">script.js</span>
<button class="btn-sm-icon-ghost text-muted-foreground hover:text-accent-foreground size-6">
{% lucide "refresh-ccw" %}
</button>
<button class="btn-sm-icon-ghost text-muted-foreground hover:text-accent-foreground size-6">
{% lucide "copy" %}
</button>
</header>
<footer class="absolute bottom-0 flex items-center w-full gap-2 p-3 border-t">
<span class="text-sm text-muted-foreground mr-auto">Line 1, Column 1</span>
<button type="button" class="btn-sm">
Run
{% lucide "corner-down-left" %}
</button>
</footer>
</div>
{% endset %}
{{ code_preview("input-group-textarea", code_textarea, class="w-full max-w-md") }}
<h3 id="example-spinner"><a href="#example-spinner">Spinner</a></h3>
{% set code_spinner %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pr-9" placeholder="Searching..." disabled>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4 opacity-50">
{% lucide "loader-circle", { "class": "animate-spin" } %}
</div>
</div>
<div class="relative">
<input type="text" class="input pl-9" placeholder="Processing..." disabled>
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4 opacity-50">
{% lucide "loader-circle", { "class": "animate-spin" } %}
</div>
</div>
<div class="relative">
<input type="text" class="input pr-24" placeholder="Saving changes..." disabled>
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2 text-sm text-muted-foreground opacity-50">
<div>Saving...</div>
<div class="[&>svg]:size-4">
{% lucide "loader-circle", { "class": "animate-spin" } %}
</div>
</div>
</div>
<div class="relative">
<input type="text" class="input pl-9 pr-26" placeholder="Refreshing data..." disabled>
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4 opacity-50">
{% lucide "loader", { "class": "animate-spin" } %}
</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2 text-sm text-muted-foreground opacity-50">
Please wait...
</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-spinner", code_spinner, class="w-full max-w-md") }}
<h3 id="example-dropdown"><a href="#example-dropdown">Dropdown</a></h3>
{% set code_dropdown %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pr-9" placeholder="Enter file name">
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{{ dropdown_menu(
main_attrs={"class": "absolute right-1.5 top-1/2 -translate-y-1/2 z-10"},
trigger=trigger,
trigger_attrs={"class": "btn-sm-icon-ghost text-muted-foreground hover:text-accent-foreground size-6"},
popover_attrs={"data-align": "end", "class": "min-w-32"},
items=[
{ type: "item", label: "Settings" },
{ type: "item", label: "Copy path" },
{ type: "item", label: "Open location" }
]
) }}
</div>
<div class="relative">
<input type="text" class="input pr-30" placeholder="Enter search query">
{% set trigger %}
Search in...
{% lucide "chevron-down", { "class": "size-3" } %}
{% endset %}
{{ dropdown_menu(
main_attrs={"class": "absolute right-1.5 top-1/2 -translate-y-1/2"},
trigger=trigger,
trigger_attrs={"class": "btn-sm-ghost text-muted-foreground hover:text-accent-foreground p-2 h-6"},
popover_attrs={"data-align": "end", "class": "min-w-32"},
items=[
{ type: "item", label: "Documentation" },
{ type: "item", label: "Blog Posts" },
{ type: "item", label: "Changelog" }
]
) }}
</div>
</div>
{% endset %}
{{ code_preview("input-group-dropdown", code_dropdown, class="w-full max-w-sm") }}
<h3 id="example-group"><a href="#example-group">Group</a></h3>
{% set code_group %}
<div class="flex w-fit items-stretch">
<label for="url" class="text-sm flex items-center bg-muted border border-r-0 px-4 rounded-l-md shadow-xs">https://</label>
<div class="relative">
<input type="text" class="input pr-9 rounded-none">
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4">{% lucide "link-2" %}</div>
</div>
<div class="text-sm flex items-center bg-muted border border-l-0 px-4 rounded-r-md shadow-xs">.com</div>
</div>
{% endset %}
{{ code_preview("input-group-group", code_group, class="w-full max-w-sm") }}
+320
View File
@@ -0,0 +1,320 @@
---
layout: layouts/page.njk
title: Item
description: A versatile component that you can use to display any content.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Variants
id: example-variants
- label: Size
id: example-size
- label: Icon
id: example-icon
- label: Avatar
id: example-avatar
- label: Image
id: example-image
- label: Group
id: example-group
- label: Header
id: example-header
- label: Link
id: example-link
---
{% from "macros/code_block.njk" import code_block %}
{% from "macros/code_preview.njk" import code_preview %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated Item component in Basecoat.</h2>
</div>
{% set code_basic %}
<div class="flex flex-col gap-6">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Basic Item</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">A simple item with title and description.</p>
</div>
<button class="btn-sm-outline">Action</button>
</article>
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border py-3 px-4 gap-2.5">
<div class="flex shrink-0 items-center justify-center gap-2 [&_svg]:pointer-events-none bg-transparent [&_svg]:size-5">
{% lucide "badge-check" %}
</div>
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Your profile has been verified.</h3>
</div>
<div class="flex items-center gap-2 [&_svg]:size-4">
{% lucide "chevron-right" %}
</div>
</a>
</div>
{% endset %}
{{ code_preview("item-basic", code_basic | prettyHtml, class="w-full max-w-md") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Items are pure HTML composition using Tailwind utility classes. Use semantic elements like <code>&lt;article&gt;</code> or <code>&lt;a&gt;</code>, apply flex layout, and compose with standard HTML elements like <code>&lt;h3&gt;</code>, <code>&lt;p&gt;</code>, <code>&lt;img&gt;</code>, or <code>&lt;button&gt;</code>.</p>
<p>Browse the examples below and copy the pattern that fits your needs.</p>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-variants"><a href="#example-variants">Variants</a></h3>
{% set code_variants %}
<div class="flex flex-col gap-6">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Default Variant</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Standard styling with subtle background and borders.</p>
</div>
<button class="btn-sm-outline">Open</button>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Outline Variant</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Outlined style with clear borders and transparent background.</p>
</div>
<button class="btn-sm-outline">Open</button>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent bg-muted/50 p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Muted Variant</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Subdued appearance with muted colors for secondary content.</p>
</div>
<button class="btn-sm-outline">Open</button>
</article>
</div>
{% endset %}
{{ code_preview("item-variants", code_variants) }}
<h3 id="example-size"><a href="#example-size">Size</a></h3>
<section class="prose">
<p>Use different padding and gap sizes for compact or spacious layouts.</p>
</section>
{{ code_preview("item-size", code_basic) }}
<h3 id="example-icon"><a href="#example-icon">Icon</a></h3>
{% set code_icon %}
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex shrink-0 items-center justify-center gap-2 self-start [&_svg]:pointer-events-none size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4">
{% lucide "shield-alert" %}
</div>
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Security Alert</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">New login detected from unknown device.</p>
</div>
<button class="btn-sm-outline">Review</button>
</article>
{% endset %}
{{ code_preview("item-icon", code_icon, class="max-w-lg") }}
<h3 id="example-avatar"><a href="#example-avatar">Avatar</a></h3>
{% set code_avatar %}
<div class="flex flex-col gap-6">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img src="https://github.com/hunvreus.png" alt="hunvreus" class="size-10 rounded-full object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">hunvreus</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Last seen 5 months ago</p>
</div>
<button class="btn-icon-outline rounded-full">
{% lucide "plus" %}
</button>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-8 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full self-start">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">No Team Members</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Invite your team to collaborate on this project.</p>
</div>
<button class="btn-outline">
Invite
</button>
</article>
</div>
{% endset %}
{{ code_preview("item-avatar", code_avatar, class="w-full max-w-lg") }}
<h3 id="example-image"><a href="#example-image">Image</a></h3>
{% set code_image %}
<div class="flex flex-col gap-6 w-full max-w-md">
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img src="https://avatar.vercel.sh/example_1" alt="Midnight City Lights" class="grayscale size-10 rounded-sm object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="w-fit items-center gap-2 text-sm leading-snug font-medium line-clamp-1">Midnight City Lights - <span class="text-muted-foreground">Electric Nights</span></h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Neon Dreams</p>
</div>
<div class="flex flex-col gap-1 flex-none text-center">
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">3:45</p>
</div>
</a>
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img src="https://avatar.vercel.sh/example_2" alt="Midnight City Lights" class="grayscale size-10 rounded-sm object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="w-fit items-center gap-2 text-sm leading-snug font-medium line-clamp-1">Coffee Shop Conversations - <span class="text-muted-foreground">Urban Stories</span></h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">The Morning Brew</p>
</div>
<div class="flex flex-col gap-1 flex-none text-center">
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">4:05</p>
</div>
</a>
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img src="https://avatar.vercel.sh/example_3" alt="Midnight City Lights" class="grayscale size-10 rounded-sm object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="w-fit items-center gap-2 text-sm leading-snug font-medium line-clamp-1">Digital Rain - <span class="text-muted-foreground">Binary Beats</span></h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Cyber Symphony</p>
</div>
<div class="flex flex-col gap-1 flex-none text-center">
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">3:30</p>
</div>
</a>
</div>
{% endset %}
{{ code_preview("item-image", code_image) }}
<h3 id="example-group"><a href="#example-group">Group</a></h3>
{% set code_group %}
<div class="flex flex-col">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<img src="https://github.com/hunvreus.png" alt="Ronan Berder" class="grayscale size-10 rounded-full object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">hunvreus</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">hunvreus@gmail.com</p>
</div>
<button class="btn-icon-outline rounded-full">
{% lucide "plus" %}
</button>
</article>
<hr role="separator" class="border-border">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<img src="https://github.com/shadcn.png" alt="shadcn" class="grayscale size-10 rounded-full object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">shadcn</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">shadcn@vercel.com</p>
</div>
<button class="btn-icon-outline rounded-full">
{% lucide "plus" %}
</button>
</article>
<hr role="separator" class="border-border">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<img src="https://github.com/adamwathan.png" alt="Adam Wathan" class="grayscale size-10 rounded-full object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">adamwathan</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">adam.wathan@gmail.com</p>
</div>
<button class="btn-icon-outline rounded-full">
{% lucide "plus" %}
</button>
</article>
</div>
{% endset %}
{{ code_preview("item-group", code_group, class="w-full max-w-md") }}
<h3 id="example-header"><a href="#example-header">Header</a></h3>
{% set code_header %}
<div class="flex-col grid grid-cols-3 gap-4">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img alt="v0-1.5-sm" loading="lazy" class="grayscale aspect-square w-full rounded-sm object-cover flex-basis" src="https://avatar.vercel.sh/v0-1.5-sm">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">v0-1.5-sm</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Everyday tasks and UI generation.</p>
</div>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img alt="v0-1.5-lg" loading="lazy" class="grayscale aspect-square w-full rounded-sm object-cover flex-basis" src="https://avatar.vercel.sh/v0-1.5-lg">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">v0-1.5-lg</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Advanced thinking or reasoning.</p>
</div>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img alt="v0-2.0-mini" loading="lazy" class="grayscale aspect-square w-full rounded-sm object-cover flex-basis" src="https://avatar.vercel.sh/v0-2.0-mini">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">v0-2.0-mini</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Open Source model for everyone.</p>
</div>
</article>
</div>
{% endset %}
{{ code_preview("item-header", code_header) }}
<h3 id="example-link"><a href="#example-link">Link</a></h3>
<section class="prose">
<p>Items can be rendered as links by using an <code>&lt;a&gt;</code> element. Hover and focus states are applied automatically.</p>
</section>
{% set code_link %}
<div class="flex flex-col gap-6">
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Visit our documentation</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Learn how to get started with our components.</p>
</div>
<div class="flex items-center gap-2 [&_svg]:size-4">
{% lucide "chevron-right" %}
</div>
</a>
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">External resource</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Opens in a new tab with security attributes.</p>
</div>
<div class="flex items-center gap-2 [&_svg]:size-4">
{% lucide "external-link" %}
</div>
</a>
</div>
{% endset %}
{{ code_preview("item-link", code_link, class="w-full max-w-md") }}
+76
View File
@@ -0,0 +1,76 @@
---
layout: layouts/page.njk
title: Kbd
description: Used to display textual user input from keyboard.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Group
id: example-group
- label: Button
id: example-button
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code_html %}
<div class="flex flex-col items-center gap-4">
<span class="inline-flex items-center gap-1">
<kbd class="kbd">⌘</kbd>
<kbd class="kbd">⇧</kbd>
<kbd class="kbd">⌥</kbd>
<kbd class="kbd">⌃</kbd>
</span>
<span class="inline-flex items-center gap-1">
<kbd class="kbd">Ctrl</kbd>
<span>+</span>
<kbd class="kbd">B</kbd>
</span>
</div>
{% endset %}
{{ code_preview("kbd", code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Use the <code>&lt;kbd&gt;</code> element with the <code>kbd</code> class:</p>
</section>
{% set code_simple %}
<kbd class="kbd">⌘K</kbd>
{% endset %}
{{ code_block(code_simple | prettyHtml, "html") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-group"><a href="#example-group">Group</a></h3>
{% set group_html %}
<p class="text-muted-foreground text-sm">
Use
<span class="inline-flex items-center gap-1">
<kbd class="kbd">Ctrl + B</kbd>
<kbd class="kbd">Ctrl + K</kbd>
</span>
to open the command palette
</p>
{% endset %}
{{ code_preview("kbd-group", group_html | prettyHtml) }}
<h3 id="example-button"><a href="#example-button">Button</a></h3>
{% set button_html %}
<div class="flex flex-wrap items-center gap-x-4">
<button class="btn-sm-outline pr-2">Accept <kbd class="kbd">⏎</kbd></button>
<button class="btn-sm-outline pr-2">Cancel <kbd class="kbd">Esc</kbd></button>
</div>
{% endset %}
{{ code_preview("kbd-button", button_html | prettyHtml) }}
+3 -3
View File
@@ -103,8 +103,8 @@ toc:
<li><code>onclick</code>: a simple function to update <code>aria-expanded</code>.</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;div popover class="popover" id="{ POPOVER_ID }"&gt;</code></dt>
<dd>As with the <a href="/components/popover">Popover</a> component, you can set up the side and alignment of the popover using the <code>data-side</code> and <code>data-align</code> attributes.</dd>
<dt><code class="highlight language-html">&lt;div data-popover class="popover" id="{ POPOVER_ID }"&gt;</code></dt>
<dd>You can set up the side and alignment of the popover using the <code>data-side</code> and <code>data-align</code> attributes.</dd>
</dl>
</section>
@@ -171,4 +171,4 @@ toc:
</form>
</div>
{% endcall %}{% endraw %}{% endset %}
{{ code_block(raw_code, "jinja") }}
{{ code_block(raw_code, "jinja") }}
+44
View File
@@ -0,0 +1,44 @@
---
layout: layouts/page.njk
title: Progress
description: Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.
toc:
- label: Usage
id: usage
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated Progress component in Basecoat.</h2>
</div>
{% set code_default %}
<div class="bg-primary/20 relative h-2 w-full overflow-hidden rounded-full">
<div id="demo-progress" class="bg-primary h-full w-full flex-1 transition-all" style="width: 17%"></div>
</div>
<script>
setTimeout(() => {
document.getElementById('demo-progress').style.width = '66%';
}, 500);
</script>
{% endset %}
{{ code_preview("progress-default", code_default, class="w-full max-w-sm") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Progress bars are pure HTML composition using Tailwind utility classes. Use a container div for the track and an inner div for the indicator. Set the indicator's width with inline styles or via JavaScript.</p>
</section>
{% set code_usage %}
<div class="bg-primary/20 relative h-2 w-full overflow-hidden rounded-full">
<div class="bg-primary h-full w-full flex-1 transition-all" style="width: 66%"></div>
</div>
{% endset %}
{{ code_block(code_usage) }}
+2 -2
View File
@@ -58,8 +58,8 @@ toc:
type: "group",
label: "Fruits",
items: [
{ type: "item", value: "apple", label: "Apple" },
{ type: "item", value: "banana", label: "Banana" },
{ type: "item", value: "apple", label: "Apple", attrs: {"data-force": "true"} },
{ type: "item", value: "banana", label: "Banana", attrs: {"data-keywords": "bread"} },
{ type: "item", value: "blueberry", label: "Blueberry" },
{ type: "item", value: "pineapple", label: "Grapes" },
{ type: "item", value: "pineapple", label: "Pineapple" }
+214
View File
@@ -0,0 +1,214 @@
---
layout: layouts/page.njk
title: Spinner
description: An indicator that can be used to show a loading state.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Size
id: example-size
- label: Color
id: example-color
- label: Button
id: example-button
- label: Badge
id: example-badge
- label: Input Group
id: example-input-group
- label: Empty
id: example-empty
- label: Item
id: example-item
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated Spinner component in Basecoat.</h2>
</div>
{% set code_default %}
<article class="group/item flex items-center border text-sm rounded-md transition-colors flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent bg-muted/50 p-4 gap-4">
<div class="flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:size-4">
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %}
</div>
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium line-clamp-1">Processing payment...</h3>
</div>
<div class="flex flex-col gap-1 flex-none text-center">
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">$100.00</p>
</div>
</article>
{% endset %}
{{ code_preview("spinner-default", code_default, class="w-full max-w-xs") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Spinners are pure HTML using the <code>loader-circle</code> Lucide icon with the <code>animate-spin</code> Tailwind utility. Add <code>role="status"</code> and <code>aria-label="Loading"</code> for accessibility.</p>
<p>Browse the examples below and copy the pattern that fits your needs.</p>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-size"><a href="#example-size">Size</a></h3>
<section class="prose">
<p>Use the <code>size-*</code> utility class to change the size of the spinner.</p>
</section>
{% set code_size %}
<div class="flex items-center gap-6">
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-3 animate-spin" } %}
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-4 animate-spin" } %}
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-6 animate-spin" } %}
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-8 animate-spin" } %}
</div>
{% endset %}
{{ code_preview("spinner-size", code_size) }}
<h3 id="example-color"><a href="#example-color">Color</a></h3>
<section class="prose">
<p>Use the <code>text-*</code> utility class to change the color of the spinner.</p>
</section>
{% set code_color %}
<div class="flex items-center gap-6">
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-6 animate-spin text-red-500" } %}
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-6 animate-spin text-green-500" } %}
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-6 animate-spin text-blue-500" } %}
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-6 animate-spin text-yellow-500" } %}
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-6 animate-spin text-purple-500" } %}
</div>
{% endset %}
{{ code_preview("spinner-color", code_color) }}
<h3 id="example-button"><a href="#example-button">Button</a></h3>
<section class="prose">
<p>Add a spinner to a button to indicate a loading state.</p>
</section>
{% set code_button %}
<div class="flex flex-col items-center gap-4">
<button class="btn-sm" disabled>
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %}
Loading...
</button>
<button class="btn-sm-outline" disabled>
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %}
Please wait
</button>
<button class="btn-sm-secondary" disabled>
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %}
Processing
</button>
</div>
{% endset %}
{{ code_preview("spinner-button", code_button) }}
<h3 id="example-badge"><a href="#example-badge">Badge</a></h3>
{% set code_badge %}
<div class="flex items-center gap-4">
<span class="badge">
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %}
Syncing
</span>
<span class="badge-secondary">
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %}
Updating
</span>
<span class="badge-outline">
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %}
Processing
</span>
</div>
{% endset %}
{{ code_preview("spinner-badge", code_badge) }}
<h3 id="example-input-group"><a href="#example-input-group">Input Group</a></h3>
<section class="prose">
<p>Input groups can have spinners positioned inside them.</p>
</section>
{% set code_input_group %}
<div class="flex w-full max-w-md flex-col gap-4">
<div class="relative">
<input type="text" class="input pr-9" placeholder="Send a message..." disabled>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %}
</div>
</div>
<div class="relative">
<textarea class="textarea pr-10 min-h-27 pb-12" placeholder="Send a message..." disabled></textarea>
<footer role="group" class="absolute bottom-0 px-3 pb-3 pt-1.5 flex items-center w-full gap-2">
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-4 animate-spin text-muted-foreground" } %}
<span class="text-sm text-muted-foreground">Validating...</span>
<button type="button" class="btn-sm-icon h-6 w-7.5 ml-auto rounded">
{% lucide "arrow-up" %}
</button>
</footer>
</div>
</div>
{% endset %}
{{ code_preview("spinner-input-group", code_input_group, class="w-full max-w-md") }}
<h3 id="example-empty"><a href="#example-empty">Empty</a></h3>
<section class="prose">
<p>Use a spinner in empty states to indicate a loading or processing state.</p>
</section>
{% set code_empty %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300">
<header class="flex max-w-sm flex-col items-center gap-3 text-center">
<div class="mb-2 bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6">
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin size-4" } %}
</div>
<h3 class="text-lg font-semibold tracking-tight">Processing your request</h3>
<p class="text-muted-foreground text-sm/relaxed">Please wait while we process your request. Do not refresh the page.</p>
</header>
<section class="flex w-full max-w-sm min-w-0 flex-col items-center gap-3">
<button class="btn-sm-outline">Cancel</button>
</section>
</div>
{% endset %}
{{ code_preview("spinner-empty", code_empty) }}
<h3 id="example-item"><a href="#example-item">Item</a></h3>
{% set code_item %}
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex shrink-0 items-center justify-center gap-2 self-start [&_svg]:pointer-events-none size-8 border rounded-md bg-muted [&_svg:not([class*='size-'])]:size-4">
{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin size-4 text-muted-foreground" } %}
</div>
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Downloading...</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">129 MB / 1000 MB</p>
</div>
<button class="btn-sm-outline self-start">Cancel</button>
<footer class="flex basis-full items-center justify-between gap-2">
<div class="bg-primary/20 relative h-2 w-full overflow-hidden rounded-full">
<div id="demo-progress" class="bg-primary h-full w-full flex-1 transition-all" style="width: 13%"></div>
</div>
</footer>
</article>
{% endset %}
{{ code_preview("spinner-item", code_item, class="w-full max-w-md") }}
+33 -1
View File
@@ -29,8 +29,12 @@ toc:
id: install-cli
- label: Use Nunjucks or Jinja macros
id: install-macros
- label: Icons
id: install-icons
- label: Theming
id: install-theming
- label: Customization
id: install-customization
---
{% from "macros/code_block.njk" import code_block %}
@@ -108,7 +112,8 @@ toc:
<div class="prose">
<p>Or cherry-pick specific components, for example:</p>
</div>
{% set code %}{% raw %}import 'basecoat-css/popover';
{% set code %}{% raw %}import 'basecoat-css/basecoat';
import 'basecoat-css/popover';
import 'basecoat-css/tabs';{% endraw %}{% endset %}
{{ code_block(code, "js") }}
<div class="prose">
@@ -192,6 +197,33 @@ import 'basecoat-css/tabs';{% endraw %}{% endset %}
{% endcall %}{% endraw %}{% endset %}
{{ code_block(code, "jinja") }}
<h2 id="install-icons"><a href="#install-icons">Icons</a></h2>
<div class="prose">
<p>Basecoat uses <a href="https://lucide.dev" target="_blank">Lucide icons</a>. You have three options:</p>
<ul>
<li><b>Copy SVG code</b>: Visit <a href="https://lucide.dev/icons" target="_blank">lucide.dev/icons</a>, click any icon to copy the SVG, and paste it directly in your HTML. Best for simple projects or occasional icon usage.</li>
<li><b>Install lucide</b>: Follow the <a href="https://lucide.dev/guide/installation" target="_blank">installation guide</a> to install the <code>lucide</code> package and dynamically render icons. Best for projects that need many icons or want tree-shaking.</li>
<li><b>Use framework packages</b>: Browse <a href="https://lucide.dev/packages" target="_blank">framework-specific packages</a> (React, Vue, Angular, etc.) for component-based icon usage. Best if you're using a supported framework.</li>
</ul>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="https://lucide.dev" target="_blank">
Browse icons
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://lucide.dev/guide/installation" target="_blank">
Installation guide
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://lucide.dev/packages" target="_blank">
Packages
{% lucide "arrow-right" %}
</a>
</div>
<h2 id="install-theming"><a href="#install-theming">Theming</a></h2>
<div class="prose">
+506
View File
@@ -0,0 +1,506 @@
Basecoat – LLMs Guide
About
- Basecoat is a Tailwind‑first component library using semantic HTML and small vanilla JS for interactivity. No React required. Framework‑agnostic.
Install
- Tailwind project: `npm i basecoat-css`
- In CSS: `@import "tailwindcss";` then `@import "basecoat-css";`
CDN
- CSS: `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/basecoat.cdn.min.css">`
- JS (all): `<script src="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/js/all.min.js" defer></script>`
- JS (per component): include `basecoat.min.js` then `tabs.min.js`, `select.min.js`, etc.
JavaScript
- Auto‑init on `DOMContentLoaded`; observes DOM for new nodes.
- Manual: `window.basecoat.init('<name>')` or `window.basecoat.initAll()`.
- Packages to import with ESM: `import 'basecoat-css/all'` or `import 'basecoat-css/tabs'`.
Usage
- CSS‑only components: use classes like `btn`, `card`, `input`, etc.
- JS components: include the JS and use required roles/attributes (see below).
Theming
- Tailwind projects can override design tokens (e.g., `primary`, `muted-foreground`).
- Without Tailwind, use CDN CSS and add overrides after the link tag.
Common pitfalls
- Do not invent class names; use documented classes only.
- The package name is `basecoat-css` (not `basecoat`, not `@basecoat/css`).
- Interactive components require specific roles/attributes; include them.
- Basecoat is framework‑agnostic; do not mention React/Radix dependencies.
Components
- Button (CSS): `.btn`, variants like `.btn-secondary`, sizes `.btn-sm`, `.btn-lg`.
- Button Group (CSS): `.button-group` to group adjacent buttons; supports horizontal/vertical.
- Input (CSS): `.input` for text inputs; pairs with `.label` and `.field` patterns.
- Textarea (CSS): `.textarea` styled multiline input.
- Label (CSS): `.label` for associated form labels.
- Field (CSS): `.field` wrapper for label, input, hint, error.
- Form (CSS): `.form` patterns for layout and validation styling.
- Checkbox (CSS): styled native checkbox; use `input[type='checkbox']`.
- Radio Group (CSS): `.radio-group` pattern using native radios.
- Switch (CSS): styled checkbox with `role="switch"` if needed.
- Select (JS): `.select` custom select; trigger button + popover + `[role=listbox]` and `[role=option]`. Dispatches `change` with `detail.value`.
- Combobox (JS): `.combobox` typeahead; similar roles to Select.
- Command (JS): `.command` with search input and `[role=menu]`/`[role=menuitem]`; keyboard nav.
- Tabs (JS): `.tabs` with `[role=tablist]`, `[role=tab]`, `aria-controls` panels; arrow keys switch.
- Popover (JS): `.popover` wraps a button and `[data-popover]` content; toggles `aria-hidden`.
- Dropdown Menu (JS): `.dropdown-menu` trigger + menu with `[role=menu]`/`[role=menuitem]`.
- Dialog (CSS/Native): `<dialog>` styled; `.command-dialog` variant wraps `.command`.
- Alert Dialog (CSS/Native): confirm/cancel flow using `<dialog>` semantics.
- Drawer (CSS/Native): off‑canvas using `<dialog>`/popover; class names documented in examples.
- Tooltip (CSS/JS-lite): `.tooltip` small info overlays.
- Toast (JS): global notifications container `#toaster`; dispatch `basecoat:toast` with `{ detail: { config } }`.
- Sidebar (JS): `.sidebar` with collapsible items; tracks current page via `aria-current`.
- Breadcrumb (CSS): `.breadcrumb` list of links.
- Pagination (CSS): `.pagination` navigation controls.
- Card (CSS): `.card` container with header/section/footer slots.
- Alert (CSS): `.alert` contextual banners.
- Badge (CSS): `.badge` and `.badge-outline`.
- Avatar (CSS): `.avatar` image + fallback.
- Skeleton (CSS): `.skeleton` loading placeholder.
- Progress (CSS): `.progress` bar.
- Slider (CSS): `.slider` styled input range.
- Spinner (CSS): `.spinner` loading indicator.
- Kbd (CSS): `.kbd` keyboard key style.
- Table (CSS): `.table` basic and striped tables.
- Item (CSS): `.item` list rows with icons and actions.
- Empty State (CSS): `.empty` placeholder content.
- Theme Switcher (JS-lite): `.theme-switcher` toggles themes (example in docs).
Events
- `basecoat:toast` (document): show a toast by dispatching `{ detail: { config } }`.
- `basecoat:initialized` (component root): emitted after a component initializes.
- `basecoat:popover` (document): coordination event to close other popovers.
Accessibility
- Use semantic roles/attributes from examples (tabs, menus, listbox/options).
- Disabled items: use `disabled` or `aria-disabled="true"`; JS will ignore them.
Troubleshooting
- If a component doesn’t work, ensure its JS is loaded and required roles/IDs match examples.
- For dynamic content, call `window.basecoat.init('<name>')` or `initAll()` after insertion.
License
- MIT (see LICENSE.md)
---
description: Basecoat
alwaysApply: true
applyTo: "**"
downloadedFrom: https://basecoatui.com/llms.txt
version: 0.3.x
---
# Basecoat
Basecoat is a Tailwind CSS v4‑first UI library using semantic HTML and small vanilla JS for interactivity. No React required; framework‑agnostic.
- Basecoat docs: https://basecoatui.com
- Package CSS: `basecoat-css`
## Basecoat install notes
1. Basecoat targets Tailwind CSS v4.
2. Import Tailwind in your CSS, then import Basecoat:
```css
@import "tailwindcss";
@import "basecoat-css";
```
3. For interactive components, include JS. With ESM you can load all or cherry‑pick:
```js
import 'basecoat-css/all';
// or cherry‑pick
import 'basecoat-css/tabs';
import 'basecoat-css/select';
import 'basecoat-css/popover';
import 'basecoat-css/dropdown-menu';
import 'basecoat-css/sidebar';
import 'basecoat-css/toast';
```
4. CDN usage (no build tool):
```html
<link href="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/basecoat.cdn.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/js/all.min.js" defer></script>
```
5. JS auto‑initializes on `DOMContentLoaded` and observes the DOM for added nodes. You can manually re‑init:
```js
window.basecoat.init('tabs');
window.basecoat.initAll();
```
## Basecoat usage rules
1. Add Basecoat class names to semantic HTML. Combine with Tailwind utilities for layout/spacing.
2. Interactive components require specific roles/attributes (see each component). Keep `aria-*` for semantics and `data-*` for visual states.
3. Disabled elements must use `disabled` or `aria-disabled="true"`. Basecoat JS filters them from interactions.
4. Only use documented Basecoat classes or Tailwind utilities. Don’t invent new class names.
5. Prefer native elements (button, input, dialog) and keep markup minimal.
Basecoat class name categories (for reference)
- `component`: the main component class
- `part`: a child element/slot of a component
- `style`: variants changing appearance
- `size`: size variants
- `behavior`: classes that change behavior
- `modifier`: boolean/conditional changes
## Config
Basecoat has no Tailwind plugin config. Import the CSS after Tailwind. Customize design tokens via your Tailwind theme or overrides.
Minimal CSS setup:
```css
@import "tailwindcss";
@import "basecoat-css";
```
## Basecoat tokens (commonly used)
These semantic tokens are referenced by classes (examples): `background`, `foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `muted`, `muted-foreground`, `card`, `card-foreground`, `popover`, `popover-foreground`, `destructive`, `ring`, `input`, `border`.
Rules
- Tokens should be defined in your Tailwind theme or be available via your design system. You can override colors using Tailwind utilities (e.g., `bg-primary`).
- Prefer utilities for one‑off overrides; keep semantic tokens consistent.
## Basecoat components
### alert
Alerts inform users about important events.
#### Class names
- component: `alert`, `alert-destructive`
#### Syntax
```html
<div role="alert" class="alert">{CONTENT}</div>
```
#### Rules
- Use `alert-destructive` for destructive context.
### avatar
Avatar shows a user thumbnail.
#### Class names
- component: `avatar`
#### Syntax
```html
<div class="avatar"><img src="{image-url}" alt="{alt}" /></div>
```
#### Rules
- Provide meaningful `alt` text or decorative as appropriate.
### badge
Badges display statuses or counts.
#### Class names
- component: `badge`
- style: `badge-outline`, `badge-destructive`
#### Syntax
```html
<span class="badge {MODIFIER}">{TEXT}</span>
```
#### Rules
- {MODIFIER} is optional; combine with utilities for spacing.
### button
Buttons trigger actions.
#### Class names
- component: `btn`
- style: `btn-primary`, `btn-secondary`, `btn-outline`, `btn-ghost`, `btn-link`, `btn-destructive`
- size: `btn-sm`, `btn-lg`
- icon: `btn-icon`, `btn-sm-icon`, `btn-lg-icon`
#### Syntax
```html
<button class="btn {MODIFIER}">{CONTENT}</button>
```
#### Rules
- Use `disabled` attribute for disabled state. Use `aria-pressed="true"` for toggle buttons.
### button-group
Groups adjacent buttons into a single control.
#### Class names
- component: `button-group`
#### Syntax
```html
<div class="button-group" data-orientation="{horizontal|vertical}">{BUTTONS}</div>
```
#### Rules
- Use separators with `<hr role="separator">` inside the group if needed.
### card
Card container with optional header/section/footer.
#### Class names
- component: `card`
- part: `> header`, `> section`, `> footer`
#### Syntax
```html
<article class="card">
<header><h2>{TITLE}</h2><p>{SUBTITLE}</p></header>
<section>{CONTENT}</section>
<footer>{ACTIONS}</footer>
</article>
```
#### Rules
- Parts are optional; structure with semantic elements.
### checkbox (CSS)
Styled native checkbox.
#### Class names
- component: `.input[type='checkbox']:not([role='switch'])`
#### Syntax
```html
<input type="checkbox" class="input" />
```
#### Rules
- Use standard checkbox semantics; pair with `<label>`.
### command (JS)
Command palette with search and results.
#### Class names
- component: `command`, wrapper variant: `command-dialog`
- part: `> header input`, `[role='menu']`, `[role='menuitem']`
#### Syntax
```html
<div class="command">
<header><input type="text" placeholder="Search" /></header>
<div role="menu">
<button role="menuitem">{ITEM}</button>
</div>
</div>
```
#### Rules
- Requires JS. Keyboard navigation supported. Disabled items must use `aria-disabled="true"` or `disabled` and are ignored.
### dropdown-menu (JS)
Contextual menu triggered by a button.
#### Class names
- component: `dropdown-menu`
- part: `> button` (trigger), `[role='menu']`, `[role='menuitem']`
#### Syntax
```html
<div class="dropdown-menu">
<button type="button">{TRIGGER}</button>
<div role="menu"> <button role="menuitem">{ITEM}</button> </div>
</div>
```
#### Rules
- Requires JS. Uses `basecoat:popover` to coordinate with other popovers.
### input (CSS)
Styled text input fields.
#### Class names
- component: `input` (applied to native `<input>` types)
#### Syntax
```html
<input type="text" class="input" placeholder="{Placeholder}" />
```
#### Rules
- Supports common input types; pair with `.label` and `.field` patterns.
### kbd (CSS)
Keyboard key visual.
#### Class names
- component: `kbd`
#### Syntax
```html
<kbd class="kbd">⌘K</kbd>
```
### label (CSS)
Styled form labels.
#### Class names
- component: `label`
#### Syntax
```html
<label class="label" for="{id}">{TEXT}</label>
```
### popover (JS)
Small overlay anchored to a trigger.
#### Class names
- component: `popover`
- part: `> button` (trigger), `[data-popover]` (content)
#### Syntax
```html
<div class="popover">
<button type="button">{TRIGGER}</button>
<section data-popover aria-hidden="true">{CONTENT}</section>
</div>
```
#### Rules
- Requires JS. Toggling sets `aria-hidden` on content and `aria-expanded` on trigger.
### radio-group (CSS)
Styled native radio inputs.
#### Class names
- component: `.input[type='radio']`
#### Syntax
```html
<label><input type="radio" class="input" name="{name}" /> {LABEL}</label>
```
### select (JS)
Custom select with searchable popover and listbox.
#### Class names
- component: `select`
- part: `> button` (trigger with label span), `[data-popover]` (panel), `[role='listbox']`, `[role='option']`, `> input[type='hidden']`, optional `header input[type='text']` filter
#### Syntax
```html
<div class="select">
<button type="button" aria-expanded="false"><span>{LABEL}</span></button>
<input type="hidden" name="{name}" value="{value}" />
<section data-popover aria-hidden="true">
<header><input type="text" placeholder="Filter" /></header>
<div role="listbox">
<div role="option" data-value="{value}" data-label="{label}">{label}</div>
</div>
</section>
</div>
```
#### Rules
- Requires JS. Dispatches `change` on root with `detail.value`. Disabled options use `aria-disabled="true"`.
### sidebar (JS)
Collapsible navigation sidebar.
#### Class names
- component: `sidebar`
#### Rules
- Requires JS. Uses `aria-current="page"` for active link styling.
### switch (CSS)
Styled switch using checkbox with `role="switch"`.
#### Class names
- component: `.input[type='checkbox'][role='switch']`
#### Syntax
```html
<input type="checkbox" role="switch" class="input" />
```
### table (CSS)
Styled tables.
#### Class names
- component: `table`
#### Syntax
```html
<table class="table">{ROWS}</table>
```
### tabs (JS)
Tabbed interface with keyboard navigation.
#### Class names
- component: `tabs`
- part: `[role='tablist']`, `[role='tab']`, `[role='tabpanel']`
#### Syntax
```html
<div class="tabs">
<div role="tablist">
<button role="tab" aria-controls="panel-1" aria-selected="true" tabindex="0">Tab 1</button>
<button role="tab" aria-controls="panel-2" aria-selected="false" tabindex="-1">Tab 2</button>
</div>
<section id="panel-1" role="tabpanel">...</section>
<section id="panel-2" role="tabpanel" hidden>...</section>
</div>
```
#### Rules
- Requires JS. Arrow keys move focus; keep `aria-controls` and `aria-selected` accurate.
### textarea (CSS)
Styled multiline text input.
#### Class names
- component: `textarea`
#### Syntax
```html
<textarea class="textarea" rows="{n}"></textarea>
```
### toast (JS)
Global notifications.
#### Class names
- component: `#toaster` (container), `toast` (item)
#### Syntax
```html
<section id="toaster"></section>
```
Show a toast:
```js
document.dispatchEvent(new CustomEvent('basecoat:toast', { detail: { config: { title: 'Saved', description: 'Changes persisted', category: 'success' } } }))
```
#### Rules
- Requires JS. Uses timeouts; hovering pauses timers.
### popover/dialog variants (CSS/JS‑lite)
Dialog and drawer patterns built on native elements, styled by Basecoat.
#### Class names
- wrapper: `command-dialog` (for Command palette in a dialog)
#### Rules
- Use native `<dialog>` or popover attributes where applicable.
### other CSS components
Additional CSS components include: `progress`, `skeleton`, `slider`, `spinner`, `pagination`, `breadcrumb`, `item`, `empty`, `theme-switcher`, `form`, `field`, `input-group`, `radio-group`, `switch`, `avatar`, `badge`, `alert`, `card`, `kbd`, `table`.
## Events
- `basecoat:initialized` (component root): emitted after a component initializes.
- `basecoat:popover` (document): used to close other popovers when one opens.
- `basecoat:toast` (document): creates a toast using `{ detail: { config } }`.
## Accessibility
- Use semantic roles/labels per component. Keep `aria-*` for semantics and `data-*` for visual states.
- Disabled items must use `disabled` or `aria-disabled="true"` to be filtered from interactions.
## Troubleshooting
- If interactivity doesn’t work, ensure the component’s JS is included and required roles/IDs match the examples.
- For dynamic content, call `window.basecoat.init('<name>')` or `window.basecoat.initAll()` after insertion.
## License
MIT (see LICENSE.md)
+4 -4
View File
@@ -1,12 +1,12 @@
{
"name": "basecoat",
"version": "0.2.0-beta.1",
"version": "0.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "basecoat",
"version": "0.2.0-beta.1",
"version": "0.3.2",
"license": "MIT",
"workspaces": [
"packages/*"
@@ -4337,7 +4337,7 @@
},
"packages/cli": {
"name": "basecoat-cli",
"version": "0.2.0-beta.1",
"version": "0.3.2",
"license": "MIT",
"dependencies": {
"commander": "^13.1.0",
@@ -4350,7 +4350,7 @@
},
"packages/css": {
"name": "basecoat-css",
"version": "0.2.0-beta.1",
"version": "0.3.2",
"license": "MIT"
}
}
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "basecoat",
"version": "0.3.2",
"version": "0.3.9",
"description": "shadcn/ui for the rest of us",
"author": "hunvreus",
"type": "module",
@@ -21,8 +21,7 @@
"css",
"html",
"jinja",
"nunjucks",
"alpinejs"
"nunjucks"
],
"license": "MIT",
"repository": {
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "basecoat-cli",
"version": "0.3.2",
"version": "0.3.9",
"description": "Add Basecoat components to your project",
"author": "hunvreus",
"license": "MIT",
@@ -30,8 +30,7 @@
"css",
"html",
"jinja",
"nunjucks",
"alpinejs"
"nunjucks"
],
"repository": {
"type": "git",
+6 -3
View File
@@ -1,6 +1,6 @@
{
"name": "basecoat-css",
"version": "0.3.2",
"version": "0.3.9",
"description": "Tailwind CSS for Basecoat components",
"author": {
"name": "hunvreus",
@@ -26,8 +26,7 @@
"css",
"html",
"jinja",
"nunjucks",
"alpinejs"
"nunjucks"
],
"repository": {
"type": "git",
@@ -41,8 +40,12 @@
"exports": {
".": "./dist/basecoat.css",
"./css": "./dist/basecoat.css",
"./basecoat": "./dist/js/basecoat.js",
"./basecoat.min": "./dist/js/basecoat.min.js",
"./all": "./dist/js/all.js",
"./all.min": "./dist/js/all.min.js",
"./command": "./dist/js/command.js",
"./command.min": "./dist/js/command.min.js",
"./dropdown-menu": "./dist/js/dropdown-menu.js",
"./dropdown-menu.min": "./dist/js/dropdown-menu.min.js",
"./popover": "./dist/js/popover.js",
+1 -1
View File
@@ -121,7 +121,7 @@ async function build() {
// Create combined component files
console.log('Creating combined component files...');
const componentsToCombine = ['basecoat.js', 'dropdown-menu.js', 'popover.js', 'select.js', 'sidebar.js', 'tabs.js', 'toast.js'];
const componentsToCombine = ['basecoat.js', 'command.js', 'dropdown-menu.js', 'popover.js', 'select.js', 'sidebar.js', 'tabs.js', 'toast.js'];
const componentPaths = componentsToCombine.map(f => path.join(srcJsDir, f));
// Create non-minified bundle
+359 -149
View File
@@ -146,10 +146,6 @@
border-radius: var(--scrollbar-radius);
}
}
/* Alpine.js */
[x-cloak] {
display: none !important;
}
}
/* Alert */
@@ -158,10 +154,16 @@
.alert-destructive {
@apply relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current;
h2 {
> h2,
> h3,
> h4,
> h5,
> h6,
> strong,
> [data-title] {
@apply col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight;
}
section {
> section {
@apply text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed;
ul {
@@ -175,12 +177,36 @@
.alert-destructive {
@apply text-destructive bg-card [&>svg]:text-current;
section {
> section {
@apply text-destructive;
}
}
}
/* Badge */
@layer components {
.badge,
.badge-primary,
.badge-secondary,
.badge-destructive,
.badge-outline {
@apply inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden;
}
.badge,
.badge-primary {
@apply border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90;
}
.badge-secondary {
@apply border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90;
}
.badge-destructive {
@apply border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60;
}
.badge-outline {
@apply text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground;
}
}
/* Button */
@layer components {
.btn,
@@ -359,27 +385,46 @@
}
}
/* Badge */
/* Button Group */
@layer components {
.badge,
.badge-primary,
.badge-secondary,
.badge-destructive,
.badge-outline {
@apply inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden;
}
.badge,
.badge-primary {
@apply border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90;
}
.badge-secondary {
@apply border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90;
}
.badge-destructive {
@apply border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60;
}
.badge-outline {
@apply text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground;
.button-group {
@apply inline-flex w-fit items-stretch isolate;
> *:focus-visible,
> :is(.dropdown-menu, .popover, .select) > button:focus-visible {
@apply relative z-10;
}
> hr[role='separator'] {
@apply w-0 h-auto self-stretch border border-input shrink-0 m-0;
}
&:not([data-orientation='vertical']) {
> *:not(:first-child),
> :is(.dropdown-menu, .popover, .select):not(:first-child) > button {
@apply rounded-l-none border-l-0;
}
> *:not(:last-child),
> :is(.dropdown-menu, .popover, .select):not(:last-child) > button {
@apply rounded-r-none;
}
}
&[data-orientation='vertical'] {
@apply flex-col;
> hr[role='separator'] {
@apply w-auto h-px;
}
> *:not(:first-child),
> :is(.dropdown-menu, .popover, .select):not(:first-child) > button {
@apply rounded-t-none border-t-0;
}
> *:not(:last-child),
> :is(.dropdown-menu, .popover, .select):not(:last-child) > button {
@apply rounded-b-none;
}
}
}
}
@@ -410,7 +455,7 @@
/* Checkbox */
@layer components {
.form input[type='checkbox']:not([role='switch']),
:is(.form, .field) input[type='checkbox']:not([role='switch']),
.input[type='checkbox']:not([role='switch']) {
@apply appearance-none border-input dark:bg-input/30 checked:bg-primary dark:checked:bg-primary checked:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50;
@@ -442,6 +487,115 @@
}
}
/* Command */
@layer components {
.command-dialog {
@apply inset-y-0 opacity-0 transition-all transition-discrete;
&:is([open],:popover-open) {
@apply opacity-100;
&::backdrop {
@apply opacity-100;
}
> * {
@apply scale-100;
}
@starting-style {
@apply opacity-0;
&::backdrop {
@apply opacity-0;
}
> * {
@apply scale-95;
}
}
}
&::backdrop {
@apply bg-black/50 opacity-0 transition-all transition-discrete;
}
> * {
@apply bg-background fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%_-_2rem)] -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-lg border shadow-lg sm:max-w-lg max-h-[min(640px,calc(100%_-_2rem))];
@apply transition-all scale-95;
> button,
> form[method='dialog'] {
@apply absolute top-4 right-4;
}
> button,
> form[method='dialog'] > button {
@apply ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4;
}
}
.command {
> header input {
@apply h-12;
}
[role='menu'] {
@apply px-2;
[role='menuitem'] {
@apply py-3 px-2 [&_svg]:size-5;
}
[role='heading'] {
@apply px-2 py-1.5 font-medium text-muted-foreground;
}
}
}
}
.command {
> header {
@apply flex items-center border-b px-3 gap-2;
svg {
@apply size-4 shrink-0 opacity-50;
}
input {
@apply placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50;
}
}
[role='menu'] {
@apply max-h-[300px] overflow-y-auto overflow-x-hidden p-1;
[role='menuitem'] {
@apply aria-hidden:hidden relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none aria-disabled:opacity-50 aria-disabled:pointer-events-none disabled:opacity-50 disabled:pointer-events-none w-full truncate [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4;
&.active {
@apply bg-accent text-accent-foreground;
}
}
[role='heading'] {
@apply text-muted-foreground flex px-2 py-1.5 text-xs font-medium;
}
[role='group']:not(:has([role='menuitem']:not([aria-hidden='true']))) {
@apply hidden;
}
[role='separator'] {
@apply border-border -mx-1 my-1;
}
&:not(:has([role='menuitem']:not([aria-hidden='true'])))::before {
@apply flex items-center justify-center py-6 px-3 text-sm truncate -m-1;
}
&[data-empty]:not(:has([role='menuitem']:not([aria-hidden='true'])))::before {
@apply content-[attr(data-empty)];
}
&:not([data-empty]):not(:has([role='menuitem']:not([aria-hidden='true'])))::before {
@apply content-['No_results_found'];
}
}
&:not([data-command-initialized]) [role='menuitem'] {
@apply hover:bg-accent hover:text-accent-foreground;
}
&:has(> header input:not(:placeholder-shown)) [role='separator'] {
@apply hidden;
}
}
}
/* Dialog */
@layer components {
.dialog {
@@ -453,7 +607,7 @@
&::backdrop {
@apply opacity-100;
}
> article {
> * {
@apply scale-100;
}
@@ -463,7 +617,7 @@
&::backdrop {
@apply opacity-0;
}
> article {
> * {
@apply scale-95;
}
}
@@ -471,7 +625,7 @@
&::backdrop {
@apply bg-black/50 opacity-0 transition-all transition-discrete;
}
> article {
> * {
@apply bg-background fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%_-_2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border p-6 shadow-lg sm:max-w-lg max-h-[calc(100%_-_2rem)];
@apply transition-all scale-95;
@@ -542,21 +696,65 @@
}
}
/* Field */
@layer components {
.fieldset {
@apply flex flex-col gap-6;
> legend {
@apply text-base font-medium mb-3;
}
}
.field {
@apply flex flex-col w-full gap-3 data-[invalid=true]:text-destructive [&>*]:w-full [&>.sr-only]:w-auto;
h2,
h3 {
@apply flex w-fit items-center gap-2 text-sm leading-snug font-medium;
}
[role="alert"] {
@apply text-sm text-destructive font-normal;
ul {
@apply ml-4 flex list-disc flex-col gap-1;
}
}
section {
@apply flex flex-1 flex-col gap-1.5 leading-snug;
}
&[data-orientation='horizontal'] {
@apply flex-row items-center [&>label]:flex-auto has-[>section]:items-start has-[>section]:[&>[type=checkbox],[type=radio]]:mt-px [&_p]:text-balance;
}
}
.fieldset legend + p,
.field > p,
.field section > p {
@apply text-muted-foreground text-sm leading-normal font-normal [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4;
}
.fieldset legend + p {
@apply -mt-1.5;
}
.field > p {
@apply last:mt-0 nth-last-2:-mt-1;
}
}
/* Input */
@layer components {
.form input[type='text'],
.form input[type='email'],
.form input[type='password'],
.form input[type='number'],
.form input[type='file'],
.form input[type='tel'],
.form input[type='url'],
.form input[type='search'],
.form input[type='date'],
.form input[type='datetime-local'],
.form input[type='month'],
.form input[type='week'],
.form input[type='time'],
:is(.form, .field) input[type='text']:not(:is(.select [data-popover] > header input[role='combobox'], .command > header input, .dialog > * > header input)),
:is(.form, .field) input[type='email'],
:is(.form, .field) input[type='password'],
:is(.form, .field) input[type='number'],
:is(.form, .field) input[type='file'],
:is(.form, .field) input[type='tel'],
:is(.form, .field) input[type='url'],
:is(.form, .field) input[type='search']:not(:is(.select [data-popover] > header input[role='combobox'], .command > header input, .dialog > * > header input)),
:is(.form, .field) input[type='date'],
:is(.form, .field) input[type='datetime-local'],
:is(.form, .field) input[type='month'],
:is(.form, .field) input[type='week'],
:is(.form, .field) input[type='time'],
.input[type='text'],
.input[type='email'],
.input[type='password'],
@@ -576,9 +774,16 @@
}
}
/* Kbd */
@layer components {
.kbd {
@apply bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none [&_svg:not([class*='size-'])]:size-3;
}
}
/* Label */
@layer components {
.form label,
:is(.form, .field) label,
.label {
@apply flex items-center gap-2 text-sm leading-none font-medium select-none peer-disabled:pointer-events-none peer-disabled:opacity-50;
@@ -663,7 +868,7 @@
/* Radio */
@layer components {
.form input[type='radio'],
:is(.form, .field) input[type='radio'],
.input[type='radio'] {
@apply appearance-none border-input text-primary 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 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 relative;
@@ -675,7 +880,7 @@
/* Range */
@layer components {
.form input[type='range'],
:is(.form, .field) input[type='range'],
.input[type='range'] {
@apply appearance-none flex items-center p-0 outline-none;
--slider-value: 20%;
@@ -724,7 +929,7 @@
/* Select */
@layer components {
.form select,
:is(.form, .field) select,
select.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];
@@ -780,6 +985,9 @@
@apply content-['No_results_found'];
}
}
[data-popover]:has(> header input:not(:placeholder-shown)) [role='separator'] {
@apply hidden;
}
&:not([data-select-initialized]) [data-popover] [role='option'] {
@apply hover:bg-accent hover:text-accent-foreground;
}
@@ -787,119 +995,121 @@
}
/* Sidebar */
.sidebar {
&:not([data-sidebar-initialized]) {
@apply max-md:hidden;
}
&:not([aria-hidden]),
&[aria-hidden=false] {
@apply max-md:bg-black/50 max-md:fixed max-md:inset-0 max-md:z-40;
}
nav {
@apply bg-sidebar text-sidebar-foreground flex flex-col w-(--sidebar-mobile-width) md:w-(--sidebar-width) fixed inset-y-0 z-50 transition-transform ease-in-out duration-300;
}
& + * {
@apply transition-[margin] ease-in-out duration-300;
}
&:not([data-side]),
&[data-side=left] {
@layer components {
.sidebar {
&:not([data-sidebar-initialized]) {
@apply max-md:hidden;
}
&:not([aria-hidden]),
&[aria-hidden=false] {
@apply max-md:bg-black/50 max-md:fixed max-md:inset-0 max-md:z-40;
}
nav {
@apply left-0 border-r;
@apply bg-sidebar text-sidebar-foreground flex flex-col w-(--sidebar-mobile-width) md:w-(--sidebar-width) fixed inset-y-0 z-50 transition-transform ease-in-out duration-300;
}
& + * {
@apply relative md:ml-(--sidebar-width);
@apply transition-[margin] ease-in-out duration-300;
}
&[aria-hidden=true] {
&:not([data-side]),
&[data-side=left] {
nav {
@apply -translate-x-full;
@apply left-0 border-r;
}
& + * {
@apply md:ml-0;
@apply relative md:ml-(--sidebar-width);
}
&[aria-hidden=true] {
nav {
@apply -translate-x-full;
}
& + * {
@apply md:ml-0;
}
}
}
&[data-side=right] {
nav {
@apply right-0 border-l;
}
& + * {
@apply relative md:mr-(--sidebar-width);
}
&[aria-hidden=true] {
nav {
@apply translate-x-full;
}
& + * {
@apply md:mr-0;
}
}
}
}
&[data-side=right] {
nav {
@apply right-0 border-l;
}
& + * {
@apply relative md:mr-(--sidebar-width);
}
&[aria-hidden=true] {
nav {
@apply translate-x-full;
> header,
> footer {
@apply flex flex-col gap-2 p-2;
}
& + * {
@apply md:mr-0;
[role=separator] {
@apply border-sidebar-border mx-2 w-auto;
}
}
}
nav {
> header,
> footer {
@apply flex flex-col gap-2 p-2;
}
[role=separator] {
@apply border-sidebar-border mx-2 w-auto;
}
> section {
@apply flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto;
> section {
@apply flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto;
> [role=group] {
@apply relative flex w-full min-w-0 flex-col p-2;
}
h3 {
@apply text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0;
}
ul {
@apply flex w-full min-w-0 flex-col gap-1;
li {
@apply relative;
> a,
> details > summary {
@apply flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&[aria-current=page]]:bg-sidebar-accent [&[aria-current=page]]:font-medium [&[aria-current=page]]:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0;
&:not([data-variant]),
&[data-variant=default] {
@apply hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
&[data-variant=outline] {
@apply bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))];
}
&:not([data-size]),
&[data-size=default] {
@apply h-8 text-sm;
}
&[data-size=sm] {
@apply h-7 text-xs;
}
&[data-size=lg] {
@apply h-12 text-sm group-data-[collapsible=icon]:p-0!;
}
}
> details {
&:not([open]) {
> summary {
&::after {
@apply -rotate-90;
}
}
}
> summary {
&::after {
@apply content-[''] block size-3.5 bg-primary ml-auto transition-transform ease-linear;
@apply mask-[image:var(--chevron-down-icon)] mask-size-[1rem] mask-no-repeat mask-center;
}
}
&::details-content {
@apply px-3.5;
}
}
> [role=group] {
@apply relative flex w-full min-w-0 flex-col p-2;
}
h3 {
@apply text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0;
}
ul {
@apply border-sidebar-border flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5 w-full;
@apply flex w-full min-w-0 flex-col gap-1;
li {
@apply relative;
> a,
> details > summary {
@apply flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&[aria-current=page]]:bg-sidebar-accent [&[aria-current=page]]:font-medium [&[aria-current=page]]:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0;
&:not([data-variant]),
&[data-variant=default] {
@apply hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
&[data-variant=outline] {
@apply bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))];
}
&:not([data-size]),
&[data-size=default] {
@apply h-8 text-sm;
}
&[data-size=sm] {
@apply h-7 text-xs;
}
&[data-size=lg] {
@apply h-12 text-sm group-data-[collapsible=icon]:p-0!;
}
}
> details {
&:not([open]) {
> summary {
&::after {
@apply -rotate-90;
}
}
}
> summary {
&::after {
@apply content-[''] block size-3.5 bg-primary ml-auto transition-transform ease-linear;
@apply mask-[image:var(--chevron-down-icon)] mask-size-[1rem] mask-no-repeat mask-center;
}
}
&::details-content {
@apply px-3.5;
}
}
}
ul {
@apply border-sidebar-border flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5 w-full;
}
}
}
}
@@ -908,7 +1118,7 @@
/* Switch */
@layer components {
.form input[type='checkbox'][role='switch'],
:is(.form, .field) input[type='checkbox'][role='switch'],
.input[type='checkbox'][role='switch'] {
@apply appearance-none focus-visible:border-ring focus-visible:ring-ring/50 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50;
@apply bg-input dark:bg-input/80 checked:bg-primary dark:checked:bg-primary;
@@ -970,7 +1180,7 @@
/* Textarea */
@layer components {
.form textarea,
:is(.form, .field) textarea,
.textarea {
@apply border-input placeholder:text-muted-foreground 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 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm;
}
@@ -1089,4 +1299,4 @@
}
}
}
}
}
+206
View File
@@ -0,0 +1,206 @@
{#
Renders a standalone command menu component with search and keyboard navigation.
@param id {string} [optional] - Unique identifier for the command component.
@param items {array} [optional] - Array of items to render (alternative to using caller).
@param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input.
@param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param input_attrs {object} [optional] - Additional HTML attributes for the search input.
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
#}
{% macro command(
id=None,
items=None,
placeholder="Type a command or search...",
empty_text="No results found.",
main_attrs={},
input_attrs={},
menu_attrs={}
) %}
{% set id = id or ("command-" ~ range(100000, 999999) | random | string) %}
<div
id="{{ id }}"
class="command {{ main_attrs.class }}"
aria-label="Command menu"
{% for key, value in main_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<header>
<svg 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-search-icon lucide-search"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input
type="text"
id="{{ id }}-input"
placeholder="{{ placeholder }}"
autocomplete="off"
autocorrect="off"
spellcheck="false"
aria-autocomplete="list"
role="combobox"
aria-expanded="true"
aria-controls="{{ id }}-menu"
{% for key, value in input_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
</header>
<div
role="menu"
id="{{ id }}-menu"
aria-orientation="vertical"
data-empty="{{ empty_text }}"
class="scrollbar {{ menu_attrs.class }}"
{% for key, value in menu_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% if items and items|length > 0 %}
{{ render_command_items(items, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{% endif %}
</div>
</div>
{% endmacro %}
{#
Renders a command dialog (modal command palette).
@param id {string} [optional] - Unique identifier for the command dialog.
@param items {array} [optional] - Array of items to render (alternative to using caller).
@param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input.
@param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog element.
@param input_attrs {object} [optional] - Additional HTML attributes for the search input.
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
@param open {boolean} [optional] [default=False] - Whether the command dialog should be open initially.
#}
{% macro command_dialog(
id=None,
items=None,
placeholder="Type a command or search...",
empty_text="No results found.",
dialog_attrs={},
input_attrs={},
menu_attrs={},
open=False
) %}
{% set id = id or ("command-dialog-" ~ range(100000, 999999) | random | string) %}
<dialog
id="{{ id }}"
class="dialog command-dialog {{ dialog_attrs.class }}"
aria-label="Command menu"
{% if open %}open{% endif %}
onclick="if (event.target === this) this.close()"
{% for key, value in dialog_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<div class="command">
<header>
<svg 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-search-icon lucide-search"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input
type="text"
id="{{ id }}-input"
placeholder="{{ placeholder }}"
autocomplete="off"
autocorrect="off"
spellcheck="false"
aria-autocomplete="list"
role="combobox"
aria-expanded="true"
aria-controls="{{ id }}-menu"
{% for key, value in input_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
</header>
<div
role="menu"
id="{{ id }}-menu"
aria-orientation="vertical"
data-empty="{{ empty_text }}"
class="scrollbar {{ menu_attrs.class }}"
{% for key, value in menu_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% if items and items|length > 0 %}
{{ render_command_items(items, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{% endif %}
</div>
<button type="button" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<svg 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-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
</dialog>
{% endmacro %}
{#
Renders a list of items for the command component.
@param items {array} - Array of items to render. Each item can be:
- { type: "item", label: "Text or HTML", icon: "HTML", url: "href", keywords: "search terms", disabled: true, attrs: {} }
- { type: "group", label: "Group Title", items: [...] }
- { type: "separator" }
Note: Use attrs to add data-label for alternative search text if needed. If url is provided, renders as <a role="menuitem"> instead of <div role="menuitem">. Label supports HTML (marked as safe).
@param parent_id_prefix {string} [optional] - The prefix for the item id.
#}
{% macro render_command_items(items, parent_id_prefix="items") %}
{% for item in 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 }}"
{% if item.attrs %}
{% for key, value in item.attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
{% endif %}
>
<span role="heading" id="{{ group_label_id }}">{{ item.label | safe }}</span>
{{ render_command_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
id="{{ item_id }}"
href="{{ item.url }}"
role="menuitem"
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
{% if item.disabled %}aria-disabled="true"{% endif %}
{% if item.attrs %}
{% for key, value in item.attrs.items() %}
{% if key != "href" %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
{% endif %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | safe }}
</a>
{% else %}
<div
id="{{ item_id }}"
role="menuitem"
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
{% if item.disabled %}aria-disabled="true"{% endif %}
{% if item.attrs %}
{% for key, value in item.attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
{% endif %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | safe }}
</div>
{% endif %}
{% endif %}
{% endfor %}
{% endmacro %}
+5 -5
View File
@@ -8,7 +8,7 @@
@param footer {string} [optional] - HTML content for the dialog footer.
@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 dialog_attrs {object} [optional] - Additional HTML attributes for the dialog content article.
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog content container.
@param header_attrs {object} [optional] - Additional HTML attributes for the dialog header.
@param body_attrs {object} [optional] - Additional HTML attributes for the dialog body section.
@param footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer.
@@ -40,7 +40,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
{{ trigger }}
{{ trigger | safe }}
</button>
{% endif %}
<dialog
@@ -53,10 +53,10 @@
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<article>
<div>
{% if title or description %}
<header
{% for key, value in header_attrs %}
{% for key, value in header_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
@@ -89,6 +89,6 @@
</button>
</form>
{% endif %}
</article>
</div>
</dialog>
{% endmacro %}
+4 -2
View File
@@ -86,7 +86,7 @@
{% endif %}
>
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
{{ render_dropdown_items(item.items, item_id) if item.items }}
{{ render_dropdown_items(item['items'], item_id) if item['items'] }}
</div>
{% elif item.type == "separator" %}
<hr role="separator" />
@@ -102,6 +102,7 @@
{% endfor %}
{% endif %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | safe }}
</a>
{% else %}
@@ -114,9 +115,10 @@
{% endfor %}
{% endif %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | safe }}
</div>
{% endif %}
{% endif %}
{% endfor %}
{% endmacro %}
{% endmacro %}
+3 -3
View File
@@ -33,7 +33,7 @@
{% if items %}
{% for item in items %}
{% if item.type == "group" %}
{% for sub_item in item.items %}
{% for sub_item in item['items'] %}
{% if not first_option.item %}
{% set first_option.item = sub_item %}
{% endif %}
@@ -153,7 +153,7 @@
{% endif %}
>
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
{{ render_select_items(item.items, selected, item_id) if item.items }}
{{ render_select_items(item['items'], selected, item_id) if item['items'] }}
</div>
{% elif item.type == "separator" %}
<hr role="separator" />
@@ -173,4 +173,4 @@
</div>
{% endif %}
{% endfor %}
{% endmacro %}
{% endmacro %}
+2 -2
View File
@@ -117,7 +117,7 @@
>
<summary aria-controls="submenu-{{ item_id }}-content">
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label }}
{{ item.label | safe }}
</summary>
<ul id="submenu-{{ item_id }}-content">
{{ render_sidebar_content(item["items"], item_id) if item["items"] }}
@@ -136,7 +136,7 @@
{% endif %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
<span>{{ item.label }}</span>
<span>{{ item.label | safe }}</span>
</a>
</li>
{% endif %}
+12 -3
View File
@@ -5,6 +5,8 @@
@param id {string} [optional] [default="toaster"] - Unique identifier for the toaster container.
@param toasts {array} [optional] - An array of toast objects to render initially. See the <code>toast()</code> macro for more details.
@param attrs {object} [optional] - Additional HTML attributes for the main toaster container div.
Supports caller() for custom toast markup.
#}
{% macro toaster(
id="toaster",
@@ -24,9 +26,11 @@
title=item.title,
description=item.description,
action=item.action,
cancel=item.cancel
cancel=item.cancel,
attrs=item.attrs
) }}
{% endfor %}
{{ caller() if caller }}
</div>
{% endmacro %}
@@ -44,20 +48,25 @@
- label {string}: Button text.
- onclick {string}: JavaScript code to execute on click.
- url {string}: URL for an anchor link button.
@param attrs {object} [optional] - Additional HTML attributes for the toast div.
#}
{% macro toast(
category="success",
title="",
description="",
action=None,
cancel=None
cancel=None,
attrs={}
) %}
<div
class="toast"
class="toast {{ attrs.class }}"
role="{{ 'alert' if category == 'error' else 'status' }}"
aria-atomic="true"
aria-hidden="false"
{% if category %}data-category="{{ category }}"{% endif %}
{% for key, value in attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<div class="toast-content">
{% if category in ["error", "success", "info", "warning"] %}
+160
View File
@@ -0,0 +1,160 @@
(() => {
const initCommand = (container) => {
const input = container.querySelector('header input');
const menu = container.querySelector('[role="menu"]');
if (!input || !menu) {
const missing = [];
if (!input) missing.push('input');
if (!menu) missing.push('menu');
console.error(`Command component initialization failed. Missing element(s): ${missing.join(', ')}`, container);
return;
}
const allMenuItems = Array.from(menu.querySelectorAll('[role="menuitem"]'));
const menuItems = allMenuItems.filter(item =>
!item.hasAttribute('disabled') &&
item.getAttribute('aria-disabled') !== 'true'
);
let visibleMenuItems = [...menuItems];
let activeIndex = -1;
const setActiveItem = (index) => {
if (activeIndex > -1 && menuItems[activeIndex]) {
menuItems[activeIndex].classList.remove('active');
}
activeIndex = index;
if (activeIndex > -1) {
const activeItem = menuItems[activeIndex];
activeItem.classList.add('active');
if (activeItem.id) {
input.setAttribute('aria-activedescendant', activeItem.id);
} else {
input.removeAttribute('aria-activedescendant');
}
} else {
input.removeAttribute('aria-activedescendant');
}
};
const filterMenuItems = () => {
const searchTerm = input.value.trim().toLowerCase();
setActiveItem(-1);
visibleMenuItems = [];
allMenuItems.forEach(item => {
if (item.hasAttribute('data-force')) {
item.setAttribute('aria-hidden', 'false');
if (menuItems.includes(item)) {
visibleMenuItems.push(item);
}
return;
}
const itemText = (item.dataset.filter || item.textContent).trim().toLowerCase();
const keywordList = (item.dataset.keywords || '')
.toLowerCase()
.split(/[\s,]+/)
.filter(Boolean);
const matchesKeyword = keywordList.some(keyword => keyword.includes(searchTerm));
const matches = itemText.includes(searchTerm) || matchesKeyword;
item.setAttribute('aria-hidden', String(!matches));
if (matches && menuItems.includes(item)) {
visibleMenuItems.push(item);
}
});
if (visibleMenuItems.length > 0) {
setActiveItem(menuItems.indexOf(visibleMenuItems[0]));
visibleMenuItems[0].scrollIntoView({ block: 'nearest' });
}
};
input.addEventListener('input', filterMenuItems);
const handleKeyNavigation = (event) => {
if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End'].includes(event.key)) {
return;
}
if (event.key === 'Enter') {
event.preventDefault();
if (activeIndex > -1) {
menuItems[activeIndex]?.click();
}
return;
}
if (visibleMenuItems.length === 0) return;
event.preventDefault();
const currentVisibleIndex = activeIndex > -1 ? visibleMenuItems.indexOf(menuItems[activeIndex]) : -1;
let nextVisibleIndex = currentVisibleIndex;
switch (event.key) {
case 'ArrowDown':
if (currentVisibleIndex < visibleMenuItems.length - 1) {
nextVisibleIndex = currentVisibleIndex + 1;
}
break;
case 'ArrowUp':
if (currentVisibleIndex > 0) {
nextVisibleIndex = currentVisibleIndex - 1;
} else if (currentVisibleIndex === -1) {
nextVisibleIndex = 0;
}
break;
case 'Home':
nextVisibleIndex = 0;
break;
case 'End':
nextVisibleIndex = visibleMenuItems.length - 1;
break;
}
if (nextVisibleIndex !== currentVisibleIndex) {
const newActiveItem = visibleMenuItems[nextVisibleIndex];
setActiveItem(menuItems.indexOf(newActiveItem));
newActiveItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
};
menu.addEventListener('mousemove', (event) => {
const menuItem = event.target.closest('[role="menuitem"]');
if (menuItem && visibleMenuItems.includes(menuItem)) {
const index = menuItems.indexOf(menuItem);
if (index !== activeIndex) {
setActiveItem(index);
}
}
});
menu.addEventListener('click', (event) => {
const clickedItem = event.target.closest('[role="menuitem"]');
if (clickedItem && visibleMenuItems.includes(clickedItem)) {
const dialog = container.closest('dialog.command-dialog');
if (dialog && !clickedItem.hasAttribute('data-keep-command-open')) {
dialog.close();
}
}
});
input.addEventListener('keydown', handleKeyNavigation);
if (visibleMenuItems.length > 0) {
setActiveItem(menuItems.indexOf(visibleMenuItems[0]));
visibleMenuItems[0].scrollIntoView({ block: 'nearest' });
}
container.dataset.commandInitialized = true;
container.dispatchEvent(new CustomEvent('basecoat:initialized'));
};
if (window.basecoat) {
window.basecoat.register('command', '.command:not([data-command-initialized])', initCommand);
}
})();
+22 -8
View File
@@ -16,7 +16,8 @@
return;
}
const options = Array.from(listbox.querySelectorAll('[role="option"]'));
const allOptions = Array.from(listbox.querySelectorAll('[role="option"]'));
const options = allOptions.filter(opt => opt.getAttribute('aria-disabled') !== 'true');
let visibleOptions = [...options];
let activeIndex = -1;
@@ -47,7 +48,7 @@
const updateValue = (option, triggerEvent = true) => {
if (option) {
selectedLabel.innerHTML = option.dataset.label || option.innerHTML;
selectedLabel.innerHTML = option.innerHTML;
input.value = option.dataset.value;
listbox.querySelector('[role="option"][aria-selected="true"]')?.removeAttribute('aria-selected');
option.setAttribute('aria-selected', 'true');
@@ -69,7 +70,7 @@
const resetFilter = () => {
filter.value = '';
visibleOptions = [...options];
options.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
allOptions.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
};
if (hasTransition()) {
@@ -110,11 +111,24 @@
setActiveOption(-1);
visibleOptions = [];
options.forEach(option => {
const optionText = (option.dataset.label || option.textContent).trim().toLowerCase();
const matches = optionText.includes(searchTerm);
allOptions.forEach(option => {
if (option.hasAttribute('data-force')) {
option.setAttribute('aria-hidden', 'false');
if (options.includes(option)) {
visibleOptions.push(option);
}
return;
}
const optionText = (option.dataset.filter || option.textContent).trim().toLowerCase();
const keywordList = (option.dataset.keywords || '')
.toLowerCase()
.split(/[\s,]+/)
.filter(Boolean);
const matchesKeyword = keywordList.some(keyword => keyword.includes(searchTerm));
const matches = optionText.includes(searchTerm) || matchesKeyword;
option.setAttribute('aria-hidden', String(!matches));
if (matches) {
if (matches && options.includes(option)) {
visibleOptions.push(option);
}
});
@@ -280,4 +294,4 @@
if (window.basecoat) {
window.basecoat.register('select', 'div.select:not([data-select-initialized])', initSelect);
}
})();
})();
+206
View File
@@ -0,0 +1,206 @@
{#
Renders a standalone command menu component with search and keyboard navigation.
@param id {string} [optional] - Unique identifier for the command component.
@param items {array} [optional] - Array of items to render (alternative to using caller).
@param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input.
@param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param input_attrs {object} [optional] - Additional HTML attributes for the search input.
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
#}
{% macro command(
id=None,
items=None,
placeholder="Type a command or search...",
empty_text="No results found.",
main_attrs={},
input_attrs={},
menu_attrs={}
) %}
{% set id = id or ("command-" + (range(100000, 999999) | random | string)) %}
<div
id="{{ id }}"
class="command {{ main_attrs.class }}"
aria-label="Command menu"
{% for key, value in main_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<header>
<svg 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-search-icon lucide-search"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input
type="text"
id="{{ id }}-input"
placeholder="{{ placeholder }}"
autocomplete="off"
autocorrect="off"
spellcheck="false"
aria-autocomplete="list"
role="combobox"
aria-expanded="true"
aria-controls="{{ id }}-menu"
{% for key, value in input_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
</header>
<div
role="menu"
id="{{ id }}-menu"
aria-orientation="vertical"
data-empty="{{ empty_text }}"
class="scrollbar {{ menu_attrs.class }}"
{% for key, value in menu_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% if items and items.length > 0 %}
{{ render_command_items(items, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{% endif %}
</div>
</div>
{% endmacro %}
{#
Renders a command dialog (modal command palette).
@param id {string} [optional] - Unique identifier for the command dialog.
@param items {array} [optional] - Array of items to render (alternative to using caller).
@param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input.
@param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog element.
@param input_attrs {object} [optional] - Additional HTML attributes for the search input.
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
@param open {boolean} [optional] [default=false] - Whether the command dialog should be open initially.
#}
{% macro command_dialog(
id=None,
items=None,
placeholder="Type a command or search...",
empty_text="No results found.",
dialog_attrs={},
input_attrs={},
menu_attrs={},
open=false
) %}
{% set id = id or ("command-dialog-" + (range(100000, 999999) | random | string)) %}
<dialog
id="{{ id }}"
class="command-dialog {{ dialog_attrs.class }}"
aria-label="Command menu"
{% if open %}open{% endif %}
onclick="if (event.target === this) this.close()"
{% for key, value in dialog_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<div class="command">
<header>
<svg 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-search-icon lucide-search"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input
type="text"
id="{{ id }}-input"
placeholder="{{ placeholder }}"
autocomplete="off"
autocorrect="off"
spellcheck="false"
aria-autocomplete="list"
role="combobox"
aria-expanded="true"
aria-controls="{{ id }}-menu"
{% for key, value in input_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
</header>
<div
role="menu"
id="{{ id }}-menu"
aria-orientation="vertical"
data-empty="{{ empty_text }}"
class="scrollbar {{ menu_attrs.class }}"
{% for key, value in menu_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% if items and items.length > 0 %}
{{ render_command_items(items, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{% endif %}
</div>
<button type="button" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<svg 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-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
</dialog>
{% endmacro %}
{#
Renders a list of items for the command component.
@param items {array} - Array of items to render. Each item can be:
- { type: "item", label: "Text or HTML", icon: "HTML", url: "href", keywords: "search terms", disabled: true, attrs: {} }
- { type: "group", label: "Group Title", items: [...] }
- { type: "separator" }
Note: Use attrs to add data-label for alternative search text if needed. If url is provided, renders as <a role="menuitem"> instead of <div role="menuitem">. Label supports HTML (marked as safe).
@param parent_id_prefix {string} [optional] - The prefix for the item id.
#}
{% macro render_command_items(items, parent_id_prefix="items") %}
{% for item in 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 }}"
{% if item.attrs %}
{% for key, value in item.attrs %}
{{ key }}="{{ value }}"
{% endfor %}
{% endif %}
>
<span role="heading" id="{{ group_label_id }}">{{ item.label | safe }}</span>
{{ render_command_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
id="{{ item_id }}"
href="{{ item.url }}"
role="menuitem"
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
{% if item.disabled %}aria-disabled="true"{% endif %}
{% if item.attrs %}
{% for key, value in item.attrs %}
{% if key != "href" %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
{% endif %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | safe }}
</a>
{% else %}
<div
id="{{ item_id }}"
role="menuitem"
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
{% if item.disabled %}aria-disabled="true"{% endif %}
{% if item.attrs %}
{% for key, value in item.attrs %}
{{ key }}="{{ value }}"
{% endfor %}
{% endif %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | safe }}
</div>
{% endif %}
{% endif %}
{% endfor %}
{% endmacro %}
+4 -4
View File
@@ -8,7 +8,7 @@
@param footer {string} [optional] - HTML content for the dialog footer.
@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 dialog_attrs {object} [optional] - Additional HTML attributes for the dialog content article.
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog content container.
@param header_attrs {object} [optional] - Additional HTML attributes for the dialog header.
@param body_attrs {object} [optional] - Additional HTML attributes for the dialog body section.
@param footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer.
@@ -40,7 +40,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
{{ trigger }}
{{ trigger | safe }}
</button>
{% endif %}
<dialog
@@ -53,7 +53,7 @@
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<article>
<div>
{% if title or description %}
<header
{% for key, value in header_attrs %}
@@ -87,6 +87,6 @@
<svg 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-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
{% endif %}
</article>
</div>
</dialog>
{% endmacro %}
+3 -1
View File
@@ -102,6 +102,7 @@
{% endfor %}
{% endif %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | safe }}
</a>
{% else %}
@@ -114,9 +115,10 @@
{% endfor %}
{% endif %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | safe }}
</div>
{% endif %}
{% endif %}
{% endfor %}
{% endmacro %}
{% endmacro %}
+2 -2
View File
@@ -117,7 +117,7 @@
>
<summary aria-controls="submenu-{{ item_id }}-content">
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label }}
{{ item.label | safe }}
</summary>
<ul id="submenu-{{ item_id }}-content">
{{ render_sidebar_content(item.items, item_id) if item.items }}
@@ -136,7 +136,7 @@
{% endif %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
<span>{{ item.label }}</span>
<span>{{ item.label | safe }}</span>
</a>
</li>
{% endif %}
+12 -3
View File
@@ -5,6 +5,8 @@
@param id {string} [optional] [default="toaster"] - Unique identifier for the toaster container.
@param toasts {array} [optional] - An array of toast objects to render initially. See the <code>toast()</code> macro for more details.
@param attrs {object} [optional] - Additional HTML attributes for the main toaster container div.
Supports caller() for custom toast markup.
#}
{% macro toaster(
id="toaster",
@@ -24,9 +26,11 @@
title=item.title,
description=item.description,
action=item.action,
cancel=item.cancel
cancel=item.cancel,
attrs=item.attrs
) }}
{% endfor %}
{{ caller() if caller }}
</div>
{% endmacro %}
@@ -44,20 +48,25 @@
- label {string}: Button text.
- onclick {string}: JavaScript code to execute on click.
- url {string}: URL for an anchor link button.
@param attrs {object} [optional] - Additional HTML attributes for the toast div.
#}
{% macro toast(
category="success",
title="",
description="",
action=None,
cancel=None
cancel=None,
attrs={}
) %}
<div
class="toast"
class="toast {{ attrs.class }}"
role="{{ 'alert' if category == 'error' else 'status' }}"
aria-atomic="true"
aria-hidden="false"
{% if category %}data-category="{{ category }}"{% endif %}
{% for key, value in attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<div class="toast-content">
{% if category in ["error", "success", "info", "warning"] %}