Prepare 1.0.0-beta.3

This commit is contained in:
Ronan Berder
2026-06-20 13:58:42 +08:00
parent 0ae97ae627
commit 971b2e171b
100 changed files with 4755 additions and 4287 deletions
+9
View File
@@ -2,11 +2,20 @@
## Unreleased
## [1.0.0-beta.3] - 2026-06-20
### Breaking Changes
- Changed Button, Badge, Card, Avatar, and Alert visual APIs to use a canonical root class plus data attributes instead of composed visual classes. For example, use `class="btn" data-variant="outline"`, `class="badge" data-variant="secondary"`, `class="card" data-size="sm"`, `class="avatar" data-size="lg"`, and `class="alert" data-variant="destructive"`. Legacy aliases are available only through the optional compatibility stylesheet.
- Changed icon-only Button sizing from `data-icon="only"` plus optional `data-size` to upstream-aligned `data-size="icon|icon-xs|icon-sm|icon-lg"`.
### Added
- Added an opt-in `basecoat-css/compat` stylesheet for pre-1.0 default Basecoat class aliases.
- Added an optional Chart.js helper with `window.basecoat.chart()`, chart CSS, generated tooltips, generated legends, and docs.
- Added shared Alert action layout support for direct child `<footer>` action regions.
- Added dedicated Avatar and Avatar Group component styles and docs.
- Added dedicated Progress component styles and docs with label, controlled, and RTL examples.
## [1.0.0-beta.2] - 2026-06-14
+40
View File
@@ -1,9 +1,47 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'astro/config';
import reallySimpleDocs from 'reallysimpledocs/astro';
const rsdCssPath = (relativePath) =>
fileURLToPath(new URL(`./node_modules/reallysimpledocs/src/css/${relativePath}`, import.meta.url));
function basecoatDocsStyles() {
return {
name: 'basecoat-docs-styles',
hooks: {
'astro:config:setup': ({ config }) => {
const root = fileURLToPath(config.root);
const generatedStyles = path.join(root, '.astro', 'reallysimpledocs', 'styles.css');
fs.mkdirSync(path.dirname(generatedStyles), { recursive: true });
fs.writeFileSync(
generatedStyles,
[
'@import "tailwindcss/theme";',
'@import "tailwindcss/utilities";',
'@reference "basecoat-css/base";',
`@import ${JSON.stringify(rsdCssPath('components.css'))};`,
`@import ${JSON.stringify(rsdCssPath('sources.css'))};`,
`@import ${JSON.stringify(rsdCssPath('custom.css'))};`,
`@import ${JSON.stringify(rsdCssPath('overrides.css'))};`,
`@source ${JSON.stringify(fileURLToPath(new URL('./node_modules/reallysimpledocs/src/runtime/**/*.{astro,js,ts}', import.meta.url)))};`,
`@source ${JSON.stringify(path.join(root, 'docs/src/**/*.{md,mdx}'))};`,
'',
].join('\n'),
);
},
},
};
}
export default defineConfig({
site: process.env.SITE_URL || 'https://basecoatui.com',
vite: {
build: {
assetsInlineLimit: 0,
},
resolve: {
dedupe: ['basecoat-css'],
},
@@ -14,6 +52,7 @@ export default defineConfig({
routeBase: '/',
style: 'vega',
components: {
Head: './docs/src/site/StyleHead.astro',
ContentHeader: './docs/src/site/ContentHeader.astro',
},
site: {
@@ -36,5 +75,6 @@ export default defineConfig({
},
},
}),
basecoatDocsStyles(),
],
});
+5 -1
View File
@@ -1 +1,5 @@
@import "../../../../packages/css/dist/basecoat-luma.cdn.css";
@import "tailwindcss" source(none);
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-luma.css";
@import "../../../../node_modules/reallysimpledocs/src/css/styles/luma.css";
+5 -1
View File
@@ -1 +1,5 @@
@import "../../../../packages/css/dist/basecoat-lyra.cdn.css";
@import "tailwindcss" source(none);
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-lyra.css";
@import "../../../../node_modules/reallysimpledocs/src/css/styles/lyra.css";
+5 -1
View File
@@ -1 +1,5 @@
@import "../../../../packages/css/dist/basecoat-maia.cdn.css";
@import "tailwindcss" source(none);
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-maia.css";
@import "../../../../node_modules/reallysimpledocs/src/css/styles/maia.css";
+5 -1
View File
@@ -1 +1,5 @@
@import "../../../../packages/css/dist/basecoat-mira.cdn.css";
@import "tailwindcss" source(none);
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-mira.css";
@import "../../../../node_modules/reallysimpledocs/src/css/styles/mira.css";
+5 -1
View File
@@ -1 +1,5 @@
@import "../../../../packages/css/dist/basecoat-nova.cdn.css";
@import "tailwindcss" source(none);
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-nova.css";
@import "../../../../node_modules/reallysimpledocs/src/css/styles/nova.css";
+5 -1
View File
@@ -1 +1,5 @@
@import "../../../../packages/css/dist/basecoat-rhea.cdn.css";
@import "tailwindcss" source(none);
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-rhea.css";
@import "../../../../node_modules/reallysimpledocs/src/css/styles/rhea.css";
+5 -1
View File
@@ -1 +1,5 @@
@import "../../../../packages/css/dist/basecoat-sera.cdn.css";
@import "tailwindcss" source(none);
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-sera.css";
@import "../../../../node_modules/reallysimpledocs/src/css/styles/sera.css";
+5
View File
@@ -0,0 +1,5 @@
@import "tailwindcss" source(none);
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-vega.css";
@import "../../../../node_modules/reallysimpledocs/src/css/styles/vega.css";
+14 -14
View File
@@ -1,7 +1,7 @@
# Alert Dialog
<Preview>
<button type="button" onclick="document.getElementById('demo-alert-dialog').showModal()" class="btn-outline">Open alert dialog</button>
<button type="button" onclick="document.getElementById('demo-alert-dialog').showModal()" class="btn" data-variant="outline">Open alert dialog</button>
<dialog id="demo-alert-dialog" class="dialog" aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description">
<div>
<header>
@@ -9,8 +9,8 @@
<p id="alert-dialog-description">This action cannot be undone. This will permanently delete your account and remove your data from our servers.</p>
</header>
<footer>
<button class="btn-outline" onclick="document.getElementById('demo-alert-dialog').close()">Cancel</button>
<button class="btn-primary" onclick="document.getElementById('demo-alert-dialog').close()">Continue</button>
<button class="btn" data-variant="outline" onclick="document.getElementById('demo-alert-dialog').close()">Cancel</button>
<button class="btn" data-variant="primary" onclick="document.getElementById('demo-alert-dialog').close()">Continue</button>
</footer>
</div>
</dialog>
@@ -25,7 +25,7 @@
Alert Dialog uses the same `.dialog` class as [Dialog](/components/dialog), but the example omits the close button and does not close on backdrop click.
```html
<button type="button" onclick="document.getElementById('demo-alert-dialog').showModal()" class="btn-outline">Open alert dialog</button>
<button type="button" onclick="document.getElementById('demo-alert-dialog').showModal()" class="btn" data-variant="outline">Open alert dialog</button>
<dialog id="demo-alert-dialog" class="dialog" aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description">
<div>
<header>
@@ -33,8 +33,8 @@ Alert Dialog uses the same `.dialog` class as [Dialog](/components/dialog), but
<p id="alert-dialog-description">This action cannot be undone. This will permanently delete your account and remove your data from our servers.</p>
</header>
<footer>
<button class="btn-outline" onclick="document.getElementById('demo-alert-dialog').close()">Cancel</button>
<button class="btn-primary" onclick="document.getElementById('demo-alert-dialog').close()">Continue</button>
<button class="btn" data-variant="outline" onclick="document.getElementById('demo-alert-dialog').close()">Cancel</button>
<button class="btn" data-variant="primary" onclick="document.getElementById('demo-alert-dialog').close()">Continue</button>
</footer>
</div>
</dialog>
@@ -43,29 +43,29 @@ Alert Dialog uses the same `.dialog` class as [Dialog](/components/dialog), but
### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;button type="button"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;button type="button"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The trigger button to open the alert dialog (using <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal" target="_blank">the native <code>showModal()</code> method</a>).</dd>
<dt><code class="highlight language-html">&lt;dialog class="dialog" id="&#123; DIALOG_ID &#125;"&gt;</code></dt>
<dt><code>&lt;dialog class="dialog" id="&#123; DIALOG_ID &#125;"&gt;</code></dt>
<dd>
Native modal dialog. Add <code>aria-labelledby</code> and <code>aria-describedby</code> when title and description are present.
<dl>
<dt><code class="highlight language-html">&lt;div&gt;</code></dt>
<dt><code>&lt;div&gt;</code></dt>
<dd>
Wraps the content of the alert dialog.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dt><code>&lt;header&gt;</code></dt>
<dd>
Contains the header for the alert dialog:
<dl>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dt><code>&lt;h2&gt;</code></dt>
<dd>The title of the alert dialog, must have an <code>id</code> if you use the <code>aria-labelledby</code> attribute on the <code>&lt;dialog&gt;</code>.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;p&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The description of the alert dialog, must have an <code>id</code> if you use the <code>aria-describedby</code> attribute on the <code>&lt;dialog&gt;</code>.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;section&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>For the content.</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;footer&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Usually contains actions.</dd>
</dl>
</dd>
+12 -12
View File
@@ -12,7 +12,7 @@
<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">
<div class="alert" data-variant="destructive">
<svg class="lucide lucide-circle-check" 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"><circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" /></svg>
<h2>Unable to process your payment.</h2>
<section>
@@ -29,7 +29,7 @@
## Usage
Use `alert` for default styling or `alert-destructive` for error states. Add a direct child `<footer>` for an inline-end action region.
Use `alert` for default styling. Add `data-variant="destructive"` for error states. Add a direct child `<footer>` for an inline-end action region.
```html
<div class="alert">
@@ -42,17 +42,17 @@ Use `alert` for default styling or `alert-destructive` for error states. Add a d
### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;div class="alert"&gt;</code></dt>
<dt><code>&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.
Main container. Use <code>data-variant="destructive"</code> for error states.
<dl>
<dt><code class="highlight language-html">&lt;svg&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;svg&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The icon.</dd>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dt><code>&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>
<dt><code>&lt;section&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The description.</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;footer&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The action region, aligned to the inline end of the alert. Put the action element inside it, such as a button or link.</dd>
</dl>
</dd>
@@ -63,7 +63,7 @@ Use `alert` for default styling or `alert-destructive` for error states. Add a d
### Destructive
<Preview>
<div class="alert-destructive">
<div class="alert" data-variant="destructive">
<svg class="lucide lucide-circle-alert" 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"><circle cx="12" cy="12" r="10" /><line x1="12" x2="12" y1="8" y2="12" /><line x1="12" x2="12.01" y1="16" y2="16" /></svg>
<h2>Something went wrong!</h2>
<section>Your session has expired. Please log in again.</section>
@@ -96,7 +96,7 @@ Use `alert` for default styling or `alert-destructive` for error states. Add a d
<svg class="lucide lucide-circle-alert" 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" ><circle cx="12" cy="12" r="10" /><line x1="12" x2="12" y1="8" y2="12" /><line x1="12" x2="12.01" y1="16" y2="16" /></svg>
<h2>The selected emails have been marked as spam.</h2>
<footer>
<button type="button" class="btn-xs-outline">Undo</button>
<button type="button" class="btn" data-variant="outline" data-size="xs">Undo</button>
</footer>
</div>
<div class="alert">
@@ -104,7 +104,7 @@ Use `alert` for default styling or `alert-destructive` for error states. Add a d
<h2>The selected emails have been marked as spam.</h2>
<section>This is a longer alert description that demonstrates how the action stays aligned to the inline end.</section>
<footer>
<span class="badge-secondary">Badge</span>
<span class="badge">Badge</span>
</footer>
</div>
</div>
@@ -130,7 +130,7 @@ Add `dir="rtl"` to the alert or an ancestor.
<h2>تم حفظ التغييرات</h2>
<section>تم تحديث إعداداتك بنجاح.</section>
<footer>
<button type="button" class="btn-xs-outline">عرض</button>
<button type="button" class="btn" data-variant="outline" data-size="xs">عرض</button>
</footer>
</div>
</Preview>
+11 -11
View File
@@ -31,7 +31,7 @@
## Usage
Use `.avatar`, `.avatar-sm`, or `.avatar-lg` on a small inline container. Put the image first and a fallback `<span>` after it.
Add `class="avatar"` to a small inline container. Add `data-size="sm"` or `data-size="lg"` when needed. Put the image first and a fallback `<span>` after it.
```html
<span class="avatar">
@@ -43,23 +43,23 @@ Use `.avatar`, `.avatar-sm`, or `.avatar-lg` on a small inline container. Put th
### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;div class="avatar-group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;div class="avatar-group"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>
Optional parent that groups avatars with overlap.
<dl>
<dt><code class="highlight language-html">&lt;span class="avatar|avatar-sm|avatar-lg"&gt;</code></dt>
<dt><code>&lt;span class="avatar"&gt;</code></dt>
<dd>
Avatar root. Use <code>.avatar-sm</code>, <code>.avatar</code>, or <code>.avatar-lg</code>. Use it standalone or as a direct child of <code>.avatar-group</code>.
Avatar root. Use it standalone or as a direct child of <code>.avatar-group</code>. Add <code>data-size="sm"</code> or <code>data-size="lg"</code> for alternate sizes.
<dl>
<dt><code class="highlight language-html">&lt;img&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;img&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The avatar image. Provide meaningful <code>alt</code> text.</dd>
<dt><code class="highlight language-html">&lt;span&gt;</code></dt>
<dt><code>&lt;span&gt;</code></dt>
<dd>Fallback initials or icon.</dd>
<dt><code class="highlight language-html">&lt;span data-badge&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;span data-badge&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Status badge positioned at the block-end inline-end corner.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;span data-count&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;span data-count&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Count or icon indicator displayed at the end of the group.</dd>
</dl>
</dd>
@@ -136,7 +136,7 @@ Use `.avatar`, `.avatar-sm`, or `.avatar-lg` on a small inline container. Put th
<Preview>
<div class="flex flex-wrap items-center gap-2 grayscale">
<span class="avatar-sm">
<span class="avatar" data-size="sm">
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
</span>
@@ -144,7 +144,7 @@ Use `.avatar`, `.avatar-sm`, or `.avatar-lg` on a small inline container. Put th
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
</span>
<span class="avatar-lg">
<span class="avatar" data-size="lg">
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
</span>
@@ -155,7 +155,7 @@ Use `.avatar`, `.avatar-sm`, or `.avatar-lg` on a small inline container. Put th
<Preview>
<div class="dropdown-menu">
<button type="button" id="avatar-dropdown-trigger" aria-haspopup="menu" aria-controls="avatar-dropdown-menu" aria-expanded="false" class="btn-icon-ghost rounded-full">
<button type="button" id="avatar-dropdown-trigger" aria-label="Open user menu" aria-haspopup="menu" aria-controls="avatar-dropdown-menu" aria-expanded="false" class="btn rounded-full" data-variant="ghost" data-size="icon">
<span class="avatar">
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
+20 -20
View File
@@ -4,36 +4,36 @@
<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>
<span class="badge" data-variant="secondary">Secondary</span>
<span class="badge" data-variant="destructive">Destructive</span>
<span class="badge" data-variant="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">
<span class="badge bg-blue-500 text-white dark:bg-blue-600">
<svg class="lucide lucide-badge-check" 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" ><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z" /><path d="m9 12 2 2 4-4" /></svg>
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>
<span class="badge rounded-full h-5 min-w-5 px-1 font-mono tabular-nums" data-variant="destructive">99</span>
<span class="badge rounded-full h-5 min-w-5 px-1 font-mono tabular-nums" data-variant="outline">20+</span>
</div>
</div>
</Preview>
## Usage
Use one of the badge classes: `badge`, `badge-primary`, `badge-secondary`, `badge-destructive`, `badge-outline`, `badge-ghost`, or `badge-link`.
Add `class="badge"` to an inline element. Add `data-variant` when the badge is not the default primary variant.
When an icon needs directional spacing, add `data-icon="inline-start"` or `data-icon="inline-end"` to the icon.
Inline SVG icons are spaced automatically by the badge gap.
```html
<span class="badge">Badge</span>
<span class="badge-secondary">Secondary</span>
<span class="badge-destructive">Destructive</span>
<span class="badge-outline">Outline</span>
<span class="badge" data-variant="secondary">Secondary</span>
<span class="badge" data-variant="destructive">Destructive</span>
<span class="badge" data-variant="outline">Outline</span>
```
Badges support right-to-left layouts through native text flow and logical icon spacing.
Badges support right-to-left layouts through native text flow. Flip directional icons explicitly.
## Examples
@@ -46,25 +46,25 @@ Badges support right-to-left layouts through native text flow and logical icon s
### Secondary
<Preview>
<span class="badge-secondary">Secondary</span>
<span class="badge" data-variant="secondary">Secondary</span>
</Preview>
### Destructive
<Preview>
<span class="badge-destructive">Destructive</span>
<span class="badge" data-variant="destructive">Destructive</span>
</Preview>
### Outline
<Preview>
<span class="badge-outline">Outline</span>
<span class="badge" data-variant="outline">Outline</span>
</Preview>
### With icon
<Preview>
<span class="badge-destructive">
<span class="badge" data-variant="destructive">
<svg class="lucide lucide-circle-alert" 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" ><circle cx="12" cy="12" r="10" /><line x1="12" x2="12" y1="8" y2="12" /><line x1="12" x2="12.01" y1="16" y2="16" /></svg>
With icon
</span>
@@ -73,9 +73,9 @@ Badges support right-to-left layouts through native text flow and logical icon s
### Link
<Preview>
<a href="#" class="badge-outline">
<a href="#" class="badge" data-variant="outline">
Link
<svg data-icon="inline-end" class="rtl:rotate-180 lucide lucide-arrow-right" 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" ><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>
<svg class="rtl:rotate-180 lucide lucide-arrow-right" 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" ><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>
</a>
</Preview>
@@ -94,9 +94,9 @@ Flip directional icons explicitly.
<Preview>
<div dir="rtl" class="flex flex-wrap gap-2">
<span class="badge">نشط</span>
<a href="#" class="badge-outline">
<a href="#" class="badge" data-variant="outline">
التفاصيل
<svg data-icon="inline-end" class="rtl:rotate-180 lucide lucide-arrow-right" 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" ><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>
<svg class="rtl:rotate-180 lucide lucide-arrow-right" 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" ><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>
</a>
</div>
</Preview>
+41 -41
View File
@@ -2,17 +2,17 @@
<Preview>
<div class="flex w-fit items-stretch gap-2">
<button type="button" class="btn-icon-outline" aria-label="Go Back">
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Go Back">
<svg class="lucide lucide-arrow-left" 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"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg>
</button>
<div role="group" class="button-group">
<button type="button" class="btn-outline">Archive</button>
<button type="button" class="btn-outline">Report</button>
<button type="button" class="btn" data-variant="outline">Archive</button>
<button type="button" class="btn" data-variant="outline">Report</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn-outline">Snooze</button>
<button type="button" class="btn" data-variant="outline">Snooze</button>
<div id="dropdown-menu-277805" class="dropdown-menu">
<button type="button" id="dropdown-menu-277805-trigger" aria-haspopup="menu" aria-controls="dropdown-menu-277805-menu" aria-expanded="false" class="btn-icon-outline">
<button type="button" id="dropdown-menu-277805-trigger" aria-label="More archive options" aria-haspopup="menu" aria-controls="dropdown-menu-277805-menu" aria-expanded="false" class="btn" data-variant="outline" data-size="icon">
<svg class="lucide lucide-ellipsis" 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"><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></svg>
</button>
<div id="dropdown-menu-277805-popover" data-popover aria-hidden="true" data-align="end">
@@ -56,8 +56,8 @@ Wrap related controls in a `button-group`.
```html
<div role="group" aria-label="Message actions" class="button-group">
<button type="button" class="btn-outline">Archive</button>
<button type="button" class="btn-outline">Report</button>
<button type="button" class="btn" data-variant="outline">Archive</button>
<button type="button" class="btn" data-variant="outline">Report</button>
</div>
```
@@ -71,10 +71,10 @@ Use `data-orientation="vertical"` to stack buttons vertically.
<Preview>
<div role="group" class="button-group" data-orientation="vertical">
<button type="button" class="btn-icon-outline" aria-label="Zoom in">
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Zoom in">
<svg class="lucide lucide-plus" 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"><path d="M5 12h14" /><path d="M12 5v14" /></svg>
</button>
<button type="button" class="btn-icon-outline" aria-label="Zoom out">
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Zoom out">
<svg class="lucide lucide-minus" 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"><path d="M5 12h14" /></svg>
</button>
</div>
@@ -85,26 +85,26 @@ Use `data-orientation="vertical"` to stack buttons vertically.
<Preview>
<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">
<button type="button" class="btn" data-variant="outline" data-size="sm">Small</button>
<button type="button" class="btn" data-variant="outline" data-size="sm">Button</button>
<button type="button" class="btn" data-variant="outline" data-size="sm">Group</button>
<button type="button" class="btn" data-variant="outline" data-size="icon-sm" aria-label="Add small item">
<svg class="lucide lucide-plus" 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"><path d="M5 12h14" /><path d="M12 5v14" /></svg>
</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">
<button type="button" class="btn" data-variant="outline">Default</button>
<button type="button" class="btn" data-variant="outline">Button</button>
<button type="button" class="btn" data-variant="outline">Group</button>
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Add item">
<svg class="lucide lucide-plus" 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"><path d="M5 12h14" /><path d="M12 5v14" /></svg>
</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn-lg-outline">Large</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">
<button type="button" class="btn" data-variant="outline" data-size="lg">Large</button>
<button type="button" class="btn" data-variant="outline" data-size="lg">Button</button>
<button type="button" class="btn" data-variant="outline" data-size="lg">Group</button>
<button type="button" class="btn" data-variant="outline" data-size="icon-lg" aria-label="Add large item">
<svg class="lucide lucide-plus" 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"><path d="M5 12h14" /><path d="M12 5v14" /></svg>
</button>
</div>
@@ -116,17 +116,17 @@ Use `data-orientation="vertical"` to stack buttons vertically.
<Preview>
<div role="group" aria-label="Pagination controls" class="button-group">
<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>
<button type="button" class="btn" data-variant="outline" data-size="sm">1</button>
<button type="button" class="btn" data-variant="outline" data-size="sm">2</button>
<button type="button" class="btn" data-variant="outline" data-size="sm">3</button>
<button type="button" class="btn" data-variant="outline" data-size="sm">4</button>
<button type="button" class="btn" data-variant="outline" data-size="sm">5</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn-sm-icon-outline" aria-label="Previous page">
<button type="button" class="btn" data-variant="outline" data-size="icon-sm" aria-label="Previous page">
<svg class="lucide lucide-chevron-left" 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"><path d="m15 18-6-6 6-6" /></svg>
</button>
<button type="button" class="btn-sm-icon-outline" aria-label="Next page">
<button type="button" class="btn" data-variant="outline" data-size="icon-sm" aria-label="Next page">
<svg class="lucide lucide-chevron-right" 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"><path d="m9 18 6-6-6-6" /></svg>
</button>
</div>
@@ -139,9 +139,9 @@ Use `<hr role="separator" />` to visually divide buttons within a group.
<Preview>
<div role="group" class="button-group">
<button type="button" class="btn-secondary">Copy</button>
<button type="button" class="btn" data-variant="secondary">Copy</button>
<hr role="separator" />
<button type="button" class="btn-secondary">Paste</button>
<button type="button" class="btn" data-variant="secondary">Paste</button>
</div>
</Preview>
@@ -151,11 +151,11 @@ Use a separator to create a split action with a primary action and a secondary m
<Preview>
<div role="group" aria-label="Split save actions" class="button-group">
<button type="button" class="btn-secondary">Save</button>
<button type="button" class="btn" data-variant="secondary">Save</button>
<hr role="separator" />
<div id="dropdown-menu-465061" class="dropdown-menu">
<button type="button" id="dropdown-menu-465061-trigger" aria-haspopup="menu" aria-controls="dropdown-menu-465061-menu" aria-expanded="false" class="btn-icon-secondary" aria-label="More save options">
<button type="button" id="dropdown-menu-465061-trigger" aria-haspopup="menu" aria-controls="dropdown-menu-465061-menu" aria-expanded="false" class="btn" data-variant="secondary" data-size="icon" aria-label="More save options">
<svg class="lucide lucide-chevron-down" 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"><path d="m6 9 6 6 6-6" /></svg>
</button>
<div id="dropdown-menu-465061-popover" data-popover aria-hidden="true" data-align="end">
@@ -174,7 +174,7 @@ Use a separator to create a split action with a primary action and a secondary m
<Preview>
<div role="group" aria-label="Search" class="button-group w-full max-w-sm">
<input type="text" class="input" placeholder="Search..." />
<button type="button" class="btn-icon-outline" aria-label="Search">
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Search">
<svg class="lucide lucide-search" 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"><path d="m21 21-4.34-4.34" /><circle cx="11" cy="11" r="8" /></svg>
</button>
</div>
@@ -194,10 +194,10 @@ Use a separator to create a split action with a primary action and a secondary m
<Preview>
<div role="group" class="button-group">
<button type="button" class="btn-outline">Follow</button>
<button type="button" class="btn" data-variant="outline">Follow</button>
<div id="dropdown-menu-609880" class="dropdown-menu">
<button type="button" id="dropdown-menu-609880-trigger" aria-haspopup="menu" aria-controls="dropdown-menu-609880-menu" aria-expanded="false" class="btn-icon-outline">
<button type="button" id="dropdown-menu-609880-trigger" aria-label="More follow options" aria-haspopup="menu" aria-controls="dropdown-menu-609880-menu" aria-expanded="false" class="btn" data-variant="outline" data-size="icon">
<svg class="lucide lucide-chevron-down" 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"><path d="m6 9 6 6 6-6" /></svg>
</button>
<div id="dropdown-menu-609880-popover" data-popover aria-hidden="true" data-align="end">
@@ -267,7 +267,7 @@ Use a separator to create a split action with a primary action and a secondary m
</div>
<input type="text" class="input" placeholder="10.00" />
</div>
<button type="button" class="btn-icon-outline">
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Submit amount">
<svg class="lucide lucide-arrow-right" 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"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>
</button>
</div>
@@ -277,12 +277,12 @@ Use a separator to create a split action with a primary action and a secondary m
<Preview>
<div role="group" class="button-group">
<button type="button" class="btn-outline">
<button type="button" class="btn" data-variant="outline">
<svg class="lucide lucide-bot" 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"><path d="M12 8V4H8" /><rect width="16" height="12" x="4" y="8" rx="2" /><path d="M2 14h2" /><path d="M20 14h2" /><path d="M15 13v2" /><path d="M9 13v2" /></svg>
Copilot
</button>
<div id="popover-788465" class="popover">
<button id="popover-788465-trigger" type="button" aria-expanded="false" aria-controls="popover-788465-popover" class="btn-icon-outline">
<button id="popover-788465-trigger" type="button" aria-label="Open copilot tasks" aria-expanded="false" aria-controls="popover-788465-popover" class="btn" data-variant="outline" data-size="icon">
<svg class="lucide lucide-chevron-down" 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"><path d="m6 9 6 6 6-6" /></svg>
</button>
<div id="popover-788465-popover" data-popover aria-hidden="true" class="w-72 p-0" data-align="end">
@@ -304,9 +304,9 @@ Add `dir="rtl"` to the group or an ancestor. Flip directional icons explicitly.
<Preview>
<div dir="rtl">
<div role="group" aria-label="إجراءات الرسالة" class="button-group">
<button type="button" class="btn-outline">أرشفة</button>
<button type="button" class="btn-outline">تقرير</button>
<button type="button" class="btn-outline">
<button type="button" class="btn" data-variant="outline">أرشفة</button>
<button type="button" class="btn" data-variant="outline">تقرير</button>
<button type="button" class="btn" data-variant="outline">
إرسال
<svg class="rtl:rotate-180 lucide lucide-arrow-right" 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"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>
</button>
+54 -54
View File
@@ -8,53 +8,53 @@
```html
<button type="button" class="btn">Primary</button>
<button type="button" class="btn-outline">Outline</button>
<button type="button" class="btn-sm-secondary">Small secondary</button>
<button type="button" class="btn-icon-outline" aria-label="Settings">
<button type="button" class="btn" data-variant="outline">Outline</button>
<button type="button" class="btn" data-variant="secondary" data-size="sm">Small secondary</button>
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Settings">
<svg class="lucide lucide-settings" 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" ><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" /></svg>
</button>
```
Use exactly one `btn` class on a `<button>` or `<a>`. The class name combines size, icon shape, and variant:
Add `class="btn"` to a `<button>` or `<a>`. Add `data-variant` and `data-size` when needed. Add `data-icon="inline-start"` or `data-icon="inline-end"` to inline icons when a text button needs icon-aware spacing.
| Pattern | Example | Use for |
| --- | --- | --- |
| `btn` | `btn` | Default-size primary button |
| `btn-{variant}` | `btn-outline` | Default-size button with a visual variant |
| `btn-{size}` | `btn-sm` | Sized primary button |
| `btn-{size}-{variant}` | `btn-sm-outline` | Sized button with a visual variant |
| `btn-icon` | `btn-icon` | Default-size icon-only primary button |
| `btn-icon-{variant}` | `btn-icon-outline` | Default-size icon-only button with a visual variant |
| `btn-{size}-icon` | `btn-sm-icon` | Sized icon-only primary button |
| `btn-{size}-icon-{variant}` | `btn-sm-icon-outline` | Sized icon-only button with a visual variant |
#### HTML structure
Do not stack button classes like `btn-sm btn-outline`. Use the composed class, such as `btn-sm-outline`.
<dl>
<dt><code>&lt;button class="btn"&gt;</code></dt>
<dd>Button root. Use a native <code>&lt;button&gt;</code> for actions and <code>&lt;a&gt;</code> for navigation.</dd>
<dt><code>data-variant="primary|secondary|outline|ghost|link|destructive"</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Visual variant. Omit it for the primary variant.</dd>
<dt><code>data-size="xs|sm|default|lg|icon|icon-xs|icon-sm|icon-lg"</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Button size. Omit it for the default size.</dd>
</dl>
### Variants
| Variant | Classes |
| Variant | Markup |
| --- | --- |
| Primary | `btn`, `btn-primary` |
| Secondary | `btn-secondary` |
| Outline | `btn-outline` |
| Ghost | `btn-ghost` |
| Link | `btn-link` |
| Destructive | `btn-destructive` |
Every variant can be combined with a size or icon shape. For example: `btn-xs-secondary`, `btn-lg-destructive`, `btn-icon-ghost`, `btn-sm-icon-outline`.
| Primary | `<button class="btn">` |
| Secondary | `<button class="btn" data-variant="secondary">` |
| Outline | `<button class="btn" data-variant="outline">` |
| Ghost | `<button class="btn" data-variant="ghost">` |
| Link | `<button class="btn" data-variant="link">` |
| Destructive | `<button class="btn" data-variant="destructive">` |
### Sizes
| Size | Text button | Icon-only button |
| --- | --- | --- |
| Extra small | `btn-xs` | `btn-xs-icon` |
| Small | `btn-sm` | `btn-sm-icon` |
| Default | `btn` | `btn-icon` |
| Large | `btn-lg` | `btn-lg-icon` |
| Size | Markup |
| --- | --- |
| Extra small | `<button class="btn" data-size="xs">` |
| Small | `<button class="btn" data-size="sm">` |
| Default | `<button class="btn">` |
| Large | `<button class="btn" data-size="lg">` |
| Icon extra small | `<button class="btn" data-size="icon-xs">` |
| Icon small | `<button class="btn" data-size="icon-sm">` |
| Icon | `<button class="btn" data-size="icon">` |
| Icon large | `<button class="btn" data-size="icon-lg">` |
Use icon-only classes only when the button has no visible text. Icon-only buttons must have an accessible name, usually with `aria-label`.
Use `data-size="icon"` or one of the icon size variants for square buttons. Icon-only buttons need an accessible name, usually with `aria-label`.
Mark inline icons with `data-icon="inline-start"` or `data-icon="inline-end"` when a text button should use icon-aware spacing.
Use `data-icon="inline-start"` or `data-icon="inline-end"` on inline icons when a text button needs icon-aware spacing. Use icon size variants only for icon-only buttons.
To group related buttons, use the [Button Group](/components/button-group/) component.
@@ -62,14 +62,14 @@ To group related buttons, use the [Button Group](/components/button-group/) comp
### Size
Use size-specific classes to change the size of the button.
Use `data-size` to change the size of the button.
<Preview>
<div class="flex flex-wrap items-center gap-2">
<button type="button" class="btn-xs">Extra Small</button>
<button type="button" class="btn-sm">Small</button>
<button type="button" class="btn" data-size="xs">Extra Small</button>
<button type="button" class="btn" data-size="sm">Small</button>
<button type="button" class="btn">Default</button>
<button type="button" class="btn-lg">Large</button>
<button type="button" class="btn" data-size="lg">Large</button>
</div>
</Preview>
@@ -82,44 +82,44 @@ Use size-specific classes to change the size of the button.
### Outline
<Preview>
<button type="button" class="btn-outline">Outline</button>
<button type="button" class="btn" data-variant="outline">Outline</button>
</Preview>
### Secondary
<Preview>
<button type="button" class="btn-secondary">Secondary</button>
<button type="button" class="btn" data-variant="secondary">Secondary</button>
</Preview>
### Ghost
<Preview>
<button type="button" class="btn-ghost">Ghost</button>
<button type="button" class="btn" data-variant="ghost">Ghost</button>
</Preview>
### Destructive
<Preview>
<button type="button" class="btn-destructive">Destructive</button>
<button type="button" class="btn" data-variant="destructive">Destructive</button>
</Preview>
### Link
<Preview>
<button type="button" class="btn-link">Link</button>
<button type="button" class="btn" data-variant="link">Link</button>
</Preview>
### Icon
<Preview>
<button type="button" class="btn-icon-outline" aria-label="Open">
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Open">
<svg class="lucide lucide-chevron-right" 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" ><path d="m9 18 6-6-6-6" /></svg>
</button>
</Preview>
### With Icon
Mark inline icons with `data-icon="inline-start"` or `data-icon="inline-end"` when the button should use icon-aware spacing.
Use `data-icon="inline-start"` or `data-icon="inline-end"` on inline icons to match upstream icon-aware spacing.
<Preview>
<button type="button" class="btn">
@@ -144,12 +144,12 @@ Use standard Tailwind classes to override the button radius.
<Preview>
<div class="flex flex-wrap items-center gap-2">
<button type="button" class="btn" disabled>
<svg class="animate-spin lucide lucide-loader" 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" ><path d="M12 2v4" /><path d="m16.2 7.8 2.9-2.9" /><path d="M18 12h4" /><path d="m16.2 16.2 2.9 2.9" /><path d="M12 18v4" /><path d="m4.9 19.1 2.9-2.9" /><path d="M2 12h4" /><path d="m4.9 4.9 2.9 2.9" /></svg>
<svg data-icon="inline-start" class="animate-spin lucide lucide-loader" 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" ><path d="M12 2v4" /><path d="m16.2 7.8 2.9-2.9" /><path d="M18 12h4" /><path d="m16.2 16.2 2.9 2.9" /><path d="M12 18v4" /><path d="m4.9 19.1 2.9-2.9" /><path d="M2 12h4" /><path d="m4.9 4.9 2.9 2.9" /></svg>
Generating
</button>
<button type="button" class="btn-outline">
<button type="button" class="btn" data-variant="outline">
Downloading
<svg class="animate-spin lucide lucide-loader" 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" ><path d="M12 2v4" /><path d="m16.2 7.8 2.9-2.9" /><path d="M18 12h4" /><path d="m16.2 16.2 2.9 2.9" /><path d="M12 18v4" /><path d="m4.9 19.1 2.9-2.9" /><path d="M2 12h4" /><path d="m4.9 4.9 2.9 2.9" /></svg>
<svg data-icon="inline-end" class="animate-spin lucide lucide-loader" 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" ><path d="M12 2v4" /><path d="m16.2 7.8 2.9-2.9" /><path d="M18 12h4" /><path d="m16.2 16.2 2.9 2.9" /><path d="M12 18v4" /><path d="m4.9 19.1 2.9-2.9" /><path d="M2 12h4" /><path d="m4.9 4.9 2.9 2.9" /></svg>
</button>
</div>
</Preview>
@@ -161,12 +161,12 @@ To group related buttons, use the [Button Group](/components/button-group/) comp
<Preview>
<div class="flex w-fit items-stretch gap-2">
<div role="group" class="button-group">
<button type="button" class="btn-outline">Archive</button>
<button type="button" class="btn-outline">Report</button>
<button type="button" class="btn" data-variant="outline">Archive</button>
<button type="button" class="btn" data-variant="outline">Report</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn-outline">Snooze</button>
<button type="button" class="btn-icon-outline" aria-label="More options">
<button type="button" class="btn" data-variant="outline">Snooze</button>
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="More options">
<svg class="lucide lucide-ellipsis" 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" ><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></svg>
</button>
</div>
@@ -179,13 +179,13 @@ Add `dir="rtl"` to the button or an ancestor. Flip directional icons explicitly.
<Preview>
<div dir="rtl" class="flex flex-wrap items-center gap-2">
<button type="button" class="btn-outline">زر</button>
<button type="button" class="btn-destructive">حذف</button>
<button type="button" class="btn-outline">
<button type="button" class="btn" data-variant="outline">زر</button>
<button type="button" class="btn" data-variant="destructive">حذف</button>
<button type="button" class="btn" data-variant="outline">
إرسال
<svg data-icon="inline-end" class="rtl:rotate-180 lucide lucide-arrow-right" 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" ><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>
</button>
<button type="button" class="btn-secondary" disabled>
<button type="button" class="btn" data-variant="secondary" disabled>
<svg class="animate-spin lucide lucide-loader" 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" ><path d="M12 2v4" /><path d="m16.2 7.8 2.9-2.9" /><path d="M18 12h4" /><path d="m16.2 16.2 2.9 2.9" /><path d="M12 18v4" /><path d="m4.9 19.1 2.9-2.9" /><path d="M2 12h4" /><path d="m4.9 4.9 2.9 2.9" /></svg>
جاري التحميل
</button>
+13 -13
View File
@@ -23,7 +23,7 @@
</section>
<footer class="flex flex-col items-center gap-2">
<button type="button" class="btn w-full">Login</button>
<button type="button" class="btn-outline w-full">Login with Google</button>
<button type="button" class="btn w-full" data-variant="outline">Login with Google</button>
<p class="mt-4 text-center text-sm">Don't have an account? <a href="#" class="underline-offset-4 hover:underline">Sign up</a></p>
</footer>
</div>
@@ -49,23 +49,23 @@
### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;div class="card|card-sm"&gt;</code></dt>
<dt><code>&lt;div class="card"&gt;</code></dt>
<dd>
The card container. Use <code>.card-sm</code> for a denser layout.
The card container. Use <code>data-size="sm"</code> for a denser layout.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dt><code>&lt;header&gt;</code></dt>
<dd>
The header of the card.
<dl>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dt><code>&lt;h2&gt;</code></dt>
<dd>The title of the card.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;p&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The description of the card.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code></dt>
<dt><code>&lt;section&gt;</code></dt>
<dd>The content of the card.</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;footer&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The footer of the card.</dd>
</dl>
</dd>
@@ -76,13 +76,13 @@
### Size
<Preview>
<div class="card-sm">
<div class="card" data-size="sm">
<header>
<h2>Small Card</h2>
<p>A compact card with reduced spacing.</p>
</header>
<section>
<p class="text-sm">Use <code>.card-sm</code> for denser layouts.</p>
<p class="text-sm">Use <code>data-size="sm"</code> for denser layouts.</p>
</section>
</div>
</Preview>
@@ -138,15 +138,15 @@
/>
</section>
<footer class="flex items-center gap-2">
<span class="badge-outline">
<span class="badge" data-variant="outline">
<svg class="lucide lucide-bed" 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" ><path d="M2 4v16" /><path d="M2 8h18a2 2 0 0 1 2 2v10" /><path d="M2 17h20" /><path d="M6 8v9" /></svg>
1
</span>
<span class="badge-outline">
<span class="badge" data-variant="outline">
<svg class="lucide lucide-bath" 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" ><path d="M10 4 8 6" /><path d="M17 19v2" /><path d="M2 12h20" /><path d="M7 19v2" /><path d="M9 5 7.621 3.621A2.121 2.121 0 0 0 4 5v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5" /></svg>
2
</span>
<span class="badge-outline">
<span class="badge" data-variant="outline">
<svg class="lucide lucide-land-plot" 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" ><path d="m12 8 6-3-6-3v10" /><path d="m8 11.99-5.5 3.14a1 1 0 0 0 0 1.74l8.5 4.86a2 2 0 0 0 2 0l8.5-4.86a1 1 0 0 0 0-1.74L16 12" /><path d="m6.49 12.85 11.02 6.3" /><path d="M17.51 12.85 6.5 19.15" /></svg>
350m²
</span>
+44 -6
View File
@@ -1,5 +1,4 @@
import ChartExample from "../site/ChartExample.astro"
import ChartExamples from "../site/ChartExamples.astro"
import chartExamplesUrl from "../site/chart-examples.js?url"
# Chart
@@ -9,14 +8,16 @@ import ChartExamples from "../site/ChartExamples.astro"
Use Chart.js with Basecoat defaults for themed colors, external tooltips, and optional generated legends.
<ChartExample />
<Preview class="w-full">
<canvas id="chart-example-overview" aria-label="Visitors by device"></canvas>
</Preview>
Chart is an optional JavaScript helper. Load Chart.js first, then `basecoat.js`, then `chart.js`.
```html
<script src="https://cdn.jsdelivr.net/npm/chart.js/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/chart.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/chart.min.js" defer></script>
```
## Usage
@@ -148,7 +149,44 @@ window.basecoat.chart("#custom-chart", {
## Examples
<ChartExamples />
### Bar Chart - Multiple
<Preview class="w-full">
<canvas id="chart-example-visitors" aria-label="Monthly visitors by device"></canvas>
</Preview>
### Line Chart - Linear
<Preview class="w-full">
<canvas id="chart-example-line" aria-label="Revenue line chart"></canvas>
</Preview>
### Line Chart - Step
<Preview class="w-full">
<canvas id="chart-example-step" aria-label="Support queue step chart"></canvas>
</Preview>
### Bar Chart - Stacked
<Preview class="w-full">
<canvas id="chart-example-stacked" aria-label="Stacked pipeline bar chart"></canvas>
</Preview>
### Pie Chart - Donut
<Preview class="w-full">
<canvas id="chart-example-donut" aria-label="Traffic source donut chart"></canvas>
</Preview>
### Radar Chart
<Preview class="w-full">
<canvas id="chart-example-radar" aria-label="Channel performance radar chart"></canvas>
</Preview>
<script is:inline src="https://cdn.jsdelivr.net/npm/chart.js/dist/chart.umd.min.js"></script>
<script type="module" src={chartExamplesUrl}></script>
## API
+3 -3
View File
@@ -9,7 +9,7 @@
## Usage
Add `input` to checkbox inputs. Pair it with [Field](/components/field) and a native `<label>` for layout and labeling.
Add `class="input"` to checkbox inputs. Pair it with [Field](/components/field) and a native `<label>` for layout and labeling.
```html
<input type="checkbox" class="input" />
@@ -33,7 +33,7 @@ Add `input` to checkbox inputs. Pair it with [Field](/components/field) and a na
<input type="checkbox" id="checkbox-description" class="input" aria-describedby="checkbox-description-text" />
<section>
<label for="checkbox-description">Accept terms and conditions</label>
<p id="checkbox-description-text" class="field-description">By clicking this checkbox, you agree to the terms and conditions.</p>
<p id="checkbox-description-text">By clicking this checkbox, you agree to the terms and conditions.</p>
</section>
</div>
</Preview>
@@ -120,7 +120,7 @@ Checkbox layout uses document direction and logical spacing. Set `dir="rtl"` on
<input type="checkbox" id="checkbox-rtl" class="input" aria-describedby="checkbox-rtl-description" checked />
<section>
<label for="checkbox-rtl">قبول الشروط والأحكام</label>
<p id="checkbox-rtl-description" class="field-description">يمكنك تغيير هذا الخيار في أي وقت.</p>
<p id="checkbox-rtl-description">يمكنك تغيير هذا الخيار في أي وقت.</p>
</section>
</div>
</Preview>
+14 -14
View File
@@ -27,8 +27,8 @@
<Step title="Include the JavaScript files">
```html
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/combobox.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/combobox.min.js" defer></script>
```
</Step>
@@ -59,41 +59,41 @@ Combobox is input-first. The visible input filters the list; the hidden input st
### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;div class="combobox"&gt;</code></dt>
<dt><code>&lt;div class="combobox"&gt;</code></dt>
<dd>
Root element. Add <code>data-auto-highlight="true"</code> to highlight the first match while filtering. Add <code>data-format="object"</code> to serialize selected values as <code>&#123; value, label &#125;</code> objects in the hidden input. Add <code>data-filter="manual"</code> when your app owns filtering, such as remote autocomplete or a local Lunr index.
<dl>
<dt><code class="highlight language-html">&lt;input type="text" role="combobox"&gt;</code></dt>
<dt><code>&lt;input type="text" role="combobox"&gt;</code></dt>
<dd>The editable control. It should include <code>aria-expanded</code>, <code>aria-controls</code>, and <code>aria-autocomplete="list"</code>.</dd>
<dt><code class="highlight language-html">&lt;div class="combobox-chips"&gt;</code> <span class="badge-secondary">Multiple only</span></dt>
<dt><code>&lt;div class="combobox-chips"&gt;</code> <span class="badge" data-variant="secondary">Multiple only</span></dt>
<dd>
Wraps selected chips and the editable input in multiple mode.
<dl>
<dt><code class="highlight language-html">&lt;input type="text" role="combobox"&gt;</code></dt>
<dt><code>&lt;input type="text" role="combobox"&gt;</code></dt>
<dd>The editable control in multiple mode.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;div data-popover&gt;</code></dt>
<dt><code>&lt;div data-popover&gt;</code></dt>
<dd>
The suggestions popup. Use <code>data-side</code> and <code>data-align</code> like Popover.
<dl>
<dt><code class="highlight language-html">&lt;div role="listbox"&gt;</code></dt>
<dt><code>&lt;div role="listbox"&gt;</code></dt>
<dd>
The suggestions list. Add <code>aria-multiselectable="true"</code> in multiple mode and <code>data-empty</code> for the empty message.
<dl>
<dt><code class="highlight language-html">&lt;div role="option" data-value="..."&gt;</code></dt>
<dt><code>&lt;div role="option" data-value="..."&gt;</code></dt>
<dd>Selectable option. Options can contain custom HTML; use <code>data-label</code> when the visible input should use different text than the rendered content.</dd>
<dt><code class="highlight language-html">&lt;div role="group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;div role="group"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Option group. Use <code>aria-labelledby</code> to connect it to its heading.</dd>
<dt><code class="highlight language-html">&lt;div role="heading"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;div role="heading"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Group heading.</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;hr role="separator"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Separator between groups or options.</dd>
</dl>
</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;input type="hidden"&gt;</code></dt>
<dt><code>&lt;input type="hidden"&gt;</code></dt>
<dd>Submitted value. Single mode stores a string; multiple mode stores a JSON array. With <code>data-format="object"</code>, single mode stores a JSON object and multiple mode stores a JSON array of objects.</dd>
</dl>
</dd>
@@ -207,7 +207,7 @@ Combobox is input-first. The visible input filters the list; the hidden input st
</div>
<input type="hidden" value="" />
</div>
<p class="field-error" role="alert">Select a framework to continue.</p>
<p role="alert">Select a framework to continue.</p>
</div>
</Preview>
+15 -15
View File
@@ -56,8 +56,8 @@ shadcn/ui's Command component is built on `cmdk`. Basecoat implements the same c
<Step title="Include the JavaScript files">
```html
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/command.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/command.min.js" defer></script>
```
</Step>
@@ -108,7 +108,7 @@ shadcn/ui's Command component is built on `cmdk`. Basecoat implements the same c
Use `<dialog class="command-dialog">` for the command palette variant.
```html
<button class="btn-outline" onclick="document.getElementById('command-palette').showModal()">Open</button>
<button class="btn" data-variant="outline" onclick="document.getElementById('command-palette').showModal()">Open</button>
<dialog id="command-palette" class="command-dialog" aria-label="Command menu">
<div class="command">
<!-- Command content -->
@@ -124,37 +124,37 @@ Use `<dialog class="command-dialog">` for the command palette variant.
The command menu can be used standalone or inside a native dialog. The structure is:
<dl>
<dt><code class="highlight language-html">&lt;dialog class="command-dialog"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;dialog class="command-dialog"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>
Dialog variant. Wraps one <code>&lt;div class="command"&gt;</code>. Add <code>aria-label</code> or <code>aria-labelledby</code> for an accessible name.
<dl>
<dt><code class="highlight language-html">&lt;div class="command"&gt;</code></dt>
<dt><code>&lt;div class="command"&gt;</code></dt>
<dd>
Root command menu. Can also be used standalone without the dialog wrapper. Add <code>data-filter="manual"</code> when your app owns filtering, such as remote search or a local Lunr index.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dt><code>&lt;header&gt;</code></dt>
<dd>
Search input wrapper. The macro includes a search icon and input.
<dl>
<dt><code class="highlight language-html">&lt;input type="text" role="combobox"&gt;</code></dt>
<dt><code>&lt;input type="text" role="combobox"&gt;</code></dt>
<dd>Filter input. Use <code>aria-expanded="true"</code> and <code>aria-controls="&#123; MENU_ID &#125;"</code> to point to the command list.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;div role="menu" id="&#123; MENU_ID &#125;"&gt;</code></dt>
<dt><code>&lt;div role="menu" id="&#123; MENU_ID &#125;"&gt;</code></dt>
<dd>
Command list. Set <code>data-empty</code> to customize the empty state.
<dl>
<dt><code class="highlight language-html">&lt;div role="group" aria-labelledby="&#123; HEADING_ID &#125;"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;div role="group" aria-labelledby="&#123; HEADING_ID &#125;"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Groups related command items. Hidden automatically when every item in the group is filtered out.</dd>
<dt><code class="highlight language-html">&lt;span role="heading" id="&#123; HEADING_ID &#125;"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;span role="heading" id="&#123; HEADING_ID &#125;"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Group heading. Use an <code>id</code> when the parent group uses <code>aria-labelledby</code>.</dd>
<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>
<dt><code>&lt;div role="menuitem"&gt;</code> or <code>&lt;a role="menuitem"&gt;</code></dt>
<dd>Selectable command item. Use <code>&lt;a&gt;</code> for navigation and <code>&lt;div&gt;</code> for actions. Supports <code>data-filter</code>, <code>data-keywords</code>, <code>data-force</code>, <code>data-keep-command-open</code>, and <code>aria-disabled="true"</code>.</dd>
<dt><code class="highlight language-html">&lt;span data-shortcut&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;span data-shortcut&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Shortcut hint aligned to the inline end of the item.</dd>
<dt><code class="highlight language-html">&lt;span data-indicator&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;span data-indicator&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Item indicator. It becomes visible with <code>data-checked="true"</code> or <code>aria-selected="true"</code>.</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;hr role="separator"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Separator between groups/items. Hidden while filtering.</dd>
</dl>
</dd>
@@ -177,7 +177,7 @@ The command menu can be used standalone or inside a native dialog. The structure
A simple command menu in a dialog. The button also shows the keyboard shortcut used in this example.
<Preview>
<button type="button" class="btn-outline" onclick="document.getElementById('command-basic').showModal()">
<button type="button" class="btn" data-variant="outline" onclick="document.getElementById('command-basic').showModal()">
Open command menu
<kbd class="kbd">⌘J</kbd>
</button>
+63 -79
View File
@@ -1,9 +1,9 @@
# Dialog
<Preview>
<button type="button" onclick="document.getElementById('demo-dialog-edit-profile').showModal()" class="btn-outline">Edit Profile</button>
<button type="button" onclick="document.getElementById('demo-dialog-edit-profile').showModal()" class="btn" data-variant="outline">Open Dialog</button>
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onclick="if (event.target === this) this.close()">
<div>
<div class="sm:max-w-sm">
<header>
<h2 id="demo-dialog-edit-profile-title">Edit profile</h2>
<p id="demo-dialog-edit-profile-description">Make changes to your profile here. Click save when you're done.</p>
@@ -21,10 +21,10 @@
</form>
</section>
<footer>
<button class="btn-outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save changes</button>
</footer>
<button type="button" class="btn-sm-icon-ghost" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" 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>
@@ -40,9 +40,9 @@
Basecoat uses the native `<dialog>` element and `showModal()`. This differs from shadcn/ui's portalled Base UI implementation, but preserves native modality, focus handling, and inert page content without component JavaScript.
```html
<button type="button" onclick="document.getElementById('demo-dialog-edit-profile').showModal()" class="btn-outline">Edit Profile</button>
<button type="button" onclick="document.getElementById('demo-dialog-edit-profile').showModal()" class="btn" data-variant="outline">Open Dialog</button>
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onclick="if (event.target === this) this.close()">
<div>
<div class="sm:max-w-sm">
<header>
<h2 id="demo-dialog-edit-profile-title">Edit profile</h2>
<p id="demo-dialog-edit-profile-description">Make changes to your profile here. Click save when you're done.</p>
@@ -60,10 +60,10 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
</form>
</section>
<footer>
<button class="btn-outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save changes</button>
</footer>
<button type="button" class="btn-sm-icon-ghost" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" 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>
@@ -73,31 +73,31 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;button type="button" onclick="dialog.showModal()"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;button type="button" onclick="dialog.showModal()"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Trigger button. Basecoat intentionally uses the native <code>HTMLDialogElement.showModal()</code> method.</dd>
<dt><code class="highlight language-html">&lt;dialog class="dialog" id="&#123; DIALOG_ID &#125;"&gt;</code></dt>
<dt><code>&lt;dialog class="dialog" id="&#123; DIALOG_ID &#125;"&gt;</code></dt>
<dd>
Native modal dialog. Add <code>aria-labelledby="&#123; TITLE_ID &#125;"</code> and <code>aria-describedby="&#123; DESCRIPTION_ID &#125;"</code> when title and description are present. The macro also adds backdrop-click close handling.
<dl>
<dt><code class="highlight language-html">&lt;div&gt;</code></dt>
<dt><code>&lt;div&gt;</code></dt>
<dd>
Dialog content surface.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;header&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>
Dialog header.
<dl>
<dt><code class="highlight language-html">&lt;h2 id="&#123; TITLE_ID &#125;"&gt;</code></dt>
<dt><code>&lt;h2 id="&#123; TITLE_ID &#125;"&gt;</code></dt>
<dd>Dialog title. Reference it from <code>aria-labelledby</code>.</dd>
<dt><code class="highlight language-html">&lt;p id="&#123; DESCRIPTION_ID &#125;"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;p id="&#123; DESCRIPTION_ID &#125;"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Dialog description. Reference it from <code>aria-describedby</code>.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;section&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Dialog body/content area. Add overflow utilities when the body should scroll.</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;footer&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Action area. It stacks actions on small screens and aligns them to the end on larger screens.</dd>
<dt><code class="highlight language-html">&lt;button type="button" onclick="this.closest('dialog').close()"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;button type="button" onclick="this.closest('dialog').close()"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Close button. You can also wrap a button in <code>&lt;form method="dialog"&gt;</code>.</dd>
</dl>
</dd>
@@ -110,23 +110,21 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
### Custom Close Button
<Preview>
<button type="button" onclick="document.getElementById('dialog-custom-close').showModal()" class="btn-outline">Open</button>
<button type="button" onclick="document.getElementById('dialog-custom-close').showModal()" class="btn" data-variant="outline">Share</button>
<dialog id="dialog-custom-close" class="dialog" aria-labelledby="dialog-custom-close-title" aria-describedby="dialog-custom-close-description" onclick="if (event.target === this) this.close()">
<div>
<div class="sm:max-w-md">
<header>
<h2 id="dialog-custom-close-title">Custom close button</h2>
<p id="dialog-custom-close-description">The default close icon is hidden and the footer provides the close action.</p>
<h2 id="dialog-custom-close-title">Share link</h2>
<p id="dialog-custom-close-description">Anyone who has this link will be able to view this.</p>
</header>
<section>
<p class="text-sm text-muted-foreground">
Use
<code>close_button=false</code>
and add a close control where it fits your layout.
</p>
<section class="flex items-center gap-2">
<div class="grid flex-1 gap-2">
<label class="label sr-only" for="dialog-custom-close-link">Link</label>
<input class="input" id="dialog-custom-close-link" value="https://ui.shadcn.com/docs/installation" readonly />
</div>
</section>
<footer>
<button class="btn-outline" onclick="this.closest('dialog').close()">Close</button>
<button class="btn" onclick="this.closest('dialog').close()">Continue</button>
<footer class="sm:justify-start">
<button class="btn" onclick="this.closest('dialog').close()">Close</button>
</footer>
</div>
</dialog>
@@ -135,19 +133,13 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
### No Close Button
<Preview>
<button type="button" onclick="document.getElementById('dialog-no-close').showModal()" class="btn-outline">Open</button>
<button type="button" onclick="document.getElementById('dialog-no-close').showModal()" class="btn" data-variant="outline">No Close Button</button>
<dialog id="dialog-no-close" class="dialog" aria-labelledby="dialog-no-close-title" aria-describedby="dialog-no-close-description" onclick="if (event.target === this) this.close()">
<div>
<header>
<h2 id="dialog-no-close-title">No close button</h2>
<p id="dialog-no-close-description">The dialog can still be closed with Escape or a custom action.</p>
<h2 id="dialog-no-close-title">No Close Button</h2>
<p id="dialog-no-close-description">This dialog doesn't have a close button in the top-right corner.</p>
</header>
<section>
<p class="text-sm text-muted-foreground">Native dialogs remain keyboard-accessible even without the default close icon.</p>
</section>
<footer>
<button class="btn" onclick="this.closest('dialog').close()">I understand</button>
</footer>
</div>
</dialog>
</Preview>
@@ -155,30 +147,27 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
### Sticky Footer
<Preview>
<button type="button" onclick="document.getElementById('dialog-sticky-footer').showModal()" class="btn-outline">Sticky Footer</button>
<button type="button" onclick="document.getElementById('dialog-sticky-footer').showModal()" class="btn" data-variant="outline">Sticky Footer</button>
<dialog id="dialog-sticky-footer" class="dialog" aria-labelledby="dialog-sticky-footer-title" aria-describedby="dialog-sticky-footer-description" onclick="if (event.target === this) this.close()">
<div>
<header>
<h2 id="dialog-sticky-footer-title">Sticky footer</h2>
<p id="dialog-sticky-footer-description">Keep actions visible while the body scrolls.</p>
<h2 id="dialog-sticky-footer-title">Sticky Footer</h2>
<p id="dialog-sticky-footer-description">This dialog has a sticky footer that stays visible while the content scrolls.</p>
</header>
<section class="overflow-y-auto scrollbar">
<div class="space-y-4 text-sm text-muted-foreground">
<p>Dialog content row 1. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 2. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 3. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 4. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 5. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 6. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 7. Add enough content to make the body scroll while the footer remains available.</p>
<p>Dialog content row 8. Add enough content to make the body scroll while the footer remains available.</p>
</div>
<section class="-mx-4 max-h-[50vh] overflow-y-auto px-4 scrollbar">
<p class="mb-4 leading-normal">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<p class="mb-4 leading-normal">Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p class="mb-4 leading-normal">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p class="mb-4 leading-normal">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<p class="mb-4 leading-normal">Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
<p class="mb-4 leading-normal">Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p class="mb-4 leading-normal">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p class="mb-4 leading-normal">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</section>
<footer class="sticky bottom-0">
<button class="btn-outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save</button>
<footer>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Close</button>
</footer>
<button type="button" class="btn-sm-icon-ghost" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" 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>
@@ -188,31 +177,26 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
### Scrollable Content
<Preview>
<button type="button" onclick="document.getElementById('dialog-scrollable').showModal()" class="btn-outline">Scrollable Content</button>
<button type="button" onclick="document.getElementById('dialog-scrollable').showModal()" class="btn" data-variant="outline">Scrollable Content</button>
<dialog id="dialog-scrollable" class="dialog" aria-labelledby="dialog-scrollable-title" aria-describedby="dialog-scrollable-description" onclick="if (event.target === this) this.close()">
<div>
<header>
<h2 id="dialog-scrollable-title">Scrollable Content</h2>
<p id="dialog-scrollable-description">Long content can scroll inside the dialog body.</p>
<p id="dialog-scrollable-description">This is a dialog with scrollable content.</p>
</header>
<section class="overflow-y-auto scrollbar">
<div class="space-y-4 text-sm text-muted-foreground">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 1.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 2.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 3.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 4.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 5.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 6.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 7.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 8.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 9.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row 10.</p>
</div>
<section class="-mx-4 max-h-[50vh] overflow-y-auto px-4 scrollbar">
<p class="mb-4 leading-normal">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p class="mb-4 leading-normal">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<p class="mb-4 leading-normal">Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
<p class="mb-4 leading-normal">Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p class="mb-4 leading-normal">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p class="mb-4 leading-normal">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<p class="mb-4 leading-normal">Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
<p class="mb-4 leading-normal">Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p class="mb-4 leading-normal">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p class="mb-4 leading-normal">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</section>
<footer>
<button class="btn-outline" onclick="this.closest('dialog').close()">Close</button>
</footer>
<button type="button" class="btn-sm-icon-ghost" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" 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>
@@ -225,7 +209,7 @@ Dialog positioning and close button placement use logical properties. Set `dir="
<Preview>
<div dir="rtl">
<button type="button" onclick="document.getElementById('dialog-rtl').showModal()" class="btn-outline">فتح</button>
<button type="button" onclick="document.getElementById('dialog-rtl').showModal()" class="btn" data-variant="outline">فتح</button>
<dialog id="dialog-rtl" class="dialog" aria-labelledby="dialog-rtl-title" aria-describedby="dialog-rtl-description" onclick="if (event.target === this) this.close()">
<div>
<header>
@@ -241,10 +225,10 @@ Dialog positioning and close button placement use logical properties. Set `dir="
</form>
</section>
<footer>
<button class="btn-outline" onclick="this.closest('dialog').close()">إلغاء</button>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">إلغاء</button>
<button class="btn" onclick="this.closest('dialog').close()">حفظ</button>
</footer>
<button type="button" class="btn-sm-icon-ghost" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" 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>
+57 -63
View File
@@ -2,18 +2,18 @@
<Preview>
<div id="demo-dropdown-menu" class="dropdown-menu">
<button type="button" id="demo-dropdown-menu-trigger" aria-haspopup="menu" aria-controls="demo-dropdown-menu-menu" aria-expanded="false" class="btn-outline">Open</button>
<button type="button" id="demo-dropdown-menu-trigger" aria-haspopup="menu" aria-controls="demo-dropdown-menu-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="demo-dropdown-menu-popover" data-popover aria-hidden="true" class="min-w-56">
<div role="menu" id="demo-dropdown-menu-menu" aria-labelledby="demo-dropdown-menu-trigger">
<div role="group" aria-labelledby="demo-dropdown-account">
<div role="heading" id="demo-dropdown-account">My Account</div>
<div role="menuitem">
Profile
<span data-shortcut>⇧⌘P</span>
<span>Profile</span>
<kbd>⇧⌘P</kbd>
</div>
<div role="menuitem">
Billing
<span data-shortcut>⌘B</span>
<span>Billing</span>
<kbd>⌘B</kbd>
</div>
<div role="menuitem">Settings</div>
<div role="menuitem">Keyboard shortcuts</div>
@@ -39,29 +39,26 @@ Basecoat dropdown menus are inline-positioned relative to the `.dropdown-menu` w
<Steps>
<Step title="Include the JavaScript files">
```html
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/dropdown-menu.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/dropdown-menu.min.js" defer></script>
```
</Step>
<Step title="Add your dropdown menu HTML">
```html
<div id="demo-dropdown-menu" class="dropdown-menu">
<button type="button" id="demo-dropdown-menu-trigger" aria-haspopup="menu" aria-controls="demo-dropdown-menu-menu" aria-expanded="false" class="btn-outline">Open</button>
<button type="button" id="demo-dropdown-menu-trigger" aria-haspopup="menu" aria-controls="demo-dropdown-menu-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="demo-dropdown-menu-popover" data-popover aria-hidden="true" class="min-w-56">
<div role="menu" id="demo-dropdown-menu-menu" aria-labelledby="demo-dropdown-menu-trigger">
<div role="group" aria-labelledby="demo-dropdown-account">
<div role="heading" id="demo-dropdown-account">My Account</div>
<div role="menuitem">
Profile
<span data-shortcut>⇧⌘P</span>
<span>Profile</span>
<kbd>⇧⌘P</kbd>
</div>
<div role="menuitem">
Billing
<span data-shortcut>⌘B</span>
<span>Billing</span>
<kbd>⌘B</kbd>
</div>
<div role="menuitem">Settings</div>
<div role="menuitem">Keyboard shortcuts</div>
@@ -76,40 +73,39 @@ Basecoat dropdown menus are inline-positioned relative to the `.dropdown-menu` w
</div>
</div>
```
</Step>
</Steps>
#### HTML structure
### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;div class="dropdown-menu"&gt;</code></dt>
<dt><code>&lt;div class="dropdown-menu"&gt;</code></dt>
<dd>
Relative wrapper for the trigger and inline menu content.
<dl>
<dt><code class="highlight language-html">&lt;button aria-haspopup="menu" aria-expanded="false"&gt;</code></dt>
<dt><code>&lt;button aria-haspopup="menu" aria-expanded="false"&gt;</code></dt>
<dd>Trigger button. The script toggles <code>aria-expanded</code> and manages keyboard navigation.</dd>
<dt><code class="highlight language-html">&lt;div data-popover aria-hidden="true"&gt;</code></dt>
<dt><code>&lt;div data-popover aria-hidden="true"&gt;</code></dt>
<dd>
Menu content popover. Set <code>data-side="top|right|bottom|left"</code> and <code>data-align="start|center|end"</code> to control placement.
<dl>
<dt><code class="highlight language-html">&lt;div role="menu"&gt;</code></dt>
<dt><code>&lt;div role="menu"&gt;</code></dt>
<dd>
Container for menu items, groups, labels, and separators.
<dl>
<dt><code class="highlight language-html">&lt;div role="group" aria-labelledby="&#123; HEADING_ID &#125;"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;div role="group" aria-labelledby="&#123; HEADING_ID &#125;"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Groups related menu items.</dd>
<dt><code class="highlight language-html">&lt;div role="heading" id="&#123; HEADING_ID &#125;"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;div role="heading" id="&#123; HEADING_ID &#125;"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Group heading/label.</dd>
<dt><code class="highlight language-html">&lt;div role="menuitem"&gt;</code></dt>
<dt><code>&lt;div role="menuitem"&gt;</code></dt>
<dd>Standard action item. Use <code>aria-disabled="true"</code> for disabled items.</dd>
<dt><code class="highlight language-html">&lt;div role="menuitemcheckbox" aria-checked="true"&gt;</code></dt>
<dt><code>&lt;div role="menuitemcheckbox" aria-checked="true"&gt;</code></dt>
<dd>Checkbox-style item. Add a child with <code>data-indicator</code> for the checked icon.</dd>
<dt><code class="highlight language-html">&lt;div role="menuitemradio" aria-checked="true"&gt;</code></dt>
<dt><code>&lt;div role="menuitemradio" aria-checked="true"&gt;</code></dt>
<dd>Radio-style item. Add a child with <code>data-indicator</code> for the selected icon.</dd>
<dt><code class="highlight language-html">&lt;span data-shortcut&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;kbd&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Shortcut hint aligned to the inline end of the item.</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;hr role="separator"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Separator between groups or options.</dd>
</dl>
</dd>
@@ -135,8 +131,8 @@ Basecoat dropdown menus are inline-positioned relative to the `.dropdown-menu` w
<Preview>
<div id="dropdown-basic" class="dropdown-menu">
<button type="button" id="dropdown-basic-trigger" aria-haspopup="menu" aria-controls="dropdown-basic-menu" aria-expanded="false" class="btn-outline">Open</button>
<div id="dropdown-basic-popover" data-popover aria-hidden="true" class="min-w-56">
<button type="button" id="dropdown-basic-trigger" aria-haspopup="menu" aria-controls="dropdown-basic-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="dropdown-basic-popover" data-popover aria-hidden="true" class="min-w-32">
<div role="menu" id="dropdown-basic-menu" aria-labelledby="dropdown-basic-trigger">
<div role="group" aria-labelledby="dropdown-basic-account">
<div role="heading" id="dropdown-basic-account">My Account</div>
@@ -156,25 +152,25 @@ Basecoat dropdown menus are inline-positioned relative to the `.dropdown-menu` w
<Preview>
<div id="dropdown-shortcuts" class="dropdown-menu">
<button type="button" id="dropdown-shortcuts-trigger" aria-haspopup="menu" aria-controls="dropdown-shortcuts-menu" aria-expanded="false" class="btn-outline">Open</button>
<div id="dropdown-shortcuts-popover" data-popover aria-hidden="true" class="min-w-56">
<button type="button" id="dropdown-shortcuts-trigger" aria-haspopup="menu" aria-controls="dropdown-shortcuts-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="dropdown-shortcuts-popover" data-popover aria-hidden="true" class="min-w-46">
<div role="menu" id="dropdown-shortcuts-menu" aria-labelledby="dropdown-shortcuts-trigger">
<div role="menuitem">
New Tab
<span data-shortcut>⌘T</span>
<span>New Tab</span>
<kbd>⌘T</kbd>
</div>
<div role="menuitem">
New Window
<span data-shortcut>⌘N</span>
<span>New Window</span>
<kbd>⌘N</kbd>
</div>
<div role="menuitem">
New Private Window
<span data-shortcut>⇧⌘N</span>
<span>New Private Window</span>
<kbd>⇧⌘N</kbd>
</div>
<hr role="separator" />
<div role="menuitem">
Print
<span data-shortcut>⌘P</span>
<span>Print</span>
<kbd>⌘P</kbd>
</div>
</div>
</div>
@@ -185,8 +181,8 @@ Basecoat dropdown menus are inline-positioned relative to the `.dropdown-menu` w
<Preview>
<div id="dropdown-icons" class="dropdown-menu">
<button type="button" id="dropdown-icons-trigger" aria-haspopup="menu" aria-controls="dropdown-icons-menu" aria-expanded="false" class="btn-outline">Open</button>
<div id="dropdown-icons-popover" data-popover aria-hidden="true" class="min-w-56">
<button type="button" id="dropdown-icons-trigger" aria-haspopup="menu" aria-controls="dropdown-icons-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="dropdown-icons-popover" data-popover aria-hidden="true" class="min-w-32">
<div role="menu" id="dropdown-icons-menu" aria-labelledby="dropdown-icons-trigger">
<div role="menuitem">
<svg class="lucide lucide-user" 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"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>
@@ -201,7 +197,7 @@ Basecoat dropdown menus are inline-positioned relative to the `.dropdown-menu` w
Settings
</div>
<hr role="separator" />
<div role="menuitem">
<div role="menuitem" data-variant="destructive">
<svg class="lucide lucide-log-out" 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"><path d="m16 17 5-5-5-5" /><path d="M21 12H9" /><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /></svg>
Log out
</div>
@@ -214,8 +210,8 @@ Basecoat dropdown menus are inline-positioned relative to the `.dropdown-menu` w
<Preview>
<div id="dropdown-checkboxes" class="dropdown-menu">
<button type="button" id="dropdown-checkboxes-trigger" aria-haspopup="menu" aria-controls="dropdown-checkboxes-menu" aria-expanded="false" class="btn-outline">Open</button>
<div id="dropdown-checkboxes-popover" data-popover aria-hidden="true" class="min-w-56">
<button type="button" id="dropdown-checkboxes-trigger" aria-haspopup="menu" aria-controls="dropdown-checkboxes-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="dropdown-checkboxes-popover" data-popover aria-hidden="true" class="min-w-32">
<div role="menu" id="dropdown-checkboxes-menu" aria-labelledby="dropdown-checkboxes-trigger">
<div role="group" aria-labelledby="dropdown-checkboxes-label">
<div role="heading" id="dropdown-checkboxes-label">Appearance</div>
@@ -223,28 +219,27 @@ Basecoat dropdown menus are inline-positioned relative to the `.dropdown-menu` w
<span data-indicator>
<svg class="lucide lucide-check" 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"><path d="M20 6 9 17l-5-5" /></svg>
</span>
Status Bar
<span data-shortcut>⌘S</span>
<span>Status Bar</span>
<kbd>⌘S</kbd>
</div>
<div role="menuitemcheckbox" aria-checked="false">
<span data-indicator>
<svg class="lucide lucide-check" 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"><path d="M20 6 9 17l-5-5" /></svg>
</span>
Activity Bar
<span data-shortcut>⌘A</span>
<span>Activity Bar</span>
<kbd>⌘A</kbd>
</div>
<div role="menuitemcheckbox" aria-checked="false" aria-disabled="true">
<span data-indicator>
<svg class="lucide lucide-check" 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"><path d="M20 6 9 17l-5-5" /></svg>
</span>
Panel
<span data-shortcut>⌘P</span>
<span>Panel</span>
<kbd>⌘P</kbd>
</div>
</div>
</div>
</div>
</div>
<script>
(() => {
const dropdownMenu = document.querySelector("#dropdown-checkboxes");
@@ -263,8 +258,8 @@ Basecoat dropdown menus are inline-positioned relative to the `.dropdown-menu` w
<Preview>
<div id="dropdown-radio-group" class="dropdown-menu">
<button type="button" id="dropdown-radio-group-trigger" aria-haspopup="menu" aria-controls="dropdown-radio-group-menu" aria-expanded="false" class="btn-outline">Open</button>
<div id="dropdown-radio-group-popover" data-popover aria-hidden="true" class="min-w-56">
<button type="button" id="dropdown-radio-group-trigger" aria-haspopup="menu" aria-controls="dropdown-radio-group-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="dropdown-radio-group-popover" data-popover aria-hidden="true" class="min-w-32">
<div role="menu" id="dropdown-radio-group-menu" aria-labelledby="dropdown-radio-group-trigger">
<div role="group" aria-labelledby="dropdown-radio-label">
<div role="heading" id="dropdown-radio-label">Panel Position</div>
@@ -290,7 +285,6 @@ Basecoat dropdown menus are inline-positioned relative to the `.dropdown-menu` w
</div>
</div>
</div>
<script>
(() => {
const dropdownMenu = document.querySelector("#dropdown-radio-group");
@@ -309,8 +303,8 @@ Basecoat dropdown menus are inline-positioned relative to the `.dropdown-menu` w
<Preview>
<div id="dropdown-destructive" class="dropdown-menu">
<button type="button" id="dropdown-destructive-trigger" aria-haspopup="menu" aria-controls="dropdown-destructive-menu" aria-expanded="false" class="btn-outline">Open</button>
<div id="dropdown-destructive-popover" data-popover aria-hidden="true" class="min-w-56">
<button type="button" id="dropdown-destructive-trigger" aria-haspopup="menu" aria-controls="dropdown-destructive-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="dropdown-destructive-popover" data-popover aria-hidden="true" class="min-w-32">
<div role="menu" id="dropdown-destructive-menu" aria-labelledby="dropdown-destructive-trigger">
<div role="menuitem">Edit</div>
<div role="menuitem">Duplicate</div>
@@ -331,17 +325,17 @@ Dropdown menus support document direction. Set `dir="rtl"` on the dropdown root
<Preview>
<div dir="rtl">
<div id="dropdown-rtl" class="dropdown-menu">
<button type="button" id="dropdown-rtl-trigger" aria-haspopup="menu" aria-controls="dropdown-rtl-menu" aria-expanded="false" class="btn-outline">فتح</button>
<div id="dropdown-rtl-popover" data-popover aria-hidden="true" class="min-w-56" data-align="end">
<button type="button" id="dropdown-rtl-trigger" aria-haspopup="menu" aria-controls="dropdown-rtl-menu" aria-expanded="false" class="btn" data-variant="outline">فتح</button>
<div id="dropdown-rtl-popover" data-popover aria-hidden="true" class="min-w-32" data-align="end">
<div role="menu" id="dropdown-rtl-menu" aria-labelledby="dropdown-rtl-trigger">
<div role="heading" id="dropdown-rtl-label">الحساب</div>
<div role="menuitem">
الملف الشخصي
<span data-shortcut>⌘P</span>
<span>الملف الشخصي</span>
<kbd>⌘P</kbd>
</div>
<div role="menuitem">
الإعدادات
<span data-shortcut>⌘S</span>
<span>الإعدادات</span>
<kbd>⌘S</kbd>
</div>
<hr role="separator" />
<div role="menuitem" data-variant="destructive">حذف</div>
+17 -17
View File
@@ -12,9 +12,9 @@
<footer>
<div class="flex gap-2">
<button class="btn">Create Project</button>
<button class="btn-outline">Import Project</button>
<button class="btn" data-variant="outline">Import Project</button>
</div>
<a href="#" class="btn-link text-muted-foreground">
<a href="#" class="btn text-muted-foreground" data-variant="link">
Learn More
<svg class="lucide lucide-arrow-up-right" 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" ><path d="M7 7h10v10" /><path d="M7 17 17 7" /></svg>
</a>
@@ -24,7 +24,7 @@
## Usage
Use `.empty` on a semantic container. The component styles common children like `header`, `figure`, headings, descriptions, and `footer`.
Add `class="empty"` to a semantic container. The component styles common children like `header`, `figure`, headings, descriptions, and `footer`.
```html
<section class="empty">
@@ -38,9 +38,9 @@ Use `.empty` on a semantic container. The component styles common children like
<footer>
<div class="flex gap-2">
<button class="btn">Create Project</button>
<button class="btn-outline">Import Project</button>
<button class="btn" data-variant="outline">Import Project</button>
</div>
<a href="#" class="btn-link text-muted-foreground">
<a href="#" class="btn text-muted-foreground" data-variant="link">
Learn More
<svg class="lucide lucide-arrow-up-right" 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" ><path d="M7 7h10v10" /><path d="M7 17 17 7" /></svg>
</a>
@@ -51,23 +51,23 @@ Use `.empty` on a semantic container. The component styles common children like
#### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;section class="empty"&gt;</code></dt>
<dt><code>&lt;section class="empty"&gt;</code></dt>
<dd>
The empty-state container.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dt><code>&lt;header&gt;</code></dt>
<dd>
Groups the media, title, and description.
<dl>
<dt><code class="highlight language-html">&lt;figure&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;figure&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Icon, avatar, image, or avatar group. An icon-only figure is styled automatically with <code>:has(&gt; svg:only-child)</code>.</dd>
<dt><code class="highlight language-html">&lt;h2&gt;</code>, <code class="highlight language-html">&lt;h3&gt;</code>, or <code class="highlight language-html">&lt;h4&gt;</code></dt>
<dt><code>&lt;h2&gt;</code>, <code>&lt;h3&gt;</code>, or <code>&lt;h4&gt;</code></dt>
<dd>The empty-state title.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;p&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The empty-state description.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> or <code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;section&gt;</code> or <code>&lt;footer&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Actions and supporting content.</dd>
</dl>
</dd>
@@ -87,7 +87,7 @@ Use `.empty` on a semantic container. The component styles common children like
<p>Upload files to your cloud storage to access them anywhere.</p>
</header>
<footer>
<button class="btn-sm-outline">Upload Files</button>
<button class="btn" data-variant="outline" data-size="sm">Upload Files</button>
</footer>
</section>
</Preview>
@@ -104,7 +104,7 @@ Use `.empty` on a semantic container. The component styles common children like
<p>This user is currently offline. You can leave a message or try again later.</p>
</header>
<footer>
<button class="btn-sm">Leave Message</button>
<button class="btn" data-size="sm">Leave Message</button>
</footer>
</section>
</Preview>
@@ -123,9 +123,9 @@ Use `.empty` on a semantic container. The component styles common children like
<p>Invite your team to collaborate on this project.</p>
</header>
<footer>
<button class="btn-sm">
<button class="btn" data-size="sm">
<svg class="lucide lucide-plus" 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" ><path d="M5 12h14" /><path d="M12 5v14" /></svg>
Invite Members
<span>Invite Members</span>
</button>
</footer>
</section>
@@ -140,7 +140,7 @@ Use `.empty` on a semantic container. The component styles common children like
<p>The page you're looking for doesn't exist. Try searching for what you need below.</p>
</header>
<footer>
<div class="input-group w-full">
<div class="input-group sm:w-3/4">
<svg class="lucide lucide-search" 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" ><path d="m21 21-4.34-4.34" /><circle cx="11" cy="11" r="8" /></svg>
<input type="search" placeholder="Try searching for pages..." />
<kbd class="kbd">/</kbd>
@@ -162,7 +162,7 @@ Use `.empty` on a semantic container. The component styles common children like
<p>ابدأ بإنشاء مشروعك الأول.</p>
</header>
<footer>
<button class="btn-sm">إنشاء مشروع</button>
<button class="btn" data-size="sm">إنشاء مشروع</button>
</footer>
</section>
</Preview>
+326 -296
View File
@@ -1,162 +1,169 @@
# Field
<Preview>
<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>
<form class="w-full max-w-md">
<div role="group" aria-label="Checkout fields" class="fieldset">
<fieldset class="fieldset">
<legend>Payment Method</legend>
<p>All transactions are secure and encrypted</p>
<div role="group" aria-label="Payment method fields">
<div role="group" class="field">
<label for="checkout-card-name">Name on Card</label>
<input id="checkout-card-name" type="text" placeholder="Evil Rabbit" required />
</div>
<div role="group" class="field">
<label for="checkout-card-number">Card Number</label>
<input id="checkout-card-number" type="text" placeholder="1234 5678 9012 3456" aria-describedby="checkout-card-number-description" required />
<p id="checkout-card-number-description">Enter your 16-digit card number</p>
</div>
<div class="grid grid-cols-3 gap-4">
<div role="group" class="field">
<label for="checkout-exp-month">Month</label>
<select id="checkout-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="checkout-exp-year">Year</label>
<select id="checkout-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="checkout-cvv">CVV</label>
<input id="checkout-cvv" type="text" placeholder="123" required />
</div>
</div>
</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>
</fieldset>
<div class="field-separator">
<hr role="separator" />
</div>
<fieldset class="fieldset">
<legend>Billing Address</legend>
<p>The billing address associated with your payment method</p>
<div role="group" class="field" data-orientation="horizontal">
<input id="checkout-same-as-shipping" type="checkbox" checked />
<label for="checkout-same-as-shipping" class="font-normal">Same as shipping address</label>
</div>
</fieldset>
<fieldset class="fieldset">
<div role="group" class="field">
<label for="cvv">CVV</label>
<input id="cvv" type="text" placeholder="123" required />
<label for="checkout-comments">Comments</label>
<textarea id="checkout-comments" placeholder="Add any additional comments" rows="3"></textarea>
</div>
</fieldset>
<div role="group" class="field" data-orientation="horizontal">
<button type="submit" class="btn">Submit</button>
<button type="button" class="btn" data-variant="outline">Cancel</button>
</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>
</Preview>
## Usage
Field uses semantic HTML with minimal classes. Two main patterns can be used independently or composed together:
Use `class="field"` for one control and `class="fieldset"` for related controls. Use a native `<fieldset>` when the group needs a `<legend>`; otherwise use a labelled `role="group"`.
- `<div role="group" class="field">` for individual field containers.
- `<fieldset class="fieldset">` for grouping multiple related fields together.
```html
<fieldset class="fieldset">
<legend>Profile</legend>
<p>This appears on invoices and emails.</p>
<div role="group" aria-label="Profile fields">
<div role="group" class="field">
<label for="name">Full name</label>
<input id="name" autocomplete="off" placeholder="Evil Rabbit" />
<p>This appears on invoices and emails.</p>
</div>
<div role="group" class="field" data-invalid>
<label for="username">Username</label>
<input id="username" autocomplete="off" aria-invalid="true" aria-describedby="username-error" />
<p id="username-error" role="alert">Choose another username.</p>
</div>
<div role="group" class="field" data-orientation="horizontal">
<input id="newsletter" type="checkbox" role="switch" />
<label for="newsletter">Subscribe to the newsletter</label>
</div>
</div>
</fieldset>
```
Elements inside these containers are styled automatically based on their purpose. Use `aria-describedby` to connect helper text and errors to inputs.
**Fields** have the following HTML structure:
### HTML structure
<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>
**Fieldsets** have the following HTML structure:
<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>
<dt><code>&lt;fieldset class="fieldset"&gt;</code></dt>
<dd>Native group for related fields. Use <code>&lt;legend&gt;</code> for the label and a following <code>&lt;p&gt;</code> for description.</dd>
<dt><code>&lt;div role="group" class="fieldset"&gt;</code></dt>
<dd>Structural group when <code>&lt;fieldset&gt;</code> is not appropriate. Add <code>aria-label</code> or <code>aria-labelledby</code>.</dd>
<dt><code>&lt;div role="group" class="field"&gt;</code></dt>
<dd>Single field wrapper. Add <code>data-orientation</code> when needed.</dd>
<dt><code>&lt;label&gt;</code></dt>
<dd>Label for the control. Use <code>for</code> when the control has an <code>id</code>.</dd>
<dt><code>&lt;input&gt;</code>, <code>&lt;select&gt;</code>, <code>&lt;textarea&gt;</code></dt>
<dd>Native form control. Put <code>aria-invalid="true"</code> on the invalid control, not only on the wrapper.</dd>
<dt><code>&lt;section&gt;</code></dt>
<dd>Optional content wrapper for label and description when the control sits beside text.</dd>
<dt><code>&lt;p&gt;</code></dt>
<dd>Helper text or error text. Use <code>role="alert"</code> for validation errors and connect it with <code>aria-describedby</code>.</dd>
</dl>
## Examples
### Input
<Preview class="w-full max-w-md">
<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>
<Preview class="w-full max-w-xs">
<fieldset class="fieldset">
<div role="group" aria-label="Account credentials">
<div role="group" class="field">
<label for="username">Username</label>
<input id="username" type="text" placeholder="Max Leiter" aria-describedby="username-description" />
<p id="username-description">Choose a unique username for your account.</p>
</div>
<div role="group" class="field">
<label for="password">Password</label>
<p id="password-description">Must be at least 8 characters long.</p>
<input id="password" type="password" placeholder="••••••••" aria-describedby="password-description" />
</div>
</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>
</fieldset>
</Preview>
### Textarea
<Preview class="w-full max-w-md">
<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>
<Preview class="w-full max-w-xs">
<fieldset class="fieldset">
<div role="group" class="field">
<label for="feedback">Feedback</label>
<textarea id="feedback" placeholder="Your feedback helps us improve..." rows="4" aria-describedby="feedback-description"></textarea>
<p id="feedback-description">Share your thoughts about our service.</p>
</div>
</fieldset>
</Preview>
### Select
<Preview class="w-full max-w-md">
<Preview class="w-full max-w-xs">
<div role="group" class="field">
<label for="country">Department</label>
<select id="country" class="select w-full" aria-describedby="country-desc">
<label for="department">Department</label>
<select id="department" class="select w-full" aria-describedby="department-description">
<option value="">Choose department</option>
<option value="engineering">Engineering</option>
<option value="design">Design</option>
@@ -167,139 +174,119 @@ Elements inside these containers are styled automatically based on their purpose
<option value="finance">Finance</option>
<option value="operations">Operations</option>
</select>
<p id="country-desc">Select your department or area of work.</p>
<p id="department-description">Select your department or area of work.</p>
</div>
</Preview>
### Slider
<Preview class="w-full max-w-sm">
<Preview class="w-full max-w-xs">
<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" />
<label for="price-range">Price Range</label>
<p id="price-range-description">Set your maximum budget <span id="price-range-output">(up to $800)</span>.</p>
<input id="price-range" type="range" min="0" max="1000" step="10" value="800" aria-describedby="price-range-description" class="input" />
</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}%\`);
const input = document.getElementById("price-range");
const output = document.getElementById("price-range-output");
if (!input || !output) return;
const updateRange = () => {
const min = Number(input.min || 0);
const max = Number(input.max || 100);
const value = Number(input.value || 0);
const percent = max === min ? 0 : ((value - min) / (max - min)) * 100;
input.style.setProperty("--slider-value", `${percent}%`);
output.textContent = `(up to $${value})`;
};
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);
input.addEventListener("input", updateRange);
updateRange();
})();
</script>
</Preview>
### Fieldset
Group related fields with `<fieldset class="fieldset">` and `<legend>`.
<Preview>
<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>
</Preview>
### Checkbox
<Preview class="w-full max-w-md">
<Preview class="w-full max-w-sm">
<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">
<legend>Address Information</legend>
<p>We need your address to deliver your order.</p>
<div role="group" aria-label="Address fields">
<div role="group" class="field">
<label class="gap-3">
<input type="checkbox" />
Hard disks
</label>
<label for="street">Street Address</label>
<input id="street" type="text" placeholder="123 Main St" />
</div>
<div role="group" class="field">
<label class="gap-3">
<input type="checkbox" />
External disks
</label>
<div class="grid grid-cols-2 gap-4">
<div role="group" class="field">
<label for="city">City</label>
<input id="city" type="text" placeholder="New York" />
</div>
<div role="group" class="field">
<label for="zip">Postal Code</label>
<input id="zip" type="text" placeholder="90502" />
</div>
</div>
<div role="group" class="field">
<label class="gap-3">
<input type="checkbox" />
CDs, DVDs and iPods
</label>
</div>
<div role="group" class="field">
<label class="gap-3">
<input type="checkbox" />
Connected servers
</label>
</div>
</div>
<hr role="separator" />
<div role="group" class="field" data-orientation="horizontal">
<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>
</fieldset>
</Preview>
### Radio Group
### Checkbox
<Preview class="w-full max-w-md">
<Preview class="w-full max-w-xs">
<div role="group" aria-label="Finder preferences" class="fieldset">
<fieldset class="fieldset">
<legend data-variant="label">Show these items on the desktop</legend>
<p>Select the items you want to show on the desktop.</p>
<div role="group" aria-label="Desktop items">
<div role="group" class="field" data-orientation="horizontal">
<input id="finder-hard-disks" type="checkbox" />
<label for="finder-hard-disks" class="font-normal">Hard disks</label>
</div>
<div role="group" class="field" data-orientation="horizontal">
<input id="finder-external-disks" type="checkbox" />
<label for="finder-external-disks" class="font-normal">External disks</label>
</div>
<div role="group" class="field" data-orientation="horizontal">
<input id="finder-cds" type="checkbox" />
<label for="finder-cds" class="font-normal">CDs, DVDs, and iPods</label>
</div>
<div role="group" class="field" data-orientation="horizontal">
<input id="finder-connected-servers" type="checkbox" />
<label for="finder-connected-servers" class="font-normal">Connected servers</label>
</div>
</div>
</fieldset>
<div class="field-separator">
<hr role="separator" />
</div>
<div role="group" class="field" data-orientation="horizontal">
<input id="sync-folders" type="checkbox" checked />
<section>
<label for="sync-folders">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>
</Preview>
### Radio
<Preview class="w-full max-w-xs">
<fieldset class="fieldset">
<legend>Subscription plan</legend>
<legend data-variant="label">Subscription Plan</legend>
<p>Yearly and lifetime plans offer significant savings.</p>
<div role="radiogroup" class="grid gap-3">
<div role="group" class="field">
<label class="gap-3">
<input type="radio" name="subscription-plan" checked />
Monthly ($9.99/month)
</label>
<div role="radiogroup" aria-label="Subscription plan">
<div role="group" class="field" data-orientation="horizontal">
<input id="plan-monthly" type="radio" name="plan" value="monthly" checked />
<label for="plan-monthly" class="font-normal">Monthly ($9.99/month)</label>
</div>
<div role="group" class="field">
<label class="gap-3">
<input type="radio" name="subscription-plan" />
Yearly ($99.99/year)
</label>
<div role="group" class="field" data-orientation="horizontal">
<input id="plan-yearly" type="radio" name="plan" value="yearly" />
<label for="plan-yearly" class="font-normal">Yearly ($99.99/year)</label>
</div>
<div role="group" class="field">
<label class="gap-3">
<input type="radio" name="subscription-plan" />
Lifetime ($299.99)
</label>
<div role="group" class="field" data-orientation="horizontal">
<input id="plan-lifetime" type="radio" name="plan" value="lifetime" />
<label for="plan-lifetime" class="font-normal">Lifetime ($299.99)</label>
</div>
</div>
</fieldset>
@@ -307,9 +294,7 @@ Group related fields with `<fieldset class="fieldset">` and `<legend>`.
### Switch
Set `data-orientation="horizontal"` on `.field` to align labels and controls side-by-side.
<Preview class="w-full max-w-md">
<Preview class="w-full max-w-sm">
<div role="group" class="field" data-orientation="horizontal">
<section>
<label for="multi-factor-authentication">Multi-factor authentication</label>
@@ -321,26 +306,30 @@ Set `data-orientation="horizontal"` on `.field` to align labels and controls sid
### Choice Card
<Preview class="w-full max-w-md">
Wrap a `.field` inside a `<label>` to create selectable field cards. This works with radio, checkbox, and switch controls.
<Preview class="w-full max-w-xs">
<fieldset class="fieldset">
<legend class="text-sm">Compute Environment</legend>
<legend data-variant="label">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 />
<div role="radiogroup" aria-label="Compute environment">
<label for="compute-kubernetes">
<div role="group" class="field" data-orientation="horizontal">
<section>
<h3>Kubernetes</h3>
<p>Run GPU workloads on a K8s cluster.</p>
</section>
<input id="compute-kubernetes" type="radio" name="compute" value="kubernetes" checked />
</div>
</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 for="compute-vm">
<div role="group" class="field" data-orientation="horizontal">
<section>
<h3>Virtual Machine</h3>
<p>Access a cluster to run GPU workloads.</p>
</section>
<input id="compute-vm" type="radio" name="compute" value="vm" />
</div>
</label>
</div>
</fieldset>
@@ -348,70 +337,91 @@ Set `data-orientation="horizontal"` on `.field` to align labels and controls sid
### Field Group
<Preview class="w-full max-w-md">
<fieldset class="fieldset">
<legend>Account</legend>
<p>Use a field group to stack related fields with consistent spacing.</p>
<div role="group" aria-label="Field group">
<div role="group" class="field">
<label for="field-group-name">Name</label>
<input id="field-group-name" type="text" placeholder="Evil Rabbit" />
</div>
<div class="field-separator">
<hr role="separator" />
<span>Optional</span>
</div>
<div role="group" class="field">
<label for="field-group-bio">Bio</label>
<textarea id="field-group-bio" rows="3" placeholder="Tell us about yourself"></textarea>
Stack fields inside a group and use `.field-separator` to divide sections.
<Preview class="w-full max-w-xs">
<div role="group" aria-label="Notification preferences" class="fieldset">
<fieldset class="fieldset">
<legend data-variant="label">Responses</legend>
<p>Get notified when ChatGPT responds to requests that take time, like research or image generation.</p>
<div role="group" aria-label="Response notifications">
<div role="group" class="field" data-orientation="horizontal" data-disabled="true">
<input id="push-responses" type="checkbox" checked disabled />
<label for="push-responses" class="font-normal">Push notifications</label>
</div>
</div>
</fieldset>
<div class="field-separator">
<hr role="separator" />
</div>
</fieldset>
<fieldset class="fieldset">
<legend data-variant="label">Tasks</legend>
<p>Get notified when tasks you've created have updates. <a href="#">Manage tasks</a></p>
<div role="group" aria-label="Task notifications">
<div role="group" class="field" data-orientation="horizontal">
<input id="push-tasks" type="checkbox" />
<label for="push-tasks" class="font-normal">Push notifications</label>
</div>
<div role="group" class="field" data-orientation="horizontal">
<input id="email-tasks" type="checkbox" />
<label for="email-tasks" class="font-normal">Email notifications</label>
</div>
</div>
</fieldset>
</div>
</Preview>
### Responsive Layout
For responsive layouts, use standard Tailwind responsive classes directly on `.field`. This example stacks vertically on mobile and switches to horizontal at the `md` breakpoint.
Use `data-orientation="responsive"` to stack by default and switch to horizontal in wider containers.
<Preview class="w-full max-w-3xl">
<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" />
<Preview class="w-full max-w-4xl">
<form>
<fieldset class="fieldset">
<legend>Profile</legend>
<p>Fill in your profile information.</p>
<div class="field-separator">
<hr role="separator" />
</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 role="group" aria-label="Profile fields">
<div role="group" class="field" data-orientation="responsive">
<section>
<label for="responsive-name">Name</label>
<p id="responsive-name-description">Provide your full name for identification</p>
</section>
<input id="responsive-name" placeholder="Evil Rabbit" aria-describedby="responsive-name-description" required />
</div>
<div class="field-separator">
<hr role="separator" />
</div>
<div role="group" class="field" data-orientation="responsive">
<section>
<label for="responsive-message">Message</label>
<p id="responsive-message-description">You can write your message here. Keep it short, preferably under 100 characters.</p>
</section>
<textarea id="responsive-message" placeholder="Hello, world!" rows="4" aria-describedby="responsive-message-description" required class="min-h-[100px] resize-none sm:min-w-[300px]"></textarea>
</div>
<div class="field-separator">
<hr role="separator" />
</div>
<div role="group" class="field" data-orientation="responsive">
<button type="submit" class="btn">Submit</button>
<button type="button" class="btn" data-variant="outline">Cancel</button>
</div>
</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>
</fieldset>
</form>
</Preview>
### RTL
Fields support right-to-left layouts through native form direction and logical spacing. Add `dir="rtl"` to the field, fieldset, or an ancestor.
Add `dir="rtl"` to the field, fieldset, or an ancestor.
<Preview class="w-full max-w-md">
<Preview class="w-full max-w-xs">
<fieldset class="fieldset" dir="rtl">
<legend>الملف الشخصي</legend>
<p>تظهر هذه المعلومات في حسابك.</p>
<div role="group" aria-label="Field group">
<div role="group" aria-label="حقول الملف الشخصي">
<div role="group" class="field">
<label for="field-rtl-name">الاسم الكامل</label>
<input id="field-rtl-name" type="text" placeholder="أرنب شرير" />
@@ -420,3 +430,23 @@ Fields support right-to-left layouts through native form direction and logical s
</div>
</fieldset>
</Preview>
## Validation and Errors
Add `data-invalid` to `.field` and `aria-invalid="true"` to the invalid control.
```html
<div role="group" class="field" data-invalid>
<label for="email">Email</label>
<input id="email" type="email" aria-invalid="true" aria-describedby="email-error" />
<p id="email-error" role="alert">Enter a valid email address.</p>
</div>
```
<Preview class="w-full max-w-xs">
<div role="group" class="field" data-invalid>
<label for="invalid-email">Email</label>
<input id="invalid-email" type="email" value="wrong-email" aria-invalid="true" aria-describedby="invalid-email-error" />
<p id="invalid-email-error" role="alert">Enter a valid email address.</p>
</div>
</Preview>
+28 -68
View File
@@ -1,52 +1,12 @@
# Input Group
<Preview class="w-full max-w-sm">
<div class="grid gap-6">
<div class="input-group">
<input type="text" placeholder="Search..." />
<span data-align="start" aria-hidden="true">
<svg class="lucide lucide-search" 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"><path d="m21 21-4.34-4.34" /><circle cx="11" cy="11" r="8" /></svg>
</span>
<span data-align="end">12 results</span>
</div>
<div class="input-group">
<input type="text" placeholder="example.com" />
<span data-align="start">https://</span>
<button type="button" class="btn-xs-icon-ghost rounded-full" data-align="end" aria-label="Info">
<svg class="lucide lucide-info" 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"><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /></svg>
</button>
</div>
<div class="input-group min-h-32" data-orientation="vertical">
<textarea placeholder="Ask, Search or Chat..."></textarea>
<footer data-align="end" role="group" aria-label="Message actions">
<button type="button" class="btn-xs-icon-outline rounded-full" aria-label="Add attachment">
<svg class="lucide lucide-plus" 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"><path d="M5 12h14" /><path d="M12 5v14" /></svg>
</button>
<div id="dropdown-menu-670915" class="dropdown-menu min-w-0">
<button type="button" id="dropdown-menu-670915-trigger" aria-haspopup="menu" aria-controls="dropdown-menu-670915-menu" aria-expanded="false" class="btn-xs-ghost">Auto</button>
<div id="dropdown-menu-670915-popover" data-popover aria-hidden="true" data-side="top" data-align="start" class="min-w-28 [--radius:0.95rem]">
<div role="menu" id="dropdown-menu-670915-menu" aria-labelledby="dropdown-menu-670915-trigger">
<div id="dropdown-menu-670915-items-1" role="menuitem">Auto</div>
<div id="dropdown-menu-670915-items-2" role="menuitem">Agent</div>
<div id="dropdown-menu-670915-items-3" role="menuitem">Manual</div>
</div>
</div>
</div>
<span class="ms-auto">52% used</span>
<hr role="separator" class="h-4" />
<button type="button" class="btn-xs-icon rounded-full" disabled aria-label="Send">
<svg class="lucide lucide-arrow-up" 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"><path d="m5 12 7-7 7 7" /><path d="M12 19V5" /></svg>
</button>
</footer>
</div>
<div class="input-group">
<input type="text" placeholder="@shadcn" />
<span data-align="end" aria-hidden="true">
<span class="flex size-4 items-center justify-center rounded-full bg-primary text-primary-foreground">
<svg class="size-3 lucide lucide-check" 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"><path d="M20 6 9 17l-5-5" /></svg>
</span>
</span>
</div>
<Preview class="w-full max-w-xs">
<div class="input-group">
<input type="text" placeholder="Search..." />
<span data-align="start" aria-hidden="true">
<svg class="lucide lucide-search" 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"><path d="m21 21-4.34-4.34" /><circle cx="11" cy="11" r="8" /></svg>
</span>
<span data-align="end">12 results</span>
</div>
</Preview>
@@ -62,24 +22,24 @@
</div>
```
Use `.input-group` on the root. Native controls stay semantic and become borderless children inside that shell.
Add `class="input-group"` to the root. Native controls stay semantic and become borderless children inside that shell.
<dl>
<dt><code class="highlight language-html">&lt;div class="input-group"&gt;</code></dt>
<dt><code>&lt;div class="input-group"&gt;</code></dt>
<dd>
The root shell. Add <code>role="group"</code> and an accessible label only when the group itself needs to be named.
<dl>
<dt><code class="highlight language-html">&lt;input&gt;</code>, <code class="highlight language-html">&lt;textarea&gt;</code>, <code class="highlight language-html">&lt;select&gt;</code></dt>
<dt><code>&lt;input&gt;</code>, <code>&lt;textarea&gt;</code>, <code>&lt;select&gt;</code></dt>
<dd>The native control. Use <code>aria-invalid="true"</code> for invalid state and <code>disabled</code> for disabled state.</dd>
<dt><code class="highlight language-html">&lt;span&gt;</code>, <code class="highlight language-html">&lt;svg&gt;</code>, <code class="highlight language-html">&lt;kbd&gt;</code></dt>
<dt><code>&lt;span&gt;</code>, <code>&lt;svg&gt;</code>, <code>&lt;kbd&gt;</code></dt>
<dd>Decorative or helper content. Use <code>aria-hidden="true"</code> for decorative icons.</dd>
<dt><code class="highlight language-html">&lt;button&gt;</code>, <code class="highlight language-html">&lt;div role="group"&gt;</code></dt>
<dt><code>&lt;button&gt;</code>, <code>&lt;div role="group"&gt;</code></dt>
<dd>Interactive actions. Use <code>role="group"</code> with an accessible label when multiple related actions share one side of the input.</dd>
<dt><code class="highlight language-html">&lt;header&gt;</code>, <code class="highlight language-html">&lt;footer&gt;</code></dt>
<dt><code>&lt;header&gt;</code>, <code>&lt;footer&gt;</code></dt>
<dd>Header or footer addon rows for vertically oriented input groups.</dd>
<dt><code class="highlight language-html">data-align="start|end"</code></dt>
<dt><code>data-align="start|end"</code></dt>
<dd>Optional visual placement. Values use logical direction, so start/end automatically flip in RTL. Use <code>data-orientation="vertical"</code> on the root for header/footer layouts.</dd>
<dt><code class="highlight language-html">[data-control]</code></dt>
<dt><code>[data-control]</code></dt>
<dd>Escape hatch for custom controls that are not native <code>input</code>, <code>textarea</code>, or <code>select</code>.</dd>
</dl>
</dd>
@@ -140,7 +100,7 @@ Use `data-align="start|end"` on addon wrappers to position them inline. Use `dat
<header data-align="start">
<svg class="text-muted-foreground lucide lucide-file-code" 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"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /><path d="M14 2v5a1 1 0 0 0 1 1h5" /><path d="M10 12.5 8 15l2 2.5" /><path d="m14 12.5 2 2.5-2 2.5" /></svg>
<span class="font-mono">script.js</span>
<button type="button" class="btn-xs-icon-ghost ml-auto" aria-label="Copy">
<button type="button" class="btn ml-auto size-6" data-variant="ghost" data-size="icon-sm" aria-label="Copy">
<svg class="lucide lucide-copy" 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"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg>
</button>
</header>
@@ -171,7 +131,7 @@ Use `data-align="start|end"` on addon wrappers to position them inline. Use `dat
<textarea id="block-end-textarea" placeholder="Write a comment..."></textarea>
<footer data-align="end">
<span>0/280</span>
<button type="button" class="btn-sm ml-auto">Post</button>
<button type="button" class="btn ml-auto" data-size="sm">Post</button>
</footer>
</div>
<p>Footer positioned below the textarea.</p>
@@ -247,23 +207,23 @@ Use `data-align="start|end"` on addon wrappers to position them inline. Use `dat
<div class="grid gap-6">
<div class="input-group">
<input type="text" readonly placeholder="https://x.com/shadcn" />
<button type="button" class="btn-xs-icon-ghost" data-align="end" aria-label="Copy" title="Copy">
<button type="button" class="btn" data-variant="ghost" data-size="icon-xs" data-align="end" aria-label="Copy" title="Copy">
<svg class="lucide lucide-copy" 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"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg>
</button>
</div>
<div class="input-group [--radius:9999px]">
<button type="button" class="btn-xs-icon-secondary" data-align="start" aria-label="Connection information">
<button type="button" class="btn" data-variant="secondary" data-size="icon-xs" data-align="start" aria-label="Connection information">
<svg class="lucide lucide-info" 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"><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /></svg>
</button>
<span data-align="start" class="text-muted-foreground">https://</span>
<input type="text" id="input-secure-19" />
<button type="button" class="btn-xs-icon-ghost" data-align="end" aria-label="Favorite">
<button type="button" class="btn" data-variant="ghost" data-size="icon-xs" data-align="end" aria-label="Favorite">
<svg class="lucide lucide-star" 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"><path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" /></svg>
</button>
</div>
<div class="input-group">
<input type="text" placeholder="Type to search..." />
<button type="button" class="btn-secondary" data-align="end">Search</button>
<button type="button" class="btn" data-size="xs" data-variant="secondary" data-align="end">Search</button>
</div>
</div>
</Preview>
@@ -287,7 +247,7 @@ Use `data-align="start|end"` on addon wrappers to position them inline. Use `dat
<div class="input-group">
<input type="text" placeholder="Enter file name" />
<div id="dropdown-menu-601971" class="dropdown-menu" data-align="end">
<button type="button" id="dropdown-menu-601971-trigger" aria-haspopup="menu" aria-controls="dropdown-menu-601971-menu" aria-expanded="false" class="btn-xs-icon-ghost" aria-label="Open file actions">
<button type="button" id="dropdown-menu-601971-trigger" aria-haspopup="menu" aria-controls="dropdown-menu-601971-menu" aria-expanded="false" class="btn" data-variant="ghost" data-size="icon-xs" aria-label="Open file actions">
<svg class="lucide lucide-ellipsis" 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"><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></svg>
</button>
<div id="dropdown-menu-601971-popover" data-popover aria-hidden="true" data-align="end" class="min-w-32">
@@ -302,7 +262,7 @@ Use `data-align="start|end"` on addon wrappers to position them inline. Use `dat
<div class="input-group [--radius:1rem]">
<input type="text" placeholder="Enter search query" />
<div id="dropdown-menu-689868" class="dropdown-menu" data-align="end">
<button type="button" id="dropdown-menu-689868-trigger" aria-haspopup="menu" aria-controls="dropdown-menu-689868-menu" aria-expanded="false" class="btn-xs-ghost pr-1.5! text-xs">
<button type="button" id="dropdown-menu-689868-trigger" aria-haspopup="menu" aria-controls="dropdown-menu-689868-menu" aria-expanded="false" class="btn pr-1.5! text-xs" data-variant="ghost" data-size="xs">
Search In...
<svg class="size-3 lucide lucide-chevron-down" 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"><path d="m6 9 6 6 6-6" /></svg>
</button>
@@ -358,7 +318,7 @@ Use `data-align="start|end"` on addon wrappers to position them inline. Use `dat
<textarea placeholder="console.log('Hello, world!');"></textarea>
<footer data-align="end" class="border-t">
<span>Line 1, Column 1</span>
<button type="button" class="btn-sm ml-auto">
<button type="button" class="btn ml-auto" data-size="sm">
Run
<svg class="lucide lucide-corner-down-left" 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"><path d="M20 4v7a4 4 0 0 1-4 4H4" /><path d="m9 10-5 5 5 5" /></svg>
</button>
@@ -367,10 +327,10 @@ Use `data-align="start|end"` on addon wrappers to position them inline. Use `dat
<svg class="text-muted-foreground lucide lucide-file-code" 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"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /><path d="M14 2v5a1 1 0 0 0 1 1h5" /><path d="M10 12.5 8 15l2 2.5" /><path d="m14 12.5 2 2.5-2 2.5" /></svg>
<span>script.js</span>
<div role="group" aria-label="File actions" class="ml-auto flex gap-1">
<button type="button" class="btn-xs-icon-ghost">
<button type="button" class="btn" data-variant="ghost" data-size="icon-xs" aria-label="Refresh file">
<svg class="lucide lucide-refresh-ccw" 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"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" /><path d="M3 3v5h5" /><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" /><path d="M16 16h5v5" /></svg>
</button>
<button type="button" class="btn-xs-icon-ghost">
<button type="button" class="btn" data-variant="ghost" data-size="icon-xs" aria-label="Copy file">
<svg class="lucide lucide-copy" 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"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg>
</button>
</div>
@@ -384,7 +344,7 @@ Use `data-align="start|end"` on addon wrappers to position them inline. Use `dat
<div class="input-group" data-orientation="vertical">
<textarea data-control placeholder="Autoresize textarea..." class="field-sizing-content min-h-16 resize-none"></textarea>
<footer data-align="end">
<button type="button" class="btn-sm ml-auto">Submit</button>
<button type="button" class="btn ml-auto" data-size="sm">Submit</button>
</footer>
</div>
</Preview>
@@ -419,7 +379,7 @@ Use `data-align="start|end"` on addon wrappers to position them inline. Use `dat
<textarea id="rtl-textarea" placeholder="اكتب تعليقًا..."></textarea>
<footer data-align="end">
<span>٠/٢٨٠</span>
<button type="button" class="btn-sm ms-auto">نشر</button>
<button type="button" class="btn ms-auto" data-size="sm">نشر</button>
</footer>
</div>
<p>تذييل موضع أسفل منطقة النص.</p>
+4 -4
View File
@@ -6,7 +6,7 @@
## Usage
Add `input` to supported `<input>` elements. Inputs inside a `form` or `field` container also receive the same styling automatically for common text-like input types.
Add `class="input"` to supported `<input>` elements. Inputs inside a `class="field"` container also receive the same styling automatically for common text-like input types.
Use native HTML attributes for behavior: `disabled` for disabled inputs, `required` for required fields, and `aria-invalid="true"` for invalid state styling.
@@ -103,9 +103,9 @@ Use `field` with a label and description to create an accessible field.
<Preview class="w-full max-w-xs">
<div role="group" class="field w-full max-w-xs">
<label for="input-badge" class="flex items-center gap-2">
<label for="input-badge" class="flex w-full items-center gap-2">
Email
<span class="badge-secondary">Recommended</span>
<span class="badge ml-auto" data-variant="secondary">Recommended</span>
</label>
<input id="input-badge" type="email" placeholder="m@example.com" />
</div>
@@ -131,7 +131,7 @@ Use [Button Group](/components/button-group/) when an input should be grouped wi
<Preview class="w-full max-w-xs">
<div role="group" aria-label="Search" class="button-group w-full max-w-xs">
<input class="input" type="search" placeholder="Search..." />
<button type="submit" class="btn-icon-outline" aria-label="Search">
<button type="submit" class="btn" data-variant="outline" data-size="icon" aria-label="Search">
<svg class="lucide lucide-search" 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" ><path d="m21 21-4.34-4.34" /><circle cx="11" cy="11" r="8" /></svg>
</button>
</div>
+204 -116
View File
@@ -1,20 +1,33 @@
# Item
<Preview class="w-full max-w-md">
<article class="item" data-variant="outline">
<section>
<h3>Basic Item</h3>
<p>A simple item with title and description.</p>
</section>
<aside>
<button class="btn-sm-outline">Action</button>
</aside>
</article>
<div class="flex w-full flex-col gap-6">
<article class="item" data-variant="outline">
<section>
<h3>Basic Item</h3>
<p>A simple item with title and description.</p>
</section>
<aside>
<button class="btn" data-variant="outline" data-size="sm">Action</button>
</aside>
</article>
<a href="#" class="item" data-variant="outline" data-size="sm">
<figure>
<svg class="lucide lucide-badge-check size-5" 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"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg>
</figure>
<section>
<h3>Your profile has been verified.</h3>
</section>
<aside>
<svg class="lucide lucide-chevron-right size-4" 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"><path d="m9 18 6-6-6-6"/></svg>
</aside>
</a>
</div>
</Preview>
## Usage
Use `.item` on a semantic element such as `<article>` or `<a>`. Use `.item-group` when listing multiple items.
Add `class="item"` to a semantic element such as `<article>` or `<a>`. Use `class="item-group"` when listing multiple items.
```html
<article class="item" data-variant="outline">
@@ -23,7 +36,7 @@ Use `.item` on a semantic element such as `<article>` or `<a>`. Use `.item-group
<p>A simple item with title and description.</p>
</section>
<aside>
<button class="btn-sm-outline">Action</button>
<button class="btn" data-variant="outline" data-size="sm">Action</button>
</aside>
</article>
```
@@ -31,29 +44,29 @@ Use `.item` on a semantic element such as `<article>` or `<a>`. Use `.item-group
#### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;article class="item"&gt;</code> or <code class="highlight language-html">&lt;a class="item"&gt;</code></dt>
<dt><code>&lt;article class="item"&gt;</code> or <code>&lt;a class="item"&gt;</code></dt>
<dd>
The item root. Use <code>data-variant="outline|muted"</code> and <code>data-size="sm|xs"</code> when needed.
<dl>
<dt><code class="highlight language-html">&lt;figure&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;figure&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Media: icon, avatar, or image.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code></dt>
<dt><code>&lt;section&gt;</code></dt>
<dd>
Primary content. Headings and paragraphs are styled automatically.
<dl>
<dt><code class="highlight language-html">&lt;h2&gt;</code>, <code class="highlight language-html">&lt;h3&gt;</code>, or <code class="highlight language-html">&lt;h4&gt;</code></dt>
<dt><code>&lt;h2&gt;</code>, <code>&lt;h3&gt;</code>, or <code>&lt;h4&gt;</code></dt>
<dd>The item title.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;p&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The item description.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;aside&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;aside&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Actions or trailing metadata.</dd>
<dt><code class="highlight language-html">&lt;header&gt;</code>, <code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;header&gt;</code>, <code>&lt;footer&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Full-width item regions.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;div class="item-group" role="list"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;div class="item-group" role="list"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Groups related items. Use <code>role="listitem"</code> on child items when they are not native list items.</dd>
</dl>
@@ -62,27 +75,27 @@ Use `.item` on a semantic element such as `<article>` or `<a>`. Use `.item-group
### Variants
<Preview class="w-full max-w-md">
<div class="item-group" role="list">
<article class="item" role="listitem">
<div class="flex w-full flex-col gap-6">
<article class="item">
<section>
<h3>Default Variant</h3>
<p>Transparent border for quiet item rows.</p>
</section>
<aside><button class="btn-sm-outline">Open</button></aside>
<aside><button class="btn" data-variant="outline" data-size="sm">Open</button></aside>
</article>
<article class="item" data-variant="outline" role="listitem">
<article class="item" data-variant="outline">
<section>
<h3>Outline Variant</h3>
<p>Outlined style with a visible border.</p>
</section>
<aside><button class="btn-sm-outline">Open</button></aside>
<aside><button class="btn" data-variant="outline" data-size="sm">Open</button></aside>
</article>
<article class="item" data-variant="muted" role="listitem">
<article class="item" data-variant="muted">
<section>
<h3>Muted Variant</h3>
<p>Subdued appearance for secondary content.</p>
</section>
<aside><button class="btn-sm-outline">Open</button></aside>
<aside><button class="btn" data-variant="outline" data-size="sm">Open</button></aside>
</article>
</div>
</Preview>
@@ -90,125 +103,200 @@ Use `.item` on a semantic element such as `<article>` or `<a>`. Use `.item-group
### Size
<Preview class="w-full max-w-md">
<div class="item-group" role="list">
<article class="item" data-variant="outline" role="listitem">
<section><h3>Default</h3><p>Default item size.</p></section>
</article>
<article class="item" data-variant="outline" data-size="sm" role="listitem">
<section><h3>Small</h3><p>Small item size.</p></section>
</article>
<article class="item" data-variant="outline" data-size="xs" role="listitem">
<section><h3>Extra Small</h3><p>Extra small item size.</p></section>
<div class="flex w-full flex-col gap-6">
<article class="item" data-variant="outline">
<section><h3>Basic Item</h3><p>A simple item with title and description.</p></section>
<aside><button class="btn" data-variant="outline" data-size="sm">Action</button></aside>
</article>
<a href="#" class="item" data-variant="outline" data-size="sm">
<figure>
<svg class="lucide lucide-badge-check size-5" 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"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg>
</figure>
<section><h3>Your profile has been verified.</h3></section>
<aside><svg class="lucide lucide-chevron-right size-4" 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"><path d="m9 18 6-6-6-6"/></svg></aside>
</a>
</div>
</Preview>
### Icon
<Preview class="w-full max-w-lg">
<article class="item" data-variant="outline">
<figure>
<svg class="lucide lucide-shield-alert" 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" ><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /><path d="M12 8v4" /><path d="M12 16h.01" /></svg>
</figure>
<section>
<h3>Security Alert</h3>
<p>New login detected from an unknown device.</p>
</section>
<aside><button class="btn-sm-outline">Review</button></aside>
</article>
<div class="flex w-full flex-col gap-6">
<article class="item" data-variant="outline">
<figure>
<svg class="lucide lucide-shield-alert" 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"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>
</figure>
<section>
<h3>Security Alert</h3>
<p>New login detected from unknown device.</p>
</section>
<aside><button class="btn" data-variant="outline" data-size="sm">Review</button></aside>
</article>
</div>
</Preview>
### Avatar
<Preview class="w-full max-w-md">
<article class="item" data-variant="outline">
<figure>
<img src="https://github.com/hunvreus.png" alt="hunvreus" />
</figure>
<section>
<h3>hunvreus</h3>
<p>Last seen 5 months ago.</p>
</section>
<aside>
<button class="btn-icon-outline rounded-full">
<svg class="lucide lucide-plus" 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" ><path d="M5 12h14" /><path d="M12 5v14" /></svg>
</button>
</aside>
</article>
<Preview class="w-full max-w-lg">
<div class="flex w-full flex-col gap-6">
<article class="item" data-variant="outline">
<figure>
<span class="avatar" data-size="lg"><img src="https://github.com/evilrabbit.png" alt="Evil Rabbit" /></span>
</figure>
<section>
<h3>Evil Rabbit</h3>
<p>Last seen 5 months ago</p>
</section>
<aside>
<button class="btn rounded-full" data-variant="outline" data-size="icon-sm" aria-label="Invite Evil Rabbit">
<svg class="lucide lucide-plus" 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"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
</button>
</aside>
</article>
<article class="item" data-variant="outline">
<figure>
<span class="avatar-group">
<span class="avatar hidden sm:flex"><img class="grayscale" src="https://github.com/shadcn.png" alt="@shadcn" /></span>
<span class="avatar hidden sm:flex"><img class="grayscale" src="https://github.com/maxleiter.png" alt="@maxleiter" /></span>
<span class="avatar"><img class="grayscale" src="https://github.com/evilrabbit.png" alt="@evilrabbit" /></span>
</span>
</figure>
<section>
<h3>No Team Members</h3>
<p>Invite your team to collaborate on this project.</p>
</section>
<aside><button class="btn" data-variant="outline" data-size="sm">Invite</button></aside>
</article>
</div>
</Preview>
### Image
<Preview class="w-full max-w-md">
<a href="#" class="item" data-variant="outline">
<figure>
<img src="https://avatar.vercel.sh/example_1" alt="Midnight City Lights" />
</figure>
<section>
<h3>Midnight City Lights</h3>
<p>Neon Dreams</p>
</section>
<section>
<p>3:45</p>
</section>
</a>
<div class="item-group gap-4" role="list">
<a href="#" class="item" data-variant="outline" role="listitem">
<figure><img class="grayscale" src="https://avatar.vercel.sh/Midnight City Lights" alt="Midnight City Lights" /></figure>
<section><h3>Midnight City Lights - <span class="text-muted-foreground">Electric Nights</span></h3><p>Neon Dreams</p></section>
<section><p>3:45</p></section>
</a>
<a href="#" class="item" data-variant="outline" role="listitem">
<figure><img class="grayscale" src="https://avatar.vercel.sh/Coffee Shop Conversations" alt="Coffee Shop Conversations" /></figure>
<section><h3>Coffee Shop Conversations - <span class="text-muted-foreground">Urban Stories</span></h3><p>The Morning Brew</p></section>
<section><p>4:05</p></section>
</a>
<a href="#" class="item" data-variant="outline" role="listitem">
<figure><img class="grayscale" src="https://avatar.vercel.sh/Digital Rain" alt="Digital Rain" /></figure>
<section><h3>Digital Rain - <span class="text-muted-foreground">Binary Beats</span></h3><p>Cyber Symphony</p></section>
<section><p>3:30</p></section>
</a>
</div>
</Preview>
### Group
<Preview class="w-full max-w-md">
<div class="item-group" role="list">
<article class="item" role="listitem">
<figure><img src="https://github.com/hunvreus.png" alt="hunvreus" /></figure>
<section><h3>hunvreus</h3><p>hunvreus@gmail.com</p></section>
<aside>
<button class="btn-icon-outline rounded-full">
<svg class="lucide lucide-plus" 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" ><path d="M5 12h14" /><path d="M12 5v14" /></svg>
</button>
</aside>
</article>
<hr role="separator" />
<article class="item" role="listitem">
<figure><img src="https://github.com/shadcn.png" alt="shadcn" /></figure>
<section><h3>shadcn</h3><p>shadcn@vercel.com</p></section>
<aside>
<button class="btn-icon-outline rounded-full">
<svg class="lucide lucide-plus" 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" ><path d="M5 12h14" /><path d="M12 5v14" /></svg>
</button>
</aside>
</article>
<div class="flex w-full flex-col gap-6">
<div class="item-group" role="list">
<article class="item" role="listitem">
<figure><span class="avatar"><img class="grayscale" src="https://github.com/shadcn.png" alt="@shadcn" /></span></figure>
<section><h3>shadcn</h3><p>shadcn@vercel.com</p></section>
<aside>
<button class="btn rounded-full" data-variant="ghost" data-size="icon" aria-label="Add shadcn">
<svg class="lucide lucide-plus" 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"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
</button>
</aside>
</article>
<hr role="separator" />
<article class="item" role="listitem">
<figure><span class="avatar"><img class="grayscale" src="https://github.com/maxleiter.png" alt="@maxleiter" /></span></figure>
<section><h3>maxleiter</h3><p>maxleiter@vercel.com</p></section>
<aside>
<button class="btn rounded-full" data-variant="ghost" data-size="icon" aria-label="Add maxleiter">
<svg class="lucide lucide-plus" 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"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
</button>
</aside>
</article>
<hr role="separator" />
<article class="item" role="listitem">
<figure><span class="avatar"><img class="grayscale" src="https://github.com/evilrabbit.png" alt="@evilrabbit" /></span></figure>
<section><h3>evilrabbit</h3><p>evilrabbit@vercel.com</p></section>
<aside>
<button class="btn rounded-full" data-variant="ghost" data-size="icon" aria-label="Add evilrabbit">
<svg class="lucide lucide-plus" 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"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
</button>
</aside>
</article>
</div>
</div>
</Preview>
### Header
<Preview class="w-full max-w-md">
<article class="item" data-variant="outline">
<header>
<h3>v0-1.5-sm</h3>
<span class="badge-outline">Model</span>
</header>
<figure>
<img src="https://avatar.vercel.sh/v0-1.5-sm" alt="v0-1.5-sm" />
</figure>
<section>
<p>Everyday tasks and UI generation.</p>
</section>
</article>
<Preview class="w-full max-w-xl">
<div class="item-group grid grid-cols-1 gap-4 sm:grid-cols-3" role="list">
<article class="item" data-variant="outline" role="listitem">
<header><img class="aspect-square w-full rounded-sm object-cover grayscale" src="https://images.unsplash.com/photo-1650804068570-7fb2e3dbf888?q=80&w=640&auto=format&fit=crop" alt="v0-1.5-sm" /></header>
<section><h3>v0-1.5-sm</h3><p>Everyday tasks and UI generation.</p></section>
</article>
<article class="item" data-variant="outline" role="listitem">
<header><img class="aspect-square w-full rounded-sm object-cover grayscale" src="https://images.unsplash.com/photo-1610280777472-54133d004c8c?q=80&w=640&auto=format&fit=crop" alt="v0-1.5-lg" /></header>
<section><h3>v0-1.5-lg</h3><p>Advanced thinking or reasoning.</p></section>
</article>
<article class="item" data-variant="outline" role="listitem">
<header><img class="aspect-square w-full rounded-sm object-cover grayscale" src="https://images.unsplash.com/photo-1602146057681-08560aee8cde?q=80&w=640&auto=format&fit=crop" alt="v0-2.0-mini" /></header>
<section><h3>v0-2.0-mini</h3><p>Open Source model for everyone.</p></section>
</article>
</div>
</Preview>
### Link
<Preview class="w-full max-w-md">
<a href="#" class="item">
<section>
<h3>Visit our documentation</h3>
<p>Learn how to get started with our components.</p>
</section>
<aside>
<svg class="lucide lucide-chevron-right" 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" ><path d="m9 18 6-6-6-6" /></svg>
</aside>
</a>
<div class="flex w-full flex-col gap-4">
<a href="#" class="item">
<section>
<h3>Visit our documentation</h3>
<p>Learn how to get started with our components.</p>
</section>
<aside><svg class="lucide lucide-chevron-right size-4" 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"><path d="m9 18 6-6-6-6"/></svg></aside>
</a>
<a href="#" target="_blank" rel="noopener noreferrer" class="item" data-variant="outline">
<section>
<h3>External resource</h3>
<p>Opens in a new tab with security attributes.</p>
</section>
<aside><svg class="lucide lucide-external-link size-4" 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"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg></aside>
</a>
</div>
</Preview>
### Dropdown
<Preview class="w-full max-w-md">
<div class="flex min-h-64 w-full flex-col items-center gap-6">
<div class="dropdown-menu">
<button class="btn w-fit" data-variant="outline" data-size="sm" popovertarget="item-dropdown-menu">
Select
<svg class="lucide lucide-chevron-down" data-icon="inline-end" 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"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div id="item-dropdown-menu" popover data-popover class="w-72 [--radius:0.65rem]">
<div role="menu">
<button type="button" role="menuitem" class="item w-full p-2" data-size="sm">
<figure><span class="avatar"><img class="grayscale" src="https://github.com/shadcn.png" alt="@shadcn" /></span></figure>
<section class="gap-0.5"><h3>shadcn</h3><p>shadcn@vercel.com</p></section>
</button>
<button type="button" role="menuitem" class="item w-full p-2" data-size="sm">
<figure><span class="avatar"><img class="grayscale" src="https://github.com/maxleiter.png" alt="@maxleiter" /></span></figure>
<section class="gap-0.5"><h3>maxleiter</h3><p>maxleiter@vercel.com</p></section>
</button>
<button type="button" role="menuitem" class="item w-full p-2" data-size="sm">
<figure><span class="avatar"><img class="grayscale" src="https://github.com/evilrabbit.png" alt="@evilrabbit" /></span></figure>
<section class="gap-0.5"><h3>evilrabbit</h3><p>evilrabbit@vercel.com</p></section>
</button>
</div>
</div>
</div>
</div>
</Preview>
### RTL
@@ -216,12 +304,12 @@ Use `.item` on a semantic element such as `<article>` or `<a>`. Use `.item-group
<Preview class="w-full max-w-lg">
<article class="item" data-variant="outline" dir="rtl">
<figure>
<svg class="lucide lucide-shield-alert" 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" ><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /><path d="M12 8v4" /><path d="M12 16h.01" /></svg>
<svg class="lucide lucide-shield-alert" 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"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>
</figure>
<section>
<h3>تنبيه أمني</h3>
<p>تم اكتشاف تسجيل دخول من جهاز غير معروف.</p>
</section>
<aside><button class="btn-sm-outline">مراجعة</button></aside>
<aside><button class="btn" data-variant="outline" data-size="sm">مراجعة</button></aside>
</article>
</Preview>
+7 -7
View File
@@ -2,13 +2,13 @@
<Preview>
<div class="flex flex-col items-center gap-4">
<span class="kbd-group">
<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="kbd-group">
<span class="inline-flex items-center gap-1">
<kbd class="kbd">Ctrl</kbd>
<span>+</span>
<kbd class="kbd">B</kbd>
@@ -18,7 +18,7 @@
## Usage
Use `<kbd class="kbd">` for a key label. Use `kbd-group` to group multiple keys.
Use `<kbd class="kbd">` for a key label. Use an inline wrapper with utilities when grouping multiple keys.
```html
<kbd class="kbd">⌘K</kbd>
@@ -33,7 +33,7 @@ Keyboard labels are direction-neutral. Surrounding spacing should use logical ut
<Preview>
<p class="text-muted-foreground text-sm">
Use
<span class="kbd-group">
<span class="inline-flex items-center gap-1">
<kbd class="kbd">Ctrl + B</kbd>
<kbd class="kbd">Ctrl + K</kbd>
</span>
@@ -45,11 +45,11 @@ Keyboard labels are direction-neutral. Surrounding spacing should use logical ut
<Preview>
<div class="flex flex-wrap items-center gap-x-4">
<button class="btn-sm-outline pe-2">
<button class="btn pe-2" data-variant="outline" data-size="sm">
Accept
<kbd class="kbd">⏎</kbd>
</button>
<button class="btn-sm-outline pe-2">
<button class="btn pe-2" data-variant="outline" data-size="sm">
Cancel
<kbd class="kbd">Esc</kbd>
</button>
@@ -73,7 +73,7 @@ Keyboard labels are direction-neutral. Surrounding spacing should use logical ut
<Preview>
<p dir="rtl" class="text-muted-foreground text-sm">
استخدم
<span class="kbd-group">
<span class="inline-flex items-center gap-1">
<kbd class="kbd">Ctrl</kbd>
<kbd class="kbd">K</kbd>
</span>
+2 -2
View File
@@ -1,7 +1,7 @@
# Label
<Preview>
<label class="label gap-3">
<label class="label gap-2">
<input type="checkbox" class="input" />
Accept terms and conditions
</label>
@@ -9,7 +9,7 @@
## Usage
Add `label` to a `<label>` element.
Add `class="label"` to a `<label>` element.
```html
<label class="label" for="email">Your email address</label>
+15 -17
View File
@@ -1,7 +1,7 @@
# Native Select
<Preview>
<select class="select w-[180px]" aria-label="Fruit">
<Preview class="w-full max-w-48">
<select class="select w-full" aria-label="Fruit">
<option value="">Select a fruit</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
@@ -10,14 +10,12 @@
</select>
</Preview>
For a custom listbox with richer styling and JavaScript behavior, see [Select](/components/select).
## Usage
Add `select` to a native `<select>`. Use standard `<option>` and `<optgroup>` elements.
Add `class="select"` to a native `<select>`. Use standard `<option>` and `<optgroup>` elements.
```html
<select class="select w-[180px]" aria-label="Fruit">
<select class="select" aria-label="Fruit">
<option value="">Select a fruit</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
@@ -26,14 +24,14 @@ Add `select` to a native `<select>`. Use standard `<option>` and `<optgroup>` el
</select>
```
Use **Native Select** for native browser behavior, form integration, and mobile-optimized pickers. Use [Select](/components/select/) for custom option content, multi-select behavior, or JavaScript APIs.
Use Native Select for native browser behavior, form integration, and mobile-optimized pickers. Use [Select](/components/select/) for custom option content, multi-select behavior, or JavaScript APIs.
## Examples
### Groups
<Preview>
<select class="select w-[220px]" aria-label="Food">
<Preview class="w-full max-w-48">
<select class="select w-full" aria-label="Food">
<option value="">Select food</option>
<optgroup label="Fruits">
<option value="apple">Apple</option>
@@ -50,8 +48,8 @@ Use **Native Select** for native browser behavior, form integration, and mobile-
### Disabled
<Preview>
<select class="select w-[220px]" aria-label="Fruit" disabled>
<Preview class="w-full max-w-48">
<select class="select w-full" aria-label="Fruit" disabled>
<option value="">Select a fruit</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
@@ -60,7 +58,7 @@ Use **Native Select** for native browser behavior, form integration, and mobile-
### Invalid
<Preview class="w-full max-w-sm">
<Preview class="w-full max-w-48">
<div role="group" class="field" data-invalid="true">
<label for="native-select-invalid">Fruit</label>
<select id="native-select-invalid" class="select w-full" aria-invalid="true" aria-describedby="native-select-invalid-error">
@@ -68,7 +66,7 @@ Use **Native Select** for native browser behavior, form integration, and mobile-
<option value="apple">Apple</option>
<option value="banana">Banana</option>
</select>
<p id="native-select-invalid-error" class="field-error">Select a fruit to continue.</p>
<p id="native-select-invalid-error" role="alert">Select a fruit to continue.</p>
</div>
</Preview>
@@ -76,12 +74,12 @@ Use **Native Select** for native browser behavior, form integration, and mobile-
<Preview>
<div class="flex flex-wrap items-center gap-3">
<select class="select w-[180px]" aria-label="Default size">
<select class="select w-42" aria-label="Default size">
<option>Default</option>
<option>Apple</option>
<option>Banana</option>
</select>
<select class="select w-[180px]" data-size="sm" aria-label="Small size">
<select class="select w-42" data-size="sm" aria-label="Small size">
<option>Small</option>
<option>Apple</option>
<option>Banana</option>
@@ -93,8 +91,8 @@ Use **Native Select** for native browser behavior, form integration, and mobile-
Set `dir="rtl"` on the select or a parent element.
<Preview>
<select dir="rtl" class="select w-[220px]" aria-label="الفاكهة">
<Preview class="w-full max-w-48">
<select dir="rtl" class="select w-full" aria-label="الفاكهة">
<option value="">اختر فاكهة</option>
<option value="apple">تفاح</option>
<option value="banana">موز</option>
+49 -10
View File
@@ -8,19 +8,19 @@
<nav role="navigation" aria-label="pagination" class="mx-auto flex w-full justify-center">
<ul class="flex flex-row items-center gap-1">
<li>
<a href="#" class="btn-ghost">
<a href="#" class="btn" data-variant="ghost">
<svg class="lucide lucide-chevron-left" 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" ><path d="m15 18-6-6 6-6" /></svg>
<span>Previous</span>
</a>
</li>
<li>
<a href="#" class="btn-icon-ghost">1</a>
<a href="#" class="btn" data-variant="ghost" data-size="icon">1</a>
</li>
<li>
<a href="#" class="btn-icon-outline">2</a>
<a href="#" class="btn" data-variant="outline" data-size="icon">2</a>
</li>
<li>
<a href="#" class="btn-icon-ghost">3</a>
<a href="#" class="btn" data-variant="ghost" data-size="icon">3</a>
</li>
<li>
<div class="size-9 flex items-center justify-center">
@@ -28,7 +28,7 @@
</div>
</li>
<li>
<a href="#" class="btn-ghost">
<a href="#" class="btn" data-variant="ghost">
<span>Next</span>
<svg class="lucide lucide-chevron-right" 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" ><path d="m9 18 6-6-6-6" /></svg>
</a>
@@ -45,19 +45,19 @@ A pagination is fairly simple to implement with Tailwind using the Basecoat butt
<nav role="navigation" aria-label="pagination" class="mx-auto flex w-full justify-center">
<ul class="flex flex-row items-center gap-1">
<li>
<a href="#" class="btn-ghost">
<a href="#" class="btn" data-variant="ghost">
<svg class="lucide lucide-chevron-left" 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" ><path d="m15 18-6-6 6-6" /></svg>
Previous
</a>
</li>
<li>
<a href="#" class="btn-icon-ghost">1</a>
<a href="#" class="btn" data-variant="ghost" data-size="icon">1</a>
</li>
<li>
<a href="#" class="btn-icon-outline">2</a>
<a href="#" class="btn" data-variant="outline" data-size="icon">2</a>
</li>
<li>
<a href="#" class="btn-icon-ghost">3</a>
<a href="#" class="btn" data-variant="ghost" data-size="icon">3</a>
</li>
<li>
<div class="size-9 flex items-center justify-center">
@@ -65,7 +65,7 @@ A pagination is fairly simple to implement with Tailwind using the Basecoat butt
</div>
</li>
<li>
<a href="#" class="btn-ghost">
<a href="#" class="btn" data-variant="ghost">
Next
<svg class="lucide lucide-chevron-right" 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" ><path d="m9 18 6-6-6-6" /></svg>
</a>
@@ -73,3 +73,42 @@ A pagination is fairly simple to implement with Tailwind using the Basecoat butt
</ul>
</nav>
```
## Examples
### RTL
Pagination uses document direction and logical spacing. Set `dir="rtl"` on the pagination root or a parent.
<Preview class="max-w-md">
<nav dir="rtl" role="navigation" aria-label="ترقيم الصفحات" class="mx-auto flex w-full justify-center">
<ul class="flex flex-row items-center gap-1">
<li>
<a href="#" class="btn" data-variant="ghost">
<svg class="lucide lucide-chevron-right" 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" ><path d="m9 18 6-6-6-6" /></svg>
<span>السابق</span>
</a>
</li>
<li>
<a href="#" class="btn" data-variant="ghost" data-size="icon">١</a>
</li>
<li>
<a href="#" class="btn" data-variant="outline" data-size="icon">٢</a>
</li>
<li>
<a href="#" class="btn" data-variant="ghost" data-size="icon">٣</a>
</li>
<li>
<div class="size-9 flex items-center justify-center">
<svg class="size-4 shrink-0 lucide lucide-ellipsis" 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" ><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></svg>
</div>
</li>
<li>
<a href="#" class="btn" data-variant="ghost">
<span>التالي</span>
<svg class="lucide lucide-chevron-left" 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" ><path d="m15 18-6-6 6-6" /></svg>
</a>
</li>
</ul>
</nav>
</Preview>
+47 -13
View File
@@ -2,7 +2,7 @@
<Preview>
<div id="demo-popover" class="popover">
<button id="demo-popover-trigger" type="button" aria-expanded="false" aria-controls="demo-popover-popover" class="btn-outline">Open popover</button>
<button id="demo-popover-trigger" type="button" aria-expanded="false" aria-controls="demo-popover-popover" class="btn" data-variant="outline">Open popover</button>
<div id="demo-popover-popover" data-popover aria-hidden="true" class="w-72">
<header>
<h4>Dimensions</h4>
@@ -42,8 +42,8 @@ Basecoat popovers are inline-positioned relative to the `.popover` wrapper. This
<Step title="Include the JavaScript files">
```html
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/popover.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/popover.min.js" defer></script>
```
</Step>
@@ -51,7 +51,7 @@ Basecoat popovers are inline-positioned relative to the `.popover` wrapper. This
```html
<div id="demo-popover" class="popover">
<button id="demo-popover-trigger" type="button" aria-expanded="false" aria-controls="demo-popover-popover" class="btn-outline">Open popover</button>
<button id="demo-popover-trigger" type="button" aria-expanded="false" aria-controls="demo-popover-popover" class="btn" data-variant="outline">Open popover</button>
<div id="demo-popover-popover" data-popover aria-hidden="true" class="w-72">
<header>
<h4>Dimensions</h4>
@@ -85,13 +85,13 @@ Basecoat popovers are inline-positioned relative to the `.popover` wrapper. This
### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;div class="popover"&gt;</code></dt>
<dt><code>&lt;div class="popover"&gt;</code></dt>
<dd>
Relative wrapper for the trigger and inline popover content.
<dl>
<dt><code class="highlight language-html">&lt;button type="button" aria-expanded="false" aria-controls="&#123; POPOVER_ID &#125;"&gt;</code></dt>
<dt><code>&lt;button type="button" aria-expanded="false" aria-controls="&#123; POPOVER_ID &#125;"&gt;</code></dt>
<dd>Trigger button. The script toggles <code>aria-expanded</code> and opens or closes the content.</dd>
<dt><code class="highlight language-html">&lt;div data-popover id="&#123; POPOVER_ID &#125;" aria-hidden="true"&gt;</code></dt>
<dt><code>&lt;div data-popover id="&#123; POPOVER_ID &#125;" aria-hidden="true"&gt;</code></dt>
<dd>Popover content. Supports <code>data-side="top|right|bottom|left"</code> and <code>data-align="start|center|end"</code>.</dd>
</dl>
</dd>
@@ -103,7 +103,7 @@ Basecoat popovers are inline-positioned relative to the `.popover` wrapper. This
<Preview>
<div id="popover-basic" class="popover">
<button id="popover-basic-trigger" type="button" aria-expanded="false" aria-controls="popover-basic-popover" class="btn-outline">Open</button>
<button id="popover-basic-trigger" type="button" aria-expanded="false" aria-controls="popover-basic-popover" class="btn" data-variant="outline">Open</button>
<div id="popover-basic-popover" data-popover aria-hidden="true" class="w-72">
<header>
<h4>Title</h4>
@@ -118,21 +118,21 @@ Basecoat popovers are inline-positioned relative to the `.popover` wrapper. This
<Preview>
<div class="flex flex-wrap gap-3">
<div id="popover-align-start" class="popover">
<button id="popover-align-start-trigger" type="button" aria-expanded="false" aria-controls="popover-align-start-popover" class="btn-outline">Start</button>
<button id="popover-align-start-trigger" type="button" aria-expanded="false" aria-controls="popover-align-start-popover" class="btn" data-variant="outline">Start</button>
<div id="popover-align-start-popover" data-popover aria-hidden="true" class="w-56" data-align="start">
<p>Aligned to start.</p>
</div>
</div>
<div id="popover-align-center" class="popover">
<button id="popover-align-center-trigger" type="button" aria-expanded="false" aria-controls="popover-align-center-popover" class="btn-outline">Center</button>
<button id="popover-align-center-trigger" type="button" aria-expanded="false" aria-controls="popover-align-center-popover" class="btn" data-variant="outline">Center</button>
<div id="popover-align-center-popover" data-popover aria-hidden="true" class="w-56" data-align="center">
<p>Aligned to center.</p>
</div>
</div>
<div id="popover-align-end" class="popover">
<button id="popover-align-end-trigger" type="button" aria-expanded="false" aria-controls="popover-align-end-popover" class="btn-outline">End</button>
<button id="popover-align-end-trigger" type="button" aria-expanded="false" aria-controls="popover-align-end-popover" class="btn" data-variant="outline">End</button>
<div id="popover-align-end-popover" data-popover aria-hidden="true" class="w-56" data-align="end">
<p>Aligned to end.</p>
</div>
@@ -140,11 +140,45 @@ Basecoat popovers are inline-positioned relative to the `.popover` wrapper. This
</div>
</Preview>
### Sides
<Preview>
<div class="flex flex-wrap gap-3">
<div id="popover-side-top" class="popover">
<button id="popover-side-top-trigger" type="button" aria-expanded="false" aria-controls="popover-side-top-popover" class="btn" data-variant="outline">Top</button>
<div id="popover-side-top-popover" data-popover aria-hidden="true" data-side="top" data-align="center" class="w-40">
<p>Top side.</p>
</div>
</div>
<div id="popover-side-right" class="popover">
<button id="popover-side-right-trigger" type="button" aria-expanded="false" aria-controls="popover-side-right-popover" class="btn" data-variant="outline">Right</button>
<div id="popover-side-right-popover" data-popover aria-hidden="true" data-side="right" data-align="center" class="w-40">
<p>Right side.</p>
</div>
</div>
<div id="popover-side-bottom" class="popover">
<button id="popover-side-bottom-trigger" type="button" aria-expanded="false" aria-controls="popover-side-bottom-popover" class="btn" data-variant="outline">Bottom</button>
<div id="popover-side-bottom-popover" data-popover aria-hidden="true" data-side="bottom" data-align="center" class="w-40">
<p>Bottom side.</p>
</div>
</div>
<div id="popover-side-left" class="popover">
<button id="popover-side-left-trigger" type="button" aria-expanded="false" aria-controls="popover-side-left-popover" class="btn" data-variant="outline">Left</button>
<div id="popover-side-left-popover" data-popover aria-hidden="true" data-side="left" data-align="center" class="w-40">
<p>Left side.</p>
</div>
</div>
</div>
</Preview>
### With Form
<Preview>
<div id="popover-form" class="popover">
<button id="popover-form-trigger" type="button" aria-expanded="false" aria-controls="popover-form-popover" class="btn-outline">Open popover</button>
<button id="popover-form-trigger" type="button" aria-expanded="false" aria-controls="popover-form-popover" class="btn" data-variant="outline">Open popover</button>
<div id="popover-form-popover" data-popover aria-hidden="true" class="w-72">
<header>
<h4>Dimensions</h4>
@@ -179,7 +213,7 @@ Popover alignment uses logical start and end offsets. Set `dir="rtl"` on a paren
<Preview>
<div dir="rtl">
<div id="popover-rtl" class="popover">
<button id="popover-rtl-trigger" type="button" aria-expanded="false" aria-controls="popover-rtl-popover" class="btn-outline">فتح</button>
<button id="popover-rtl-trigger" type="button" aria-expanded="false" aria-controls="popover-rtl-popover" class="btn" data-variant="outline">فتح</button>
<div id="popover-rtl-popover" data-popover aria-hidden="true" class="w-72" data-align="start">
<header>
<h4>العنوان</h4>
+40 -14
View File
@@ -1,26 +1,52 @@
# Progress
<Callout title="No dedicated component" icon="circle-alert">
There is no dedicated Progress component in Basecoat.
</Callout>
<Preview class="w-full max-w-sm">
<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 class="progress" role="progressbar" aria-label="Loading" aria-valuenow="66" aria-valuemin="0" aria-valuemax="100">
<span style="width: 66%"></span>
</div>
<script>
setTimeout(() => {
document.getElementById('demo-progress').style.width = '66%';
}, 500);
</script>
</Preview>
## Usage
Add `class="progress"` and `role="progressbar"` to the root. Use one child `<span>` as the visual indicator.
```html
<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 class="progress" role="progressbar" aria-label="Loading" aria-valuenow="66" aria-valuemin="0" aria-valuemax="100">
<span style="width: 66%"></span>
</div>
```
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.
### HTML structure
<dl>
<dt><code>&lt;div class="progress" role="progressbar"&gt;</code></dt>
<dd>Progress root. Set <code>aria-label</code> or <code>aria-labelledby</code>, plus <code>aria-valuenow</code>, <code>aria-valuemin</code>, and <code>aria-valuemax</code>.</dd>
<dt><code>&lt;span style="width: ..."&gt;</code></dt>
<dd>Visual indicator. Its width should match the current value.</dd>
</dl>
## Examples
### Label
<Preview class="w-full max-w-sm">
<div class="field">
<div class="flex items-center gap-3">
<label id="progress-label">Upload progress</label>
<output class="text-muted-foreground ms-auto text-sm tabular-nums">66%</output>
</div>
<div class="progress" role="progressbar" aria-labelledby="progress-label" aria-valuenow="66" aria-valuemin="0" aria-valuemax="100">
<span style="width: 66%"></span>
</div>
</div>
</Preview>
### RTL
Progress uses logical direction. Set `dir="rtl"` on the progress root or a parent to fill from the right.
<Preview class="w-full max-w-sm">
<div dir="rtl" class="progress" role="progressbar" aria-label="تحميل" aria-valuenow="66" aria-valuemin="0" aria-valuemax="100">
<span style="width: 66%"></span>
</div>
</Preview>
+20 -49
View File
@@ -18,34 +18,12 @@
## Usage
Add `input` to radio inputs. Use a shared `name` attribute for mutually exclusive options, and wrap related options in a semantic `<fieldset>` or an element with `role="radiogroup"`.
Add `class="input"` to radio inputs. Use a shared `name` attribute for mutually exclusive options, and wrap related options in a semantic `<fieldset>` or an element with `role="radiogroup"`.
```html
<input type="radio" name="plan" class="input" />
```
## Checked state
Use native HTML state. Add `checked` for the initially selected option, or read and update the element's `checked` property in JavaScript.
```html
<input type="radio" name="plan" class="input" checked />
```
## Invalid state
Set `aria-invalid="true"` on the invalid radio and, when using `field`, set `data-invalid="true"` on the field wrapper.
<Preview class="w-full max-w-sm">
<div role="group" class="field flex-row items-start gap-3" data-invalid="true">
<input type="radio" id="radio-invalid" name="radio-invalid" class="input" aria-invalid="true" />
<div class="grid gap-1.5">
<label for="radio-invalid">I agree to the terms</label>
<p class="field-error">You must select an option to continue.</p>
</div>
</div>
</Preview>
## Examples
### Basic
@@ -73,17 +51,17 @@ Set `aria-invalid="true"` on the invalid radio and, when using `field`, set `dat
<div role="radiogroup" aria-label="Notifications" data-slot="radio-group">
<div role="group" class="field flex-row items-start gap-3">
<input type="radio" id="notify-all" name="notifications" value="all" class="input" checked aria-describedby="notify-all-description" />
<div class="grid gap-1.5">
<section>
<label for="notify-all">All new messages</label>
<p id="notify-all-description" class="field-description">Receive a notification for every new message.</p>
</div>
<p id="notify-all-description">Receive a notification for every new message.</p>
</section>
</div>
<div role="group" class="field flex-row items-start gap-3">
<input type="radio" id="notify-mentions" name="notifications" value="mentions" class="input" aria-describedby="notify-mentions-description" />
<div class="grid gap-1.5">
<section>
<label for="notify-mentions">Direct messages and mentions</label>
<p id="notify-mentions-description" class="field-description">Only notify me for direct messages and mentions.</p>
</div>
<p id="notify-mentions-description">Only notify me for direct messages and mentions.</p>
</section>
</div>
</div>
</Preview>
@@ -96,19 +74,19 @@ Wrap a `.field` in a native `<label>` when the whole card should select the opti
<div role="radiogroup" aria-label="Plans" data-slot="radio-group" class="w-full max-w-md">
<label>
<div role="group" class="field flex-row items-center justify-between">
<div class="grid gap-1.5">
<section>
<span>Starter</span>
<span class="field-description">For small teams getting started.</span>
</div>
<span>For small teams getting started.</span>
</section>
<input type="radio" name="choice-plan" value="starter" class="input" checked />
</div>
</label>
<label>
<div role="group" class="field flex-row items-center justify-between">
<div class="grid gap-1.5">
<section>
<span>Team</span>
<span class="field-description">Collaboration tools for growing teams.</span>
</div>
<span>Collaboration tools for growing teams.</span>
</section>
<input type="radio" name="choice-plan" value="team" class="input" />
</div>
</label>
@@ -155,20 +133,13 @@ Wrap a `.field` in a native `<label>` when the whole card should select the opti
### Invalid
<Preview class="w-full max-w-sm">
<fieldset class="fieldset" data-invalid="true">
<legend>Plan</legend>
<div role="radiogroup" aria-label="Invalid plan" data-slot="radio-group" aria-describedby="radio-group-error">
<div role="group" class="field flex-row items-center gap-3" data-invalid="true">
<input type="radio" id="radio-invalid-free" name="radio-invalid-plan" class="input" aria-invalid="true" />
<label for="radio-invalid-free">Free</label>
</div>
<div role="group" class="field flex-row items-center gap-3" data-invalid="true">
<input type="radio" id="radio-invalid-pro" name="radio-invalid-plan" class="input" aria-invalid="true" />
<label for="radio-invalid-pro">Pro</label>
</div>
</div>
<p id="radio-group-error" class="field-error">Select a plan to continue.</p>
</fieldset>
<div role="group" class="field flex-row items-start gap-3" data-invalid="true">
<input type="radio" id="radio-invalid" name="radio-invalid" class="input" aria-invalid="true" aria-describedby="radio-invalid-error" />
<section>
<label for="radio-invalid">I agree to the terms</label>
<p id="radio-invalid-error" role="alert">You must select an option to continue.</p>
</section>
</div>
</Preview>
### RTL
+6 -6
View File
@@ -1,7 +1,7 @@
# Scroll Area
<Preview class="w-full">
<div class="scrollbar h-72 w-full max-w-sm overflow-y-auto rounded-md border p-4">
<Preview class="w-full max-w-42">
<div class="scrollbar h-72 overflow-y-auto rounded-md border p-4">
<div class="space-y-4">
<h4 class="text-sm leading-none font-medium">Tags</h4>
<div class="text-sm">
@@ -43,7 +43,7 @@ The utility augments native scrolling for cross-browser styling. It does not wra
Use `scrollbar` with `overflow-x-auto` for horizontal scrolling.
<Preview class="w-full">
<Preview>
<div class="scrollbar w-full max-w-sm overflow-x-auto rounded-md border">
<div class="flex w-max gap-4 p-4">
<figure class="shrink-0">
@@ -66,7 +66,7 @@ Use `scrollbar` with `overflow-x-auto` for horizontal scrolling.
Use `scrollbar-thin` for compact scrollable regions like menus, listboxes, sidebars, and command results.
<Preview class="w-full">
<Preview class="w-full max-w-64">
<div class="w-full max-w-sm rounded-md border p-1">
<div class="px-2 py-1.5 text-xs font-medium text-muted-foreground">Recent files</div>
<div class="scrollbar-thin max-h-48 overflow-y-auto">
@@ -106,8 +106,8 @@ Use `scrollbar-thin` for compact scrollable regions like menus, listboxes, sideb
Set `dir="rtl"` on the scrollable element or one of its ancestors to use right-to-left layout.
<Preview class="w-full">
<div dir="rtl" class="scrollbar h-72 w-full max-w-sm overflow-y-auto rounded-md border p-4">
<Preview class="w-full max-w-42">
<div dir="rtl" class="scrollbar h-72 overflow-y-auto rounded-md border p-4">
<div class="space-y-4">
<h4 class="text-sm leading-none font-medium">العلامات</h4>
<div class="text-sm">
+35 -35
View File
@@ -27,8 +27,8 @@
<Step title="Include the JavaScript files">
```html
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/select.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/select.min.js" defer></script>
```
</Step>
@@ -57,33 +57,33 @@
### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;div class="select"&gt;</code></dt>
<dt><code>&lt;div class="select"&gt;</code></dt>
<dd>
Select root. Supports <code>data-placeholder</code>, <code>data-close-on-select="true"</code>, and <code>data-format="object"</code>.
<dl>
<dt><code class="highlight language-html">&lt;button type="button"&gt;</code></dt>
<dt><code>&lt;button type="button"&gt;</code></dt>
<dd>Trigger button. Use <code>aria-haspopup="listbox"</code>, <code>aria-expanded</code>, and <code>aria-controls</code>.</dd>
<dt><code class="highlight language-html">&lt;div data-popover aria-hidden="true"&gt;</code></dt>
<dt><code>&lt;div data-popover aria-hidden="true"&gt;</code></dt>
<dd>
Popover content. Supports <code>data-side</code> and <code>data-align</code> through the shared popover rules.
<dl>
<dt><code class="highlight language-html">&lt;div role="listbox"&gt;</code></dt>
<dt><code>&lt;div role="listbox"&gt;</code></dt>
<dd>
Options container. Add <code>aria-multiselectable="true"</code> for multiple selection.
<dl>
<dt><code class="highlight language-html">&lt;div role="option" data-value="..."&gt;</code></dt>
<dd>Selectable option. Use <code>data-label</code> when selected text should differ from rendered content.</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;div role="option" data-value="..." data-label="..."&gt;</code></dt>
<dd>Selectable option. <code>data-value</code> is submitted. <code>data-label</code> is optional and controls the selected display label and <code>data-format="object"</code> label; when omitted, Basecoat uses trimmed text content.</dd>
<dt><code>&lt;hr role="separator"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Separator between groups or options.</dd>
<dt><code class="highlight language-html">&lt;div role="group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;div role="group"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Option group. Use <code>aria-labelledby</code> to connect it to its heading.</dd>
<dt><code class="highlight language-html">&lt;span role="heading"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;span role="heading"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Group heading.</dd>
</dl>
</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;input type="hidden"&gt;</code></dt>
<dt><code>&lt;input type="hidden"&gt;</code></dt>
<dd>Submitted value. Single select stores a string; multiselect stores a JSON array. With <code>data-format="object"</code>, values are serialized as <code>&#123; value, label &#125;</code> objects.</dd>
</dl>
</dd>
@@ -109,7 +109,7 @@ Use `data-align` on the popover to align the list to the trigger edge. Basecoat
<span class="truncate">Light</span>
<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-chevron-down-icon lucide-chevron-down text-muted-foreground opacity-50 shrink-0"><path d="m6 9 6 6 6-6" /></svg>
</button>
<div id="select-align-start-popover" data-popover aria-hidden="true" data-align="start">
<div id="select-align-start-popover" data-popover aria-hidden="true" data-align="start" class="w-72">
<div role="listbox" id="select-align-start-listbox" aria-orientation="vertical" aria-labelledby="select-align-start-trigger">
<div id="select-align-start-items-1" role="option" data-value="light">Light</div>
<div id="select-align-start-items-2" role="option" data-value="dark">Dark</div>
@@ -123,7 +123,7 @@ Use `data-align` on the popover to align the list to the trigger edge. Basecoat
<span class="truncate">Light</span>
<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-chevron-down-icon lucide-chevron-down text-muted-foreground opacity-50 shrink-0"><path d="m6 9 6 6 6-6" /></svg>
</button>
<div id="select-align-end-popover" data-popover aria-hidden="true" data-align="end">
<div id="select-align-end-popover" data-popover aria-hidden="true" data-align="end" class="w-72">
<div role="listbox" id="select-align-end-listbox" aria-orientation="vertical" aria-labelledby="select-align-end-trigger">
<div id="select-align-end-items-1" role="option" data-value="light">Light</div>
<div id="select-align-end-items-2" role="option" data-value="dark">Dark</div>
@@ -138,9 +138,9 @@ Use `data-align` on the popover to align the list to the trigger edge. Basecoat
### Groups
<Preview>
<div id="select-groups" class="select">
<div id="select-groups" class="select" data-placeholder="Select food...">
<button type="button" class="w-[220px]" id="select-groups-trigger" aria-haspopup="listbox" aria-expanded="false" aria-controls="select-groups-listbox">
<span class="truncate"></span>
<span class="truncate">Banana</span>
<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-chevron-down-icon lucide-chevron-down text-muted-foreground opacity-50 shrink-0"><path d="m6 9 6 6 6-6" /></svg>
</button>
<div id="select-groups-popover" data-popover aria-hidden="true">
@@ -148,7 +148,7 @@ Use `data-align` on the popover to align the list to the trigger edge. Basecoat
<div role="group" aria-labelledby="select-groups-fruits">
<div role="heading" id="select-groups-fruits">Fruits</div>
<div role="option" data-value="apple">Apple</div>
<div role="option" data-value="banana">Banana</div>
<div role="option" data-value="banana" aria-selected="true">Banana</div>
<div role="option" data-value="blueberry">Blueberry</div>
</div>
<hr role="separator" />
@@ -160,21 +160,21 @@ Use `data-align` on the popover to align the list to the trigger edge. Basecoat
</div>
</div>
</div>
<input type="hidden" name="select-groups-value" value="" />
<input type="hidden" name="select-groups-value" value="banana" />
</div>
</Preview>
### Scrollable
<Preview>
<div id="select-679828" class="select">
<button type="button" class="" id="select-679828-trigger" aria-haspopup="listbox" aria-expanded="false" aria-controls="select-679828-listbox">
<span class="truncate"></span>
<div id="select-679828" class="select" data-placeholder="Select an item...">
<button type="button" class="w-[180px]" id="select-679828-trigger" aria-haspopup="listbox" aria-expanded="false" aria-controls="select-679828-listbox">
<span class="truncate">Item 0</span>
<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-chevron-down-icon lucide-chevron-down text-muted-foreground opacity-50 shrink-0"><path d="m6 9 6 6 6-6" /></svg>
</button>
<div id="select-679828-popover" data-popover aria-hidden="true">
<div role="listbox" id="select-679828-listbox" aria-orientation="vertical" aria-labelledby="select-679828-trigger" class="scrollbar-thin overflow-y-auto max-h-64">
<div role="option">Item 0</div>
<div role="option" data-value="item-0" aria-selected="true">Item 0</div>
<div role="option" data-value="item-1">Item 1</div>
<div role="option" data-value="item-2">Item 2</div>
<div role="option" data-value="item-3">Item 3</div>
@@ -275,7 +275,7 @@ Use `data-align` on the popover to align the list to the trigger edge. Basecoat
<div role="option" data-value="item-98">Item 98</div>
</div>
</div>
<input type="hidden" name="select-679828-value" value="" />
<input type="hidden" name="select-679828-value" value="item-0" />
</div>
</Preview>
@@ -283,23 +283,23 @@ Use `data-align` on the popover to align the list to the trigger edge. Basecoat
<Preview>
<div id="select-877925" class="select">
<button type="button" class="" id="select-877925-trigger" aria-haspopup="listbox" aria-expanded="false" aria-controls="select-877925-listbox" disabled="disabled">
<span class="truncate"></span>
<button type="button" class="w-[180px]" id="select-877925-trigger" aria-haspopup="listbox" aria-expanded="false" aria-controls="select-877925-listbox" disabled="disabled">
<span class="truncate">Disabled</span>
<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-chevron-down-icon lucide-chevron-down text-muted-foreground opacity-50 shrink-0"><path d="m6 9 6 6 6-6" /></svg>
</button>
<div id="select-877925-popover" data-popover aria-hidden="true">
<div role="listbox" id="select-877925-listbox" aria-orientation="vertical" aria-labelledby="select-877925-trigger">
<div role="option" data-value="disabled">Disabled</div>
<div role="option" data-value="disabled" aria-selected="true">Disabled</div>
</div>
</div>
<input type="hidden" name="select-877925-value" value="" />
<input type="hidden" name="select-877925-value" value="disabled" />
</div>
</Preview>
### Invalid
<Preview class="w-full max-w-sm">
<div role="group" class="field" data-invalid="true">
<Preview>
<div role="group" class="field w-full max-w-sm" data-invalid="true">
<label for="select-invalid-trigger">Role</label>
<div id="select-invalid" class="select">
<button type="button" class="w-[220px]" id="select-invalid-trigger" aria-haspopup="listbox" aria-expanded="false" aria-controls="select-invalid-listbox" aria-invalid="true">
@@ -317,7 +317,7 @@ Use `data-align` on the popover to align the list to the trigger edge. Basecoat
</div>
<input type="hidden" name="select-invalid-value" value="" />
</div>
<p class="field-error" role="alert">Select a role to continue.</p>
<p role="alert">Select a role to continue.</p>
</div>
</Preview>
@@ -326,24 +326,24 @@ Use `data-align` on the popover to align the list to the trigger edge. Basecoat
<Preview>
<div id="select-with-icons" class="select">
<button type="button" class="w-[180px]" id="select-with-icons-trigger" aria-haspopup="listbox" aria-expanded="false" aria-controls="select-with-icons-listbox">
<span class="truncate"></span>
<span class="truncate">Bar</span>
<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-chevron-down-icon lucide-chevron-down text-muted-foreground opacity-50 shrink-0"><path d="m6 9 6 6 6-6" /></svg>
</button>
<div id="select-with-icons-popover" data-popover aria-hidden="true">
<div role="listbox" id="select-with-icons-listbox" aria-orientation="vertical" aria-labelledby="select-with-icons-trigger">
<div role="option" data-value="bar">
<div role="option" data-value="bar" data-label="Bar" aria-selected="true">
<span class="flex items-center gap-2">
<svg class="text-muted-foreground lucide lucide-chart-bar" 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"><path d="M3 3v16a2 2 0 0 0 2 2h16" /><path d="M7 16h8" /><path d="M7 11h12" /><path d="M7 6h3" /></svg>
Bar
</span>
</div>
<div role="option" data-value="line">
<div role="option" data-value="line" data-label="Line">
<span class="flex items-center gap-2">
<svg class="text-muted-foreground lucide lucide-chart-line" 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"><path d="M3 3v16a2 2 0 0 0 2 2h16" /><path d="m19 9-5 5-4-4-3 3" /></svg>
Line
</span>
</div>
<div role="option" data-value="pie">
<div role="option" data-value="pie" data-label="Pie">
<span class="flex items-center gap-2">
<svg class="text-muted-foreground lucide lucide-chart-pie" 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"><path d="M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z" /><path d="M21.21 15.89A10 10 0 1 1 8 2.83" /></svg>
Pie
@@ -351,7 +351,7 @@ Use `data-align` on the popover to align the list to the trigger edge. Basecoat
</div>
</div>
</div>
<input type="hidden" name="select-with-icons-value" value="" />
<input type="hidden" name="select-with-icons-value" value="bar" />
</div>
</Preview>
+103 -33
View File
@@ -1,3 +1,4 @@
# Sidebar
## Usage
@@ -5,20 +6,18 @@
This component ships a `sidebar()` macro for Jinja and Nunjucks.
</Callout>
Sidebar uses a native `<aside>` next to your page content. Basecoat owns the fixed positioning, overlay behavior, active states, nested lists, and `details` submenu styling. Your app still owns the navigation data and current route.
<Steps>
<Step title="Include the JavaScript files">
```html
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/sidebar.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/sidebar.min.js" defer></script>
```
</Step>
<Step title="Add your sidebar HTML">
```html
<aside class="sidebar" data-side="left" aria-hidden="false">
<aside id="sidebar" class="sidebar" data-side="left">
<nav aria-label="Sidebar navigation">
<section class="scrollbar-thin">
<div role="group" aria-labelledby="group-label-content-1">
@@ -76,44 +75,115 @@
<h1>Content</h1>
</main>
```
</Step>
</Steps>
#### HTML structure
### RTL
Set `dir="rtl"` on the sidebar or a parent element. Logical spacing, nested borders, and item content follow document direction. `data-side` is physical: `left` and `right` refer to viewport sides.
<Callout type="warning" title="Differences with shadcn/ui" icon="triangle-alert">
Basecoat intentionally supports the stable HTML sidebar surface: fixed left/right sidebars, mobile overlay behavior, grouped navigation, active states, and native `details` submenus. It does not expose shadcn/ui's React provider, rail, inset/floating variants, icon-only collapse mode, menu actions, menu badges, or menu skeleton API.
</Callout>
### HTML structure
Basecoat maps shadcn/ui's sidebar composition to regular navigation HTML: an `<aside>` root, one `<nav>`, optional header/footer regions, grouped lists, item controls, and native `<details>` for nested menus.
```html
<!-- Server-side or router-driven current item -->
<nav aria-label="Sidebar navigation">
<ul>
<li><a href="/my-path?page=overview">Overview</a></li>
<li><a href="/my-path?page=settings" aria-current="page">Settings</a></li>
<li><button type="button" data-active="true">Active action</button></li>
</ul>
</nav>
<aside id="app-sidebar" class="sidebar" data-side="left">
<nav aria-label="Sidebar navigation">
<header>
<a href="/" aria-label="Home">
<svg aria-hidden="true"><!-- ... --></svg>
<span>Acme</span>
</a>
</header>
<section>
<div role="group" aria-labelledby="sidebar-main">
<h3 id="sidebar-main">Main</h3>
<ul>
<li>
<a href="/dashboard" aria-current="page">
<svg aria-hidden="true"><!-- ... --></svg>
<span>Dashboard</span>
</a>
</li>
<li>
<button type="button" data-active="true">
<svg aria-hidden="true"><!-- ... --></svg>
<span>Search</span>
</button>
</li>
<li>
<details id="sidebar-settings" open>
<summary aria-controls="sidebar-settings-menu">
<svg aria-hidden="true"><!-- ... --></svg>
<span>Settings</span>
</summary>
<ul id="sidebar-settings-menu">
<li><a href="/settings/profile">Profile</a></li>
<li><a href="/settings/team">Team</a></li>
<li><a href="/settings/billing">Billing</a></li>
</ul>
</details>
</li>
</ul>
</div>
</section>
<footer>
<button type="button">
<span>Ronan Berder</span>
</button>
</footer>
</nav>
</aside>
<main>...</main>
```
<dl>
<dt><code class="highlight language-html">&lt;aside class="sidebar" aria-hidden="false"&gt;</code></dt>
<dd>Sidebar root. Supports <code>id</code>, <code>aria-hidden</code>, <code>data-side="left|right"</code>, <code>data-initial-open</code>, <code>data-initial-mobile-open</code>, and <code>data-breakpoint</code>.</dd>
<dt><code class="highlight language-html">&lt;nav aria-label="..."&gt;</code></dt>
<dd>Navigation landmark inside the sidebar.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code></dt>
<dd>Scrollable sidebar content.</dd>
<dt><code class="highlight language-html">&lt;a aria-current="page"&gt;</code></dt>
<dd>Current-page link. Use <code>data-active="true"</code> for active non-link items.</dd>
<dt><code class="highlight language-html">&lt;details&gt;</code> / <code class="highlight language-html">&lt;summary&gt;</code></dt>
<dd>Native disclosure for submenus.</dd>
<dt><code class="highlight language-html">&lt;main&gt;</code></dt>
<dt><code>&lt;aside class="sidebar"&gt;</code></dt>
<dd>
Sidebar root. Supports <code>id</code>, <code>data-side="left|right"</code>, <code>data-initial-open</code>, <code>data-initial-mobile-open</code>, and <code>data-breakpoint</code>. JavaScript manages <code>aria-hidden</code>.
<dl>
<dt><code>&lt;nav aria-label="..."&gt;</code></dt>
<dd>
Navigation landmark inside the sidebar.
<dl>
<dt><code>&lt;header&gt;</code></dt>
<dd>Optional header for branding, workspace controls, user controls, or persistent actions.</dd>
<dt><code>&lt;section&gt;</code></dt>
<dd>
Scrollable content region between the optional header and footer.
<dl>
<dt><code>&lt;div role="group" aria-labelledby="..."&gt;</code></dt>
<dd>Named navigation group. Use a heading element as the group label.</dd>
<dt><code>&lt;ul&gt;</code> + <code>&lt;li&gt;</code></dt>
<dd>
Menu and menu item structure. Top-level lists and nested submenu lists receive different spacing and borders.
<dl>
<dt><code>&lt;a&gt;</code> / <code>&lt;button&gt;</code></dt>
<dd>Menu item control. Use links for navigation and buttons for actions. Supports <code>data-variant="default|outline"</code> and <code>data-size="default|sm|lg"</code>. For links, use <code>&lt;a aria-current="page"&gt;</code> for the current page, otherwise use <code>data-active="true"</code> for active action state.</dd>
<dt><code>&lt;details&gt;</code> / <code>&lt;summary&gt;</code></dt>
<dd>Native disclosure for collapsible submenus. Put the nested <code>&lt;ul&gt;</code> directly after <code>&lt;summary&gt;</code>. Add <code>id</code> and <code>aria-controls</code> when the relationship is not obvious from nearby markup.</dd>
<dt><code>&lt;hr role="separator"&gt;</code></dt>
<dd>Optional separator between groups or menu sections.</dd>
</dl>
</dd>
</dl>
</dd>
<dt><code>&lt;footer&gt;</code></dt>
<dd>Optional footer for account controls, status, or persistent actions.</dd>
</dl>
</dd>
</dl>
</dd>
<dt><code>&lt;main&gt;</code></dt>
<dd>Sibling content wrapper. The desktop margin applies to the sibling immediately after the sidebar.</dd>
</dl>
#### Basecoat differences
Basecoat supports fixed left/right sidebars, mobile overlay behavior, grouped navigation, active states, and native `details` submenus. It does not expose shadcn/ui's React provider, rail, inset/floating variants, icon-only collapse mode, menu actions, menu badges, or menu skeleton API.
RTL content is supported through logical spacing and borders. `data-side` remains physical: `left` means the left side of the viewport and `right` means the right side.
### JavaScript API
| API | Type | Description |
+42 -31
View File
@@ -1,7 +1,7 @@
# Skeleton
<Preview>
<div class="flex items-center gap-4">
<div class="flex w-fit items-center gap-4">
<div class="skeleton size-10 shrink-0 rounded-full"></div>
<div class="grid gap-2">
<div class="skeleton h-4 w-[150px]"></div>
@@ -12,16 +12,10 @@
## Usage
Use the `skeleton` class with sizing utilities to create loading placeholders.
Add `class="skeleton"` with sizing utilities to create loading placeholders.
```html
<div class="flex items-center gap-4">
<div class="skeleton size-10 shrink-0 rounded-full"></div>
<div class="grid gap-2">
<div class="skeleton h-4 w-[150px]"></div>
<div class="skeleton h-4 w-[100px]"></div>
</div>
</div>
<div class="skeleton h-4 w-[150px]"></div>
```
## Examples
@@ -29,19 +23,19 @@ Use the `skeleton` class with sizing utilities to create loading placeholders.
### Avatar
<Preview>
<div class="flex items-center gap-4">
<div class="skeleton size-12 rounded-full"></div>
<div class="flex w-fit items-center gap-4">
<div class="skeleton size-10 shrink-0 rounded-full"></div>
<div class="grid gap-2">
<div class="skeleton h-4 w-32"></div>
<div class="skeleton h-4 w-48"></div>
<div class="skeleton h-4 w-[150px]"></div>
<div class="skeleton h-4 w-[100px]"></div>
</div>
</div>
</Preview>
### Card
<Preview class="w-full max-w-md">
<div class="card w-full @md:w-auto @md:min-w-sm">
<Preview class="w-full max-w-xs">
<div class="card w-full">
<header>
<div class="skeleton h-4 w-2/3"></div>
<div class="skeleton h-4 w-1/2"></div>
@@ -54,32 +48,49 @@ Use the `skeleton` class with sizing utilities to create loading placeholders.
### Text
<Preview class="w-full max-w-md">
<div class="grid gap-2">
<Preview class="w-full max-w-xs">
<div class="flex w-full flex-col gap-2">
<div class="skeleton h-4 w-full"></div>
<div class="skeleton h-4 w-5/6"></div>
<div class="skeleton h-4 w-2/3"></div>
<div class="skeleton h-4 w-full"></div>
<div class="skeleton h-4 w-3/4"></div>
</div>
</Preview>
### Form
<Preview class="w-full max-w-md">
<div class="grid gap-4 w-full max-w-sm">
<div class="skeleton h-4 w-24"></div>
<div class="skeleton h-9 w-full"></div>
<div class="skeleton h-4 w-32"></div>
<div class="skeleton h-9 w-full"></div>
<Preview class="w-full max-w-xs">
<div class="flex w-full flex-col gap-7">
<div class="flex flex-col gap-3">
<div class="skeleton h-4 w-20"></div>
<div class="skeleton h-10 w-full"></div>
</div>
<div class="flex flex-col gap-3">
<div class="skeleton h-4 w-24"></div>
<div class="skeleton h-10 w-full"></div>
</div>
<div class="skeleton h-9 w-24"></div>
</div>
</Preview>
### Table
<Preview class="w-full max-w-md">
<div class="grid gap-3 w-full">
<div class="skeleton h-8 w-full"></div>
<div class="skeleton h-8 w-full"></div>
<div class="skeleton h-8 w-full"></div>
<Preview class="w-full max-w-sm">
<div class="flex w-full flex-col gap-2">
<div class="flex gap-4">
<div class="skeleton h-4 flex-1"></div>
<div class="skeleton h-4 w-24"></div>
<div class="skeleton h-4 w-20"></div>
</div>
<div class="flex gap-4">
<div class="skeleton h-4 flex-1"></div>
<div class="skeleton h-4 w-24"></div>
<div class="skeleton h-4 w-20"></div>
</div>
<div class="flex gap-4">
<div class="skeleton h-4 flex-1"></div>
<div class="skeleton h-4 w-24"></div>
<div class="skeleton h-4 w-20"></div>
</div>
</div>
</Preview>
@@ -88,7 +99,7 @@ Use the `skeleton` class with sizing utilities to create loading placeholders.
Skeleton blocks are direction-neutral. Use logical layout utilities around them when spacing depends on direction.
<Preview>
<div dir="rtl" class="flex items-center gap-4">
<div dir="rtl" class="flex w-fit items-center gap-4">
<div class="skeleton size-10 shrink-0 rounded-full"></div>
<div class="grid gap-2">
<div class="skeleton h-4 w-[150px]"></div>
+6 -6
View File
@@ -12,11 +12,11 @@
## Usage
Use a native `<input type="range">` with the `input` class. Include `range.js` so Basecoat can keep the filled track in sync with the input value.
Use a native `<input type="range" class="input">`. Include `range.js` so Basecoat can keep the filled track in sync with the input value.
```html
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/range.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/range.min.js" defer></script>
```
```html
@@ -26,7 +26,7 @@ Use a native `<input type="range">` with the `input` class. Include `range.js` s
### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;input type="range" class="input"&gt;</code></dt>
<dt><code>&lt;input type="range" class="input"&gt;</code></dt>
<dd>Native range input styled as a slider. Width utilities such as <code>w-full</code> can be added directly.</dd>
</dl>
@@ -40,10 +40,10 @@ Basecoat intentionally uses the native range input. Unlike shadcn/ui's Base UI s
<input type="range" class="input w-full" min="0" max="100" value="50" />
</Preview>
### Controlled Label
### Label
<Preview class="w-full max-w-sm">
<div class="grid w-full gap-3 max-w-sm">
<div class="grid w-full gap-1 max-w-sm">
<div class="flex items-center justify-between gap-2">
<label class="label" for="temperature">Temperature</label>
<output class="text-sm text-muted-foreground" for="temperature">50</output>
+19 -19
View File
@@ -10,7 +10,7 @@
<section>
<h3>Processing payment...</h3>
</section>
<aside class="tabular-nums text-muted-foreground text-sm">$100.00</aside>
<aside class="tabular-nums text-sm">$100.00</aside>
</article>
</Preview>
@@ -39,20 +39,20 @@ Use the `size-*` utility class to change the size of the spinner.
### Button
Add a spinner to a button to indicate a loading state. Place the spinner before the label with `data-icon="inline-start"` for a start position, or after the label with `data-icon="inline-end"` for an end position.
Add a spinner to a button to indicate a loading state. Place the spinner before or after the label; button spacing is handled automatically.
<Preview>
<div class="flex flex-col items-center gap-4">
<button class="btn-sm" disabled>
<svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
<button class="btn" data-size="sm" disabled>
<svg aria-label="Loading" role="status" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
Loading...
</button>
<button class="btn-sm-outline" disabled>
<svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
<button class="btn" data-variant="outline" data-size="sm" disabled>
<svg aria-label="Loading" role="status" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
Please wait
</button>
<button class="btn-sm-secondary" disabled>
<svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
<button class="btn" data-variant="secondary" data-size="sm" disabled>
<svg aria-label="Loading" role="status" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
Processing
</button>
</div>
@@ -60,20 +60,20 @@ Add a spinner to a button to indicate a loading state. Place the spinner before
### Badge
Add a spinner to a badge to indicate a loading state. Place the spinner before the label with `data-icon="inline-start"` for a start position, or after the label with `data-icon="inline-end"` for an end position.
Add a spinner to a badge to indicate a loading state. Place the spinner before or after the label; badge spacing is handled automatically.
<Preview>
<div class="flex items-center gap-4">
<span class="badge">
<svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
<svg aria-label="Loading" role="status" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
Syncing
</span>
<span class="badge-secondary">
<svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
<span class="badge">
<svg aria-label="Loading" role="status" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
Updating
</span>
<span class="badge-outline">
<svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
<span class="badge">
<svg aria-label="Loading" role="status" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
Processing
</span>
</div>
@@ -84,17 +84,17 @@ Add a spinner to a badge to indicate a loading state. Place the spinner before t
<Preview class="w-full max-w-md">
<div class="flex w-full max-w-md flex-col gap-4">
<div class="input-group">
<input class="input" placeholder="Send a message..." disabled />
<input placeholder="Send a message..." disabled />
<span data-align="inline-end">
<svg aria-label="Loading" role="status" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
</span>
</div>
<div class="input-group">
<textarea class="textarea" placeholder="Send a message..." disabled></textarea>
<textarea placeholder="Send a message..." disabled></textarea>
<footer data-align="block-end">
<svg aria-label="Loading" role="status" class="animate-spin lucide lucide-loader-circle" 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"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
Validating...
<button type="button" class="btn-sm-icon ms-auto rounded">
<button type="button" class="btn ms-auto" data-size="icon-sm">
<svg class="lucide lucide-arrow-up" 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"><path d="m5 12 7-7 7 7" /><path d="M12 19V5" /></svg>
<span class="sr-only">Send</span>
</button>
@@ -115,7 +115,7 @@ Add a spinner to a badge to indicate a loading state. Place the spinner before t
<p>Please wait while we process your request. Do not refresh the page.</p>
</header>
<section>
<button class="btn-sm-outline">Cancel</button>
<button class="btn" data-variant="outline" data-size="sm">Cancel</button>
</section>
</div>
</Preview>
@@ -130,6 +130,6 @@ Spinner layout follows document direction. Use logical utilities when surroundin
<section>
<h3>جاري معالجة الدفع...</h3>
</section>
<aside class="tabular-nums text-muted-foreground text-sm">١٠٠.٠٠ دولار</aside>
<aside class="tabular-nums text-sm">١٠٠.٠٠ دولار</aside>
</article>
</Preview>
+53 -40
View File
@@ -1,7 +1,7 @@
# Switch
<Preview>
<div role="group" class="field flex-row items-center gap-2">
<div role="group" class="field" data-orientation="horizontal">
<input type="checkbox" id="airplane-mode" role="switch" class="input" />
<label for="airplane-mode">Airplane Mode</label>
</div>
@@ -9,18 +9,18 @@
## Usage
Add `role="switch"` and the `input` class to a native `<input type="checkbox">`. Use a native `<label>` or `aria-label` for accessible naming.
Add `class="input"` and `role="switch"` to a native checkbox. Use a native `<label>` or `aria-label` for accessible naming.
```html
<input type="checkbox" role="switch" class="input" />
```
#### HTML structure
### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;input type="checkbox" role="switch" class="input"&gt;</code></dt>
<dt><code>&lt;input type="checkbox" role="switch" class="input"&gt;</code></dt>
<dd>The switch control. Use native checkbox state: add <code>checked</code> for an initially enabled switch, <code>disabled</code> for a disabled switch, and <code>aria-invalid="true"</code> for invalid state.</dd>
<dt><code>data-size="sm"</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>data-size="sm"</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Renders the smaller switch size.</dd>
</dl>
@@ -29,42 +29,55 @@ Add `role="switch"` and the `input` class to a native `<input type="checkbox">`.
### Description
<Preview class="w-full max-w-sm">
<div role="group" class="field flex-row items-start justify-between gap-3">
<div class="grid gap-1.5">
<div role="group" class="field" data-orientation="horizontal">
<section>
<label for="switch-focus-mode">Share across devices</label>
<p id="switch-focus-mode-description" class="field-description">Focus is shared across devices, and turns off when you leave the app.</p>
</div>
<p id="switch-focus-mode-description">Focus is shared across devices, and turns off when you leave the app.</p>
</section>
<input type="checkbox" id="switch-focus-mode" role="switch" class="input" aria-describedby="switch-focus-mode-description" />
</div>
</Preview>
### Choice Card
Card-style selection where a native `<label>` wraps the entire field for a clickable card pattern.
Use a wrapping `<label>` when the whole card should toggle the switch.
<Preview class="w-full max-w-md">
<label class="field flex-row items-start justify-between gap-3 rounded-lg border p-4">
<span class="grid gap-1.5">
<span>Share across devices</span>
<span class="field-description">Focus is shared across devices, and turns off when you leave the app.</span>
</span>
<input type="checkbox" role="switch" class="input" checked />
</label>
<Preview class="w-full max-w-sm">
<div role="group" class="grid w-full gap-3" aria-label="Focus settings">
<label>
<div role="group" class="field" data-orientation="horizontal">
<section>
<h3>Share across devices</h3>
<p>Focus is shared across devices, and turns off when you leave the app.</p>
</section>
<input type="checkbox" role="switch" class="input" />
</div>
</label>
<label>
<div role="group" class="field" data-orientation="horizontal">
<section>
<h3>Enable notifications</h3>
<p>Receive notifications when focus mode changes.</p>
</section>
<input type="checkbox" role="switch" class="input" checked />
</div>
</label>
</div>
</Preview>
### Disabled
Use the native `disabled` attribute on the switch and `data-disabled="true"` on the field wrapper when the surrounding field should use disabled styling.
Add `disabled` to the switch and `data-disabled="true"` to the field wrapper when the surrounding field should use disabled styling.
<Preview class="w-full max-w-sm">
<div class="grid gap-4">
<div role="group" class="field flex-row items-center justify-between" data-disabled="true">
<label for="switch-disabled-unchecked">Disabled (Unchecked)</label>
<Preview>
<div class="grid gap-12">
<div role="group" class="field" data-orientation="horizontal" data-disabled="true">
<input type="checkbox" id="switch-disabled-unchecked" role="switch" class="input" disabled />
<label for="switch-disabled-unchecked">Disabled (Unchecked)</label>
</div>
<div role="group" class="field flex-row items-center justify-between" data-disabled="true">
<label for="switch-disabled-checked">Disabled (Checked)</label>
<div role="group" class="field" data-orientation="horizontal" data-disabled="true">
<input type="checkbox" id="switch-disabled-checked" role="switch" class="input" checked disabled />
<label for="switch-disabled-checked">Disabled (Checked)</label>
</div>
</div>
</Preview>
@@ -74,11 +87,11 @@ Use the native `disabled` attribute on the switch and `data-disabled="true"` on
Add `aria-invalid="true"` to the switch and `data-invalid="true"` to the field wrapper.
<Preview class="w-full max-w-sm">
<div role="group" class="field flex-row items-start justify-between gap-3" data-invalid="true">
<div class="grid gap-1.5">
<label for="switch-invalid">Accept terms and conditions</label>
<p class="field-error">You must accept the terms and conditions to continue.</p>
</div>
<div role="group" class="field" data-orientation="horizontal" data-invalid="true">
<section>
<label for="switch-invalid">Enable notifications</label>
<p role="alert">You must enable notifications to continue.</p>
</section>
<input type="checkbox" id="switch-invalid" role="switch" class="input" aria-invalid="true" />
</div>
</Preview>
@@ -87,15 +100,15 @@ Add `aria-invalid="true"` to the switch and `data-invalid="true"` to the field w
Use `data-size="sm"` to render the smaller switch size.
<Preview class="w-full max-w-sm">
<div class="grid gap-4">
<div role="group" class="field flex-row items-center justify-between">
<label for="switch-size-sm">Small</label>
<Preview class="w-full max-w-[10rem]">
<div role="group" class="grid w-full gap-4" aria-label="Switch sizes">
<div role="group" class="field" data-orientation="horizontal">
<input type="checkbox" id="switch-size-sm" role="switch" class="input" data-size="sm" />
<label for="switch-size-sm">Small</label>
</div>
<div role="group" class="field flex-row items-center justify-between">
<label for="switch-size-default">Default</label>
<div role="group" class="field" data-orientation="horizontal">
<input type="checkbox" id="switch-size-default" role="switch" class="input" />
<label for="switch-size-default">Default</label>
</div>
</div>
</Preview>
@@ -105,11 +118,11 @@ Use `data-size="sm"` to render the smaller switch size.
Switch field layout uses document direction. Set `dir="rtl"` on the field or an ancestor.
<Preview class="w-full max-w-sm">
<div dir="rtl" role="group" class="field flex-row items-start justify-between gap-3">
<div class="grid gap-1.5">
<div dir="rtl" role="group" class="field" data-orientation="horizontal">
<section>
<label for="switch-rtl">المشاركة عبر الأجهزة</label>
<p id="switch-rtl-description" class="field-description">يتم مشاركة التركيز عبر الأجهزة، ويتم إيقاف تشغيله عند مغادرة التطبيق.</p>
</div>
<p id="switch-rtl-description">يتم مشاركة التركيز عبر الأجهزة، ويتم إيقاف تشغيله عند مغادرة التطبيق.</p>
</section>
<input type="checkbox" id="switch-rtl" role="switch" class="input" aria-describedby="switch-rtl-description" checked />
</div>
</Preview>
+2 -2
View File
@@ -68,7 +68,7 @@
## Usage
Add the `table` class to your table element. Wrap it in `table-container` when horizontal scrolling is needed.
Add `class="table"` to your table element. Wrap it in `class="table-container"` when horizontal scrolling is needed.
```html
<div class="table-container">
@@ -119,7 +119,7 @@ Add the `table` class to your table element. Wrap it in `table-container` when h
<tr>
<td class="font-medium">Project Alpha</td>
<td class="text-end">
<button type="button" class="btn-xs-outline">Open</button>
<button type="button" class="btn" data-variant="outline" data-size="xs">Open</button>
</td>
</tr>
</tbody>
+11 -11
View File
@@ -65,8 +65,8 @@
<Step title="Include the JavaScript files">
```html
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/tabs.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/tabs.min.js" defer></script>
```
</Step>
@@ -133,19 +133,19 @@
### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;div class="tabs"&gt;</code></dt>
<dt><code>&lt;div class="tabs"&gt;</code></dt>
<dd>
Root tabs container. It becomes horizontal or vertical based on the child tablist's <code>aria-orientation</code>.
<dl>
<dt><code class="highlight language-html">&lt;nav role="tablist" aria-orientation="horizontal"&gt;</code></dt>
<dt><code>&lt;nav role="tablist" aria-orientation="horizontal"&gt;</code></dt>
<dd>
Tablist containing tab buttons. Set <code>aria-orientation="vertical"</code> for vertical tabs and <code>data-variant="line"</code> for the line style.
<dl>
<dt><code class="highlight language-html">&lt;button role="tab" id="&#123; TAB_ID &#125;" aria-controls="&#123; PANEL_ID &#125;" aria-selected="true" tabindex="0"&gt;</code></dt>
<dt><code>&lt;button role="tab" id="&#123; TAB_ID &#125;" aria-controls="&#123; PANEL_ID &#125;" aria-selected="true" tabindex="0"&gt;</code></dt>
<dd>Tab trigger. The active tab has <code>aria-selected="true"</code> and <code>tabindex="0"</code>. Inactive tabs use <code>tabindex="-1"</code>. Disabled tabs use <code>disabled</code> or <code>aria-disabled="true"</code>.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;div role="tabpanel" id="&#123; PANEL_ID &#125;" aria-labelledby="&#123; TAB_ID &#125;"&gt;</code></dt>
<dt><code>&lt;div role="tabpanel" id="&#123; PANEL_ID &#125;" aria-labelledby="&#123; TAB_ID &#125;"&gt;</code></dt>
<dd>Panel controlled by a tab. Inactive panels are hidden.</dd>
</dl>
</dd>
@@ -162,7 +162,7 @@
### Line
<Preview class="w-full max-w-md">
<Preview>
<div class="tabs" id="tabs-line">
<nav role="tablist" aria-orientation="horizontal" data-variant="line">
<button type="button" role="tab" id="tabs-line-tab-1" aria-controls="tabs-line-panel-1" aria-selected="true" tabindex="0">Account</button>
@@ -177,7 +177,7 @@
### Vertical
<Preview class="w-full max-w-md">
<Preview>
<div class="tabs w-full" id="tabs-vertical">
<nav role="tablist" aria-orientation="vertical">
<button type="button" role="tab" id="tabs-vertical-tab-1" aria-controls="tabs-vertical-panel-1" aria-selected="true" tabindex="0">Account</button>
@@ -192,7 +192,7 @@
### Disabled
<Preview class="w-full max-w-md">
<Preview>
<div class="tabs" id="tabs-disabled">
<nav role="tablist" aria-orientation="horizontal">
<button type="button" role="tab" id="tabs-disabled-tab-1" aria-controls="tabs-disabled-panel-1" aria-selected="true" tabindex="0">Account</button>
@@ -207,7 +207,7 @@
### Icons
<Preview class="w-full max-w-md">
<Preview>
<div class="tabs" id="tabs-icons">
<nav role="tablist" aria-orientation="horizontal">
<button type="button" role="tab" id="tabs-icons-tab-1" aria-controls="tabs-icons-panel-1" aria-selected="true" tabindex="0">
@@ -233,7 +233,7 @@
Tabs support document direction. Set `dir="rtl"` on the tabs root or a parent element.
<Preview class="w-full max-w-md">
<Preview>
<div dir="rtl">
<div class="tabs" id="tabs-rtl">
<nav role="tablist" aria-orientation="horizontal" data-variant="line">
+1 -1
View File
@@ -6,7 +6,7 @@
## Usage
Add `textarea` to a `<textarea>` element. Textareas inside a `form` or `field` container also receive the same styling automatically.
Add `class="textarea"` to a `<textarea>` element. Textareas inside a `class="field"` container also receive the same styling automatically.
Use native HTML attributes for behavior: `disabled` for disabled textareas and `aria-invalid="true"` for invalid state styling.
+2 -2
View File
@@ -7,7 +7,7 @@
data-tooltip="Toggle dark mode"
data-side="bottom"
onclick="window.basecoat.theme.toggle()"
class="btn-icon-outline size-8"
class="btn size-8" data-variant="outline" data-size="icon"
>
<span class="hidden dark:block">
<svg class="lucide lucide-sun" 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"><circle cx="12" cy="12" r="4" /><path d="M12 2v2" /><path d="M12 20v2" /><path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" /><path d="M2 12h2" /><path d="M20 12h2" /><path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" /></svg>
@@ -44,7 +44,7 @@ Add this inline script before loading your styles so the stored mode is applied
Load `basecoat.js` or the bundled JavaScript before calling `window.basecoat.theme`.
```html
<button type="button" aria-label="Toggle dark mode" data-tooltip="Toggle dark mode" data-side="bottom" onclick="window.basecoat.theme.toggle()" class="btn-icon-outline size-8">
<button type="button" aria-label="Toggle dark mode" data-tooltip="Toggle dark mode" data-side="bottom" onclick="window.basecoat.theme.toggle()" class="btn size-8" data-variant="outline" data-size="icon">
<span class="hidden dark:block">
<svg class="lucide lucide-sun" 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"><circle cx="12" cy="12" r="4" /><path d="M12 2v2" /><path d="M12 20v2" /><path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" /><path d="M2 12h2" /><path d="M20 12h2" /><path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" /></svg>
</span>
+20 -20
View File
@@ -5,7 +5,7 @@
<Preview class="max-w-md">
<button
class="btn-outline"
class="btn" data-variant="outline"
hx-trigger="click"
hx-select="unset"
hx-get="/fragments/toast/success"
@@ -18,7 +18,7 @@
<Preview class="max-w-md">
<button
class="btn-outline"
class="btn" data-variant="outline"
onclick="document.getElementById('toaster').toast({
category: 'success',
title: 'Success',
@@ -42,8 +42,8 @@
<Step title="Include the JavaScript files">
```html
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/toast.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/toast.min.js" defer></script>
```
</Step>
@@ -81,7 +81,7 @@ Append server-rendered toast markup to the toaster, or create one from JavaScrip
```html
<button
class="btn-outline"
class="btn" data-variant="outline"
onclick="document.getElementById('toaster').toast({
category: 'success',
title: 'Success',
@@ -100,29 +100,29 @@ Append server-rendered toast markup to the toaster, or create one from JavaScrip
### HTML structure
<dl>
<dt><code class="highlight language-html">&lt;div id="toaster" class="toaster"&gt;</code></dt>
<dt><code>&lt;div id="toaster" class="toaster"&gt;</code></dt>
<dd>Toast container. Optional <code>data-align</code> values are <code>start</code>, <code>center</code>, and <code>end</code>.</dd>
<dt><code class="highlight language-html">&lt;div class="toast"&gt;</code></dt>
<dt><code>&lt;div class="toast"&gt;</code></dt>
<dd>
Toast item. Use <code>data-category="success|info|warning|error"</code> for category styling and <code>data-duration</code> to override the timeout in milliseconds. Use <code>data-duration="-1"</code> to keep it open.
<dl>
<dt><code class="highlight language-html">&lt;div class="toast-content"&gt;</code></dt>
<dt><code>&lt;div class="toast-content"&gt;</code></dt>
<dd>
Content wrapper.
<dl>
<dt><code class="highlight language-html">&lt;svg aria-hidden="true"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;svg aria-hidden="true"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Icon.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code></dt>
<dt><code>&lt;section&gt;</code></dt>
<dd>
Message region.
<dl>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dt><code>&lt;h2&gt;</code></dt>
<dd>Title.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;p&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Description.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>&lt;footer&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Action buttons or links. Clicking a footer button or link closes the toast.</dd>
</dl>
</dd>
@@ -134,23 +134,23 @@ Append server-rendered toast markup to the toaster, or create one from JavaScrip
| API | Type | Description |
| --- | --- | --- |
| `toaster.toast(config)` | Method | Creates a toast from a config object and returns the inserted `.toast` element. |
| `toaster.toast(config)` | Method | Creates a toast from a config object and returns the inserted `.toast` element. [See "Config object" below](#config-object). |
| `toaster.closeAll()` | Method | Closes every toast inside the toaster. |
| `toast.close()` | Method | Closes one initialized toast element. |
### Toast config object
### Config object
<dl>
<dt><code>duration</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>duration</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Timeout in milliseconds. Defaults to <code>3000</code>, or <code>5000</code> for error toasts. Use <code>-1</code> to keep it open.</dd>
<dt><code>category</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>category</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Category of the toast, either <code>success</code>, <code>info</code>, <code>warning</code>, or <code>error</code>.</dd>
<dt><code>title</code></dt>
<dd>The title of the toast.</dd>
<dt><code>description</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>description</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The description of the toast.</dd>
<dt><code>action</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>action</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Primary action. Supports <code>label</code> with either <code>href</code> or <code>onclick</code>.</dd>
<dt><code>cancel</code> <span class="badge-secondary">Optional</span></dt>
<dt><code>cancel</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Secondary action. Supports <code>label</code> and optional <code>onclick</code>.</dd>
</dl>
+11 -11
View File
@@ -1,7 +1,7 @@
# Tooltip
<Preview>
<button class="btn-outline" data-tooltip="Add to library">Hover</button>
<button class="btn" data-variant="outline" data-tooltip="Add to library">Hover</button>
</Preview>
## Usage
@@ -15,7 +15,7 @@ Add `data-tooltip` to the trigger element. Use `data-side` and `data-align` to c
Basecoat tooltips are CSS-only and text-only. This intentionally differs from shadcn/ui's composed tooltip content, which can render arbitrary markup and uses a portal.
```html
<button class="btn-outline" data-tooltip="Tooltip text" data-side="bottom" data-align="center">Bottom</button>
<button class="btn" data-variant="outline" data-tooltip="Tooltip text" data-side="bottom" data-align="center">Bottom</button>
```
## Examples
@@ -23,24 +23,24 @@ Basecoat tooltips are CSS-only and text-only. This intentionally differs from sh
### Default
<Preview>
<button class="btn-outline" data-tooltip="Default tooltip">Default</button>
<button class="btn" data-variant="outline" data-tooltip="Default tooltip">Default</button>
</Preview>
### Sides
<Preview>
<div class="flex flex-wrap gap-2">
<button class="btn-outline" data-tooltip="Tooltip" data-side="inline-start">Inline Start</button>
<button class="btn-outline" data-tooltip="Tooltip" data-side="top">Top</button>
<button class="btn-outline" data-tooltip="Tooltip" data-side="bottom">Bottom</button>
<button class="btn-outline" data-tooltip="Tooltip" data-side="inline-end">Inline End</button>
<button class="btn" data-variant="outline" data-tooltip="Tooltip" data-side="inline-start">Inline Start</button>
<button class="btn" data-variant="outline" data-tooltip="Tooltip" data-side="top">Top</button>
<button class="btn" data-variant="outline" data-tooltip="Tooltip" data-side="bottom">Bottom</button>
<button class="btn" data-variant="outline" data-tooltip="Tooltip" data-side="inline-end">Inline End</button>
</div>
</Preview>
### Icon Button
<Preview>
<button class="btn-icon-ghost" data-tooltip="Additional information" aria-label="Info">
<button class="btn" data-variant="ghost" data-size="icon" data-tooltip="Additional information" aria-label="Info">
<svg class="lucide lucide-info" 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"><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /></svg>
</button>
</Preview>
@@ -49,7 +49,7 @@ Basecoat tooltips are CSS-only and text-only. This intentionally differs from sh
<Preview>
<span class="inline-block" data-tooltip="This feature is currently unavailable">
<button class="btn-outline" disabled>Disabled</button>
<button class="btn" data-variant="outline" disabled>Disabled</button>
</span>
</Preview>
@@ -63,7 +63,7 @@ Basecoat tooltips are CSS-only and text-only. This intentionally differs from sh
<Preview>
<div dir="rtl" class="flex gap-2">
<button class="btn-outline" data-tooltip="تلميح" data-side="inline-start">Inline Start</button>
<button class="btn-outline" data-tooltip="تلميح" data-side="inline-end">Inline End</button>
<button class="btn" data-variant="outline" data-tooltip="تلميح" data-side="inline-start">Inline Start</button>
<button class="btn" data-variant="outline" data-tooltip="تلميح" data-side="inline-end">Inline End</button>
</div>
</Preview>
+90 -120
View File
@@ -1,84 +1,37 @@
# Customization
## Icons
Basecoat customization has three layers:
Basecoat uses [Lucide icons](https://lucide.dev). You have three options:
1. Choose a style pack.
2. Override theme tokens such as colors and fonts.
3. Add small project-level CSS overrides when tokens are not enough.
- **Copy SVG code**: Visit [lucide.dev/icons](https://lucide.dev/icons), click any icon to copy the SVG, and paste it directly in your HTML. Best for simple projects or occasional icon usage.
- **Install lucide**: Follow the [installation guide](https://lucide.dev/guide/installation) to install the `lucide` package and dynamically render icons. Best for projects that need many icons or want tree-shaking.
- **Use framework packages**: Browse [framework-specific packages](https://lucide.dev/packages) (React, Vue, Angular, etc.) for component-based icon usage. Best if you're using a supported framework.
For a full visual rewrite, import the Basecoat base layer and write your own style pack instead of overriding an existing one wholesale.
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="https://lucide.dev" target="_blank">
Browse icons
<svg
class="lucide lucide-arrow-right"
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"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
## Styles
</a>
<a class="badge-outline" href="https://lucide.dev/guide/installation" target="_blank">
Installation guide
<svg
class="lucide lucide-arrow-right"
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"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
Basecoat ships the same style families used by the current shadcn/ui registry: Vega, Nova, Maia, Lyra, Mira, Luma, Sera, and Rhea.
</a>
<a class="badge-outline" href="https://lucide.dev/packages" target="_blank">
Packages
<svg
class="lucide lucide-arrow-right"
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"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
Pick one complete style bundle in your app CSS:
</a>
</div>
```css
@import "tailwindcss";
@import "basecoat-css/vega";
```
## Theming
Swap the import to use another style:
You can import any [shadcn/ui](https://shadcn-ui.com) compatible theme, such as [tweakcn](https://tweakcn.com):
```css
@import "tailwindcss";
@import "basecoat-css/sera";
```
1. Go to [ui.shadcn.com/themes](https://ui.shadcn.com/themes) and select a theme.
2. Click "Copy code" and save the theme variables in a file, such as `theme.css`.
3. Import the theme in your CSS file:
Each style bundle is standalone. Do not import Vega first and then load another style on top of it.
Keep the selected Basecoat style after Tailwind or any other stylesheet that emits a reset/base layer.
## Themes
Basecoat uses shadcn/ui-compatible CSS variables. You can use a theme from [TweakCN](https://tweakcn.com) or another shadcn/ui theme generator by importing the variables after Basecoat:
```css
@import "tailwindcss";
@@ -86,36 +39,24 @@ You can import any [shadcn/ui](https://shadcn-ui.com) compatible theme, such as
@import "./theme.css";
```
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="https://ui.shadcn.com/docs/theming" target="_blank">
About shadcn/ui theming
<svg
class="lucide lucide-arrow-right"
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"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
`theme.css` should define token values such as `--background`, `--foreground`, `--primary`, `--muted`, `--border`, `--input`, and `--ring` for `:root` and `.dark` when needed.
</a>
</div>
Learn more in the [shadcn/ui theming docs](https://ui.shadcn.com/docs/theming).
## Fonts
Basecoat does not ship web fonts by default. The Basecoat docs site uses the same font choices as the current shadcn/ui site: [Geist Sans](https://fonts.google.com/specimen/Geist) for sans and heading text, and [Geist Mono](https://fonts.google.com/specimen/Geist+Mono) for monospace text.
Basecoat does not ship web fonts by default. The docs site uses the same font choices as the current shadcn/ui site: [Geist Sans](https://fonts.google.com/specimen/Geist) for sans and heading text, and [Geist Mono](https://fonts.google.com/specimen/Geist+Mono) for monospace text.
To use the same setup, load the fonts and override the font tokens after importing Basecoat:
Install the fonts with Fontsource:
<Code lang="css">{`@import "tailwindcss";
```bash
npm install @fontsource/geist-sans @fontsource/geist-mono
```
Then import the font files and override the font tokens after Basecoat:
```css
@import "tailwindcss";
@import "basecoat-css/sera";
@import "@fontsource/geist-sans/400.css";
@import "@fontsource/geist-sans/500.css";
@@ -130,41 +71,70 @@ To use the same setup, load the fonts and override the font tokens after importi
--font-sans: "Geist Sans", ui-sans-serif, system-ui, sans-serif;
--font-heading: "Geist Sans", ui-sans-serif, system-ui, sans-serif;
--font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}`}</Code>
}
```
Keep font overrides in a small CSS file imported after Basecoat. This makes fonts a project-level customization instead of a Basecoat package default.
Keep font overrides in a small CSS file imported after Basecoat. Fonts are project-level customization, not a Basecoat default.
## Custom styles
## Icons
Basecoat style packs combine shared Basecoat structure with visual rules for each component. If you want to write a full custom style, import the styleless Basecoat base and then your own style file.
Basecoat examples use [Lucide icons](https://lucide.dev), but Basecoat does not require an icon package.
<Code lang="css">{`@import "tailwindcss";
@import "basecoat-css/base";
@import "./style-acme.css";`}</Code>
You can:
`basecoat-css/base` includes Basecoat tokens, semantic utilities, and component structure, but no Vega/Nova/Maia/etc. visual style pack. Your style file is responsible for component visuals such as colors, radius, shadows, focus rings, spacing, and variant treatment.
- Copy SVGs from [lucide.dev/icons](https://lucide.dev/icons) and paste them into your HTML.
- Install [`lucide`](https://lucide.dev/guide/installation) and render icons from JavaScript.
- Use a framework-specific Lucide package if your app already uses a framework.
The practical way to start is to copy one existing style pack, rename it, and edit from there. The style files live in `src/css/styles/` and are also published under `basecoat-css/styles/*`.
Inline SVG is the simplest option for plain HTML, Jinja, Nunjucks, Rails, Django, Laravel, or similar stacks.
<Code lang="css">{`@import "basecoat-css/base";
@import "./style-acme.css";
## Project overrides
/* style-acme.css can start as a copy of basecoat-css/styles/nova. */`}</Code>
For small visual tweaks, add utilities or project CSS after Basecoat:
Do not load a full Basecoat style bundle and then override it wholesale. For example, avoid importing `basecoat-css/nova` before your custom style. That loads Nova component visuals and forces your file to undo them. Use `basecoat-css/base` instead.
## Style overrides
You can override default styles using Tailwind:
<Code lang="html">{`<button class="btn font-normal">Click me</button>`}</Code>
You can also extend or override the existing styles in your own Tailwind files:
<Code lang="css">{`@import "tailwindcss";
```css
@import "tailwindcss";
@import "basecoat-css/sera";
@import "./custom.css";`}</Code>
@import "./app.css";
```
More importantly, you can use the [theme variables](#theming) to customize many aspects of the components (colors, fonts, sizes, etc).
```html
<button class="btn font-normal" data-variant="outline">Click me</button>
```
**If you want to make more drastic changes**, you can [copy the `basecoat.css` file](https://github.com/hunvreus/basecoat/blob/main/src/css/basecoat.css) into your project and make your changes there.
Prefer theme tokens for broad changes and one-off utilities for local changes. Avoid copying generated Basecoat bundles into your app unless you intend to maintain a fork.
## Custom style packs
If you want a full custom style, import the styleless Basecoat base and then your own style file:
```css
@import "tailwindcss";
@import "basecoat-css/base";
@import "./styles/acme.css";
```
`basecoat-css/base` includes Basecoat tokens, semantic utilities, and component structure, but no Vega/Nova/Maia/etc. visual style pack. Your style file owns component visuals: colors, radius, shadows, focus rings, spacing, variants, and state treatment.
The practical way to start is to copy one existing style pack from `basecoat-css/styles/*`, rename it, and edit it:
```css
@import "basecoat-css/base";
@import "./styles/acme.css";
/* ./styles/acme.css can start as a copy of basecoat-css/styles/nova. */
```
Do not import a complete style bundle before your custom style. For example, avoid importing `basecoat-css/nova` and then overriding it wholesale. That loads Nova visuals and forces your file to undo them. Use `basecoat-css/base` instead.
## Compatibility
Basecoat 1.0 prefers `data-variant` and `data-size` over legacy variant classes. If you need old pre-1.0 aliases while migrating, import the compatibility layer after Basecoat:
```css
@import "tailwindcss";
@import "basecoat-css/vega";
@import "basecoat-css/compat";
```
The compatibility layer is for migration only. New code should use the documented 1.0 API.
+12 -8
View File
@@ -7,7 +7,7 @@
{
"type": "item",
"label": "Introduction",
"slug": "index",
"slug": "introduction",
"icon": "info"
},
{
@@ -16,12 +16,12 @@
"slug": "installation",
"icon": "download"
},
{
"type": "item",
"label": "Customize",
"slug": "customization",
"icon": "wrench"
},
{
"type": "item",
"label": "Customize",
"slug": "customization",
"icon": "sliders-horizontal"
},
{
"type": "item",
"label": "CLI",
@@ -65,7 +65,11 @@
"components/button",
"components/button-group",
"components/card",
"components/chart",
{
"type": "item",
"slug": "components/chart",
"badge": "Alpha"
},
"components/checkbox",
"components/combobox",
"components/command",
+150 -87
View File
@@ -1,118 +1,181 @@
# Installation
## Install using the CDN
Basecoat can be used from a CDN or installed with npm. Use the CDN for simple static pages and prototypes. Use npm when your project already has a Tailwind CSS build step.
### Install all components
## CDN
Add the following to the `` of your HTML file. This loads the default CDN stylesheet and the JavaScript bundle for every JavaScript component:
<Steps>
<Step title="Add the stylesheet">
<Code lang="html">{`<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/basecoat.cdn.min.css" />
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/all.min.js" defer></script>`}</Code>
Use the default bundled stylesheet, or choose one standalone style bundle.
The default CDN stylesheet is the Basecoat backward-compatible default. To use a specific style, load the matching standalone stylesheet instead:
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/basecoat.cdn.min.css" />
```
<Code lang="html">{`<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/basecoat-sera.cdn.min.css" />`}</Code>
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/basecoat-maia.cdn.min.css" />
```
### Install specific components
</Step>
<Step title="Add JavaScript when needed">
The CDN style bundles are aggregated. If you want to cherry-pick CSS, use the package component files through npm or your own asset pipeline.
Most components are CSS-only. Interactive components need JavaScript. Use the all-in-one bundle when file size is not a concern.
For JavaScript, you can cherry-pick the components you need as instructed in each component page. Include `basecoat.min.js` once, then include the component script.
```html
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/all.min.js" defer></script>
```
<Code lang="html">{`<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/basecoat.cdn.min.css" />
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/basecoat.min.js" defer></script> <!-- Only needed once to register and initialize components -->
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.2/dist/js/dropdown-menu.min.js" defer></script>`}</Code>
Or load the Basecoat runtime once, then only the component scripts you use.
## Install using NPM
```html
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/dropdown-menu.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0-beta.3/dist/js/select.min.js" defer></script>
```
<div class="grid gap-8 mb-12">
<section>
<h3 class="mb-4 scroll-m-20 font-semibold tracking-tight" id="install-npm-tailwind"><a href="#install-npm-tailwind">Step 1: Add Tailwind CSS</a></h3>
Basecoat uses Tailwind CSS. You need to install it in your project.
</Step>
<Step title="Use the markup">
[Follow the Tailwind CSS installation instructions to get started.](https://tailwindcss.com/docs/installation)
</section>
<section>
<h3 class="mb-4 scroll-m-20 font-semibold tracking-tight" id="install-npm-basecoat"><a href="#install-npm-basecoat">Step 2: Add Basecoat</a></h3>
Add Basecoat to your Tailwind CSS file.
<CodeGroup defaultTab="npm">
```html
<button class="btn">Button</button>
<div class="card">
<header>
<h3>Card title</h3>
<p>Card description.</p>
</header>
<section>Card content.</section>
</div>
```
</Step>
</Steps>
### CDN files
| File | Description |
| --- | --- |
| `dist/basecoat.cdn.min.css` | Default standalone CDN stylesheet. |
| `dist/basecoat-{style}.cdn.min.css` | Standalone CDN stylesheet for `vega`, `nova`, `maia`, `lyra`, `mira`, `luma`, `sera`, or `rhea`. |
| `dist/basecoat-base.cdn.min.css` | Base tokens, utilities, and component structure without a style pack. |
| `dist/basecoat-compat.cdn.min.css` | Optional pre-1.0 compatibility aliases. Load after Basecoat. |
| `dist/js/all.min.js` | All JavaScript components. |
| `dist/js/basecoat.min.js` | Runtime only. Load once before individual component scripts. |
| `dist/js/{component}.min.js` | Individual JavaScript component file. |
## NPM
<Steps>
<Step title="Install Tailwind CSS">
Basecoat is authored for Tailwind CSS. Install Tailwind first if your project does not already use it.
[Follow the Tailwind CSS installation guide](https://tailwindcss.com/docs/installation).
</Step>
<Step title="Install Basecoat">
<CodeGroup defaultTab="npm">
<Code title="npm" lang="bash">{`npm install basecoat-css`}</Code>
<Code title="pnpm" lang="bash">{`pnpm add basecoat-css`}</Code>
<Code title="bun" lang="bash">{`bun add basecoat-css`}</Code>
<Code title="yarn" lang="bash">{`yarn add basecoat-css`}</Code>
</CodeGroup>
Alternatively, you can directly <a href="https://github.com/hunvreus/basecoat/blob/main/src/css/basecoat.css" target="_blank">copy the <code>basecoat.css</code> file</a> into your project.
</section>
<section>
<h3 class="mb-4 scroll-m-20 font-semibold tracking-tight" id="install-npm-import"><a href="#install-npm-import">Step 3: Import Basecoat</a></h3>
<Code lang="css">{`@import "tailwindcss";
@import "basecoat-css";`}</Code>
The default import is the Basecoat backward-compatible default. To use a specific style, import the standalone style bundle instead:
<Code lang="css">{`@import "tailwindcss";
@import "basecoat-css/sera";`}</Code>
You can also import lower-level bundles or individual component CSS files:
<Code lang="css">{`@import "tailwindcss";
/* Base tokens and global utilities only. */
@import "basecoat-css/base";
</Step>
<Step title="Import CSS">
/* All component structure without a style pack. */
@import "basecoat-css/components";
Import Tailwind, then import one Basecoat bundle.
/* Individual component CSS. */
@import "basecoat-css/components/button";
@import "basecoat-css/components/select";
```css
@import "tailwindcss";
@import "basecoat-css";
```
/* One standalone style pack. */
@import "basecoat-css/styles/sera";`}</Code>
</section>
<section>
<h3 class="mb-4 scroll-m-20 font-semibold tracking-tight" id="install-npm-js"><a href="#install-npm-js">Step 4: Add JavaScript files</a></h3>
Some components [need JavaScript](#install-js). You can either import the all-in-one bundle or only the components you use.
To choose a specific style, import that style bundle instead.
#### Using a build tool (Vite, Webpack…)
```css
@import "tailwindcss";
@import "basecoat-css/maia";
```
To include all JavaScript components:
<Code lang="js">{`import 'basecoat-css/all';`}</Code>
Or cherry-pick specific components. Import `basecoat-css/basecoat` once, then import the component scripts:
<Code lang="js">{`import 'basecoat-css/basecoat';
import 'basecoat-css/popover';
import 'basecoat-css/tabs';`}</Code>
#### Without a build tool
<Callout type="warning" title="Stylesheet order matters" icon="triangle-alert">
Load Basecoat after any stylesheet that includes Tailwind base/preflight. Loading another full Tailwind build after Basecoat can reset borders, inputs, and other component defaults.
</Callout>
Copy the pre-built files from `node_modules` into your public directory:
<Code lang="bash">{`npx copyfiles -u 1 "node_modules/basecoat-css/dist/js/**/*" public/js/basecoat`}</Code>
Then reference the script you need as well as the `basecoat.min.js` file (used to register and initialize components):
<Code lang="html">{`<script src="/js/basecoat/basecoat.min.js" defer></script>
<script src="/js/basecoat/tabs.min.js" defer></script>`}</Code>
See each component page for the minimal script required.
</section>
<section>
<h3 class="mb-4 scroll-m-20 font-semibold tracking-tight" id="install-npm-done"><a href="#install-npm-done">Step 5: Use components</a></h3>
You can now use any of the Basecoat classes in your project (e.g. `btn`, `card`, `input`, etc). Read the documentation about each component to get started (e.g. [button](/components/button), [card](/components/card), [input](/components/input), etc).
</Step>
<Step title="Import JavaScript when needed">
Some components (e.g. [Select](/components/select)) require [JavaScript code to work](#install-js).
</section>
</div>
Use the all-in-one import:
## Components with JavaScript
```js
import "basecoat-css/all";
```
**A handful of components require JavaScript code to work**, specifically [Combobox](/components/combobox), [Command](/components/command), [Dropdown Menu](/components/dropdown-menu), [Popover](/components/popover), [Select](/components/select), [Sidebar](/components/sidebar), [Slider](/components/slider), [Tabs](/components/tabs) and [Toast](/components/toast).
Or import the runtime once, then individual components.
If a component requires JavaScript, the documentation page provides the minimal scripts. There are two options:
```js
import "basecoat-css/basecoat";
import "basecoat-css/dropdown-menu";
import "basecoat-css/select";
```
- **All-in-one**: use `dist/js/all.min.js` from the CDN or `import "basecoat-css/all"` with a build tool.
- **Per component**: load `basecoat.min.js` once, then load or import each component script, such as `select.min.js` or `basecoat-css/select`.
</Step>
</Steps>
### CSS imports
| Import | Description |
| --- | --- |
| `basecoat-css` | Default full stylesheet. |
| `basecoat-css/{style}` | Full stylesheet for `vega`, `nova`, `maia`, `lyra`, `mira`, `luma`, `sera`, or `rhea`. |
| `basecoat-css/base` | Base tokens, utilities, and component structure without a style pack. Use this before a custom style. |
| `basecoat-css/components` | All component structure without a style pack. |
| `basecoat-css/components/button` | Individual component CSS. Replace `button` with any component file. |
| `basecoat-css/styles/maia` | Individual style-pack CSS. Use with `basecoat-css/base` or `basecoat-css/components`. |
| `basecoat-css/compat` | Optional pre-1.0 compatibility aliases. Load after Basecoat. |
### JavaScript imports
| Import | Description |
| --- | --- |
| `basecoat-css/all` | All JavaScript components. |
| `basecoat-css/basecoat` | Runtime only. Import once before individual component scripts. |
| `basecoat-css/combobox` | Combobox controller. |
| `basecoat-css/command` | Command controller. |
| `basecoat-css/dropdown-menu` | Dropdown Menu controller. |
| `basecoat-css/popover` | Popover controller. |
| `basecoat-css/range` | Slider/range controller. |
| `basecoat-css/select` | Select controller. |
| `basecoat-css/sidebar` | Sidebar controller. |
| `basecoat-css/tabs` | Tabs controller. |
| `basecoat-css/toast` | Toast controller. |
## JavaScript components
These components need JavaScript for behavior:
- [Combobox](/components/combobox)
- [Command](/components/command)
- [Dropdown Menu](/components/dropdown-menu)
- [Popover](/components/popover)
- [Select](/components/select)
- [Sidebar](/components/sidebar)
- [Slider](/components/slider)
- [Tabs](/components/tabs)
- [Toast](/components/toast)
Each component page shows the minimal scripts or imports required for that component.
## Compatibility
Basecoat 1.0 introduces new style packs and a smaller public API based on root classes plus semantic HTML and documented attributes. If you need pre-1.0 class aliases while migrating, load the compatibility stylesheet after your Basecoat stylesheet.
```css
@import "tailwindcss";
@import "basecoat-css";
@import "basecoat-css/compat";
```
The compatibility stylesheet is intended for the default Basecoat style. It is not a separate style pack.
@@ -1,7 +1,5 @@
# Introduction
## Why Basecoat?
**Basecoat brings the magic of [shadcn/ui](https://ui.shadcn.com/) to any traditional web stack: no React required.**
-255
View File
@@ -1,255 +0,0 @@
---
import { Preview } from "reallysimpledocs/components";
const code = `<div>
<h2>Area Chart - Interactive</h2>
<p>Showing total visitors for the last 3 months</p>
<div class="h-[250px] [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="visitors-chart" aria-label="Visitors by device"></canvas>
</div>
</div>`;
---
<Preview class="w-full" code={code}>
<div class="w-full">
<div class="mb-4">
<h2 class="text-base font-medium">Area Chart - Interactive</h2>
<p class="text-muted-foreground text-sm">Showing total visitors for the last 3 months</p>
</div>
<div class="h-[250px] [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="chart-example-overview" aria-label="Visitors by device"></canvas>
</div>
</div>
</Preview>
<script is:inline src="https://cdn.jsdelivr.net/npm/chart.js/dist/chart.umd.min.js"></script>
<script>
import "../../../src/js/basecoat.js";
import "../../../src/js/chart.js";
const visitorsByDay = [
{ date: "Apr 1", desktop: 222, mobile: 150 },
{ date: "Apr 5", desktop: 373, mobile: 290 },
{ date: "Apr 9", desktop: 59, mobile: 110 },
{ date: "Apr 13", desktop: 342, mobile: 380 },
{ date: "Apr 17", desktop: 446, mobile: 360 },
{ date: "Apr 21", desktop: 137, mobile: 200 },
{ date: "Apr 25", desktop: 215, mobile: 250 },
{ date: "Apr 29", desktop: 315, mobile: 240 },
{ date: "May 3", desktop: 247, mobile: 190 },
{ date: "May 7", desktop: 388, mobile: 300 },
{ date: "May 11", desktop: 335, mobile: 270 },
{ date: "May 15", desktop: 473, mobile: 380 },
{ date: "May 19", desktop: 235, mobile: 180 },
{ date: "May 23", desktop: 252, mobile: 290 },
{ date: "May 27", desktop: 420, mobile: 460 },
{ date: "May 31", desktop: 178, mobile: 230 },
{ date: "Jun 4", desktop: 439, mobile: 380 },
{ date: "Jun 8", desktop: 385, mobile: 320 },
{ date: "Jun 12", desktop: 492, mobile: 420 },
{ date: "Jun 16", desktop: 371, mobile: 310 },
{ date: "Jun 20", desktop: 408, mobile: 450 },
{ date: "Jun 24", desktop: 132, mobile: 180 },
{ date: "Jun 28", desktop: 149, mobile: 200 },
];
const monthlyVisitors = [
{ month: "Jan", desktop: 186, mobile: 80 },
{ month: "Feb", desktop: 305, mobile: 200 },
{ month: "Mar", desktop: 237, mobile: 120 },
{ month: "Apr", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "Jun", desktop: 214, mobile: 140 },
];
const defaultScales = {
y: { beginAtZero: true },
x: { grid: { display: false } },
};
const initChart = (id, config) => {
const canvas = document.getElementById(id);
if (!canvas || canvas.dataset.chartInitialized) return;
window.basecoat.chart(canvas, config);
};
const initChartExamples = () => {
if (!window.Chart || !window.basecoat?.chart) return;
initChart("chart-example-overview", {
type: "line",
labelKey: "date",
legend: true,
data: visitorsByDay,
series: {
desktop: {
label: "Desktop",
color: "var(--chart-2)",
surface: "gradient",
dataset: {
borderWidth: 1.5,
fill: true,
tension: 0.35,
pointRadius: 0,
},
},
mobile: {
label: "Mobile",
color: "var(--chart-1)",
surface: "gradient",
dataset: {
borderWidth: 1.5,
fill: true,
tension: 0.35,
pointRadius: 0,
},
},
},
options: {
interaction: { mode: "index", intersect: false },
scales: {
x: { grid: { display: false }, ticks: { maxTicksLimit: 6 } },
y: { beginAtZero: true, stacked: true, ticks: { display: false } },
},
},
});
initChart("chart-example-visitors", {
type: "bar",
labelKey: "month",
legend: true,
data: monthlyVisitors,
series: {
desktop: { label: "Desktop", color: "var(--chart-1)" },
mobile: { label: "Mobile", color: "var(--chart-2)" },
},
options: { scales: defaultScales },
});
initChart("chart-example-line", {
type: "line",
labelKey: "month",
data: [
{ month: "Jan", revenue: 42 },
{ month: "Feb", revenue: 68 },
{ month: "Mar", revenue: 61 },
{ month: "Apr", revenue: 90 },
{ month: "May", revenue: 112 },
{ month: "Jun", revenue: 128 },
],
series: {
revenue: {
label: "Revenue",
color: "var(--chart-1)",
dataset: { tension: 0, pointRadius: 3, pointHoverRadius: 5 },
},
},
options: { scales: defaultScales },
});
initChart("chart-example-step", {
type: "line",
labelKey: "day",
data: [
{ day: "Mon", queued: 12 },
{ day: "Tue", queued: 18 },
{ day: "Wed", queued: 9 },
{ day: "Thu", queued: 24 },
{ day: "Fri", queued: 16 },
{ day: "Sat", queued: 14 },
],
series: {
queued: {
label: "Queued",
color: "var(--chart-3)",
dataset: { stepped: true, pointRadius: 3 },
},
},
options: { scales: defaultScales },
});
initChart("chart-example-stacked", {
type: "bar",
labelKey: "quarter",
legend: true,
data: [
{ quarter: "Q1", won: 32, open: 44, lost: 12 },
{ quarter: "Q2", won: 48, open: 38, lost: 18 },
{ quarter: "Q3", won: 52, open: 46, lost: 16 },
{ quarter: "Q4", won: 61, open: 40, lost: 14 },
],
series: {
won: { label: "Won", color: "var(--chart-1)" },
open: { label: "Open", color: "var(--chart-2)" },
lost: { label: "Lost", color: "var(--chart-5)" },
},
options: {
scales: {
x: { stacked: true, grid: { display: false } },
y: { stacked: true, beginAtZero: true, ticks: { display: false } },
},
},
});
initChart("chart-example-donut", {
type: "doughnut",
labelKey: "source",
legend: true,
data: [
{ source: "Search", visitors: 275, color: "var(--chart-1)" },
{ source: "Direct", visitors: 200, color: "var(--chart-2)" },
{ source: "Social", visitors: 187, color: "var(--chart-3)" },
{ source: "Referral", visitors: 173, color: "var(--chart-4)" },
{ source: "Email", visitors: 90, color: "var(--chart-5)" },
],
series: {
visitors: { label: "Visitors" },
},
options: {
cutout: "62%",
},
});
initChart("chart-example-radar", {
type: "radar",
labelKey: "channel",
legend: true,
data: [
{ channel: "Acquisition", current: 82, previous: 64 },
{ channel: "Activation", current: 72, previous: 70 },
{ channel: "Retention", current: 66, previous: 58 },
{ channel: "Revenue", current: 88, previous: 75 },
{ channel: "Referral", current: 61, previous: 52 },
],
series: {
current: {
label: "Current",
color: "var(--chart-1)",
surface: { from: 0.22 },
},
previous: {
label: "Previous",
color: "var(--chart-2)",
surface: { from: 0.18 },
},
},
options: {
scales: {
r: {
beginAtZero: true,
grid: { color: "color-mix(in oklab, var(--border) 60%, transparent)" },
angleLines: { color: "color-mix(in oklab, var(--border) 60%, transparent)" },
pointLabels: { color: "var(--muted-foreground)" },
ticks: { display: false },
},
},
},
});
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initChartExamples, { once: true });
} else {
initChartExamples();
}
</script>
-83
View File
@@ -1,83 +0,0 @@
---
import { Preview } from "reallysimpledocs/components";
const barCode = `<div class="h-[220px] [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="visitors-chart" aria-label="Monthly visitors by device"></canvas>
</div>`;
const lineCode = `<div class="h-[220px] [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="revenue-chart" aria-label="Revenue line chart"></canvas>
</div>`;
const stepCode = `<div class="h-[220px] [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="queue-chart" aria-label="Support queue step chart"></canvas>
</div>`;
const stackedCode = `<div class="h-[220px] [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="pipeline-chart" aria-label="Stacked pipeline bar chart"></canvas>
</div>`;
const donutCode = `<div class="h-[220px] [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="traffic-chart" aria-label="Traffic source donut chart"></canvas>
</div>`;
const radarCode = `<div class="h-[260px] [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="channel-chart" aria-label="Channel performance radar chart"></canvas>
</div>`;
---
<div class="grid gap-10">
<section>
<h3>Bar Chart - Multiple</h3>
<Preview class="w-full" code={barCode}>
<div class="h-[220px] w-full [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="chart-example-visitors" aria-label="Monthly visitors by device"></canvas>
</div>
</Preview>
</section>
<section>
<h3>Line Chart - Linear</h3>
<Preview class="w-full" code={lineCode}>
<div class="h-[220px] w-full [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="chart-example-line" aria-label="Revenue line chart"></canvas>
</div>
</Preview>
</section>
<section>
<h3>Line Chart - Step</h3>
<Preview class="w-full" code={stepCode}>
<div class="h-[220px] w-full [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="chart-example-step" aria-label="Support queue step chart"></canvas>
</div>
</Preview>
</section>
<section>
<h3>Bar Chart - Stacked</h3>
<Preview class="w-full" code={stackedCode}>
<div class="h-[220px] w-full [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="chart-example-stacked" aria-label="Stacked pipeline bar chart"></canvas>
</div>
</Preview>
</section>
<section>
<h3>Pie Chart - Donut</h3>
<Preview class="w-full" code={donutCode}>
<div class="h-[220px] w-full [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="chart-example-donut" aria-label="Traffic source donut chart"></canvas>
</div>
</Preview>
</section>
<section>
<h3>Radar Chart</h3>
<Preview class="w-full" code={radarCode}>
<div class="h-[260px] w-full [&_.chart]:h-full [&_.chart]:aspect-auto">
<canvas id="chart-example-radar" aria-label="Channel performance radar chart"></canvas>
</div>
</Preview>
</section>
</div>
+46 -11
View File
@@ -1,4 +1,5 @@
---
import vegaUrl from "../assets/styles/style-vega.css?url";
import novaUrl from "../assets/styles/style-nova.css?url";
import maiaUrl from "../assets/styles/style-maia.css?url";
import lyraUrl from "../assets/styles/style-lyra.css?url";
@@ -8,7 +9,7 @@ import seraUrl from "../assets/styles/style-sera.css?url";
import rheaUrl from "../assets/styles/style-rhea.css?url";
const styleUrls = {
vega: null,
vega: vegaUrl,
nova: novaUrl,
maia: maiaUrl,
lyra: lyraUrl,
@@ -36,29 +37,63 @@ const styleUrls = {
(() => {
const styleSelect = document.getElementById("style-variant-select");
const styleLinkId = "docs-style-variant";
const pendingStyleLinkId = "docs-style-variant-pending";
const styleGateId = "docs-style-gate";
const getStyleLink = () => document.getElementById(styleLinkId);
const clearStylePending = () => {
document.documentElement.removeAttribute("data-style-pending");
document.getElementById(styleGateId)?.remove();
};
const getStyleHref = (style) => {
const href = styleUrls[style];
return href?.startsWith("/docs/src/assets/styles/") ? `${href}?direct` : href;
};
const applyStyle = (style) => {
const nextStyle = Object.prototype.hasOwnProperty.call(styleUrls, style) ? style : "vega";
const href = styleUrls[nextStyle];
let link = getStyleLink();
document.documentElement.dataset.styleVariant = nextStyle;
const href = getStyleHref(nextStyle);
const link = getStyleLink();
if (!href) {
link?.remove();
document.getElementById(pendingStyleLinkId)?.remove();
clearStylePending();
return;
}
if (!link) {
link = document.createElement("link");
link.id = styleLinkId;
link.rel = "stylesheet";
document.head.appendChild(link);
const sameHref = link?.getAttribute("href") === href;
if (sameHref || link?.sheet) {
clearStylePending();
}
if (sameHref) return;
if (link.href !== href) link.href = href;
document.getElementById(pendingStyleLinkId)?.remove();
const pendingLink = document.createElement("link");
pendingLink.id = pendingStyleLinkId;
pendingLink.rel = "stylesheet";
pendingLink.addEventListener(
"load",
() => {
link?.remove();
pendingLink.id = styleLinkId;
document.documentElement.dataset.styleVariant = nextStyle;
clearStylePending();
document.dispatchEvent(new CustomEvent("basecoat:stylechange", { detail: { style: nextStyle } }));
},
{ once: true },
);
pendingLink.addEventListener(
"error",
() => {
pendingLink.remove();
clearStylePending();
},
{ once: true },
);
pendingLink.href = href;
document.head.appendChild(pendingLink);
};
const getStoredStyle = () => {
+80
View File
@@ -0,0 +1,80 @@
---
import vegaUrl from "../assets/styles/style-vega.css?url";
import novaUrl from "../assets/styles/style-nova.css?url";
import maiaUrl from "../assets/styles/style-maia.css?url";
import lyraUrl from "../assets/styles/style-lyra.css?url";
import miraUrl from "../assets/styles/style-mira.css?url";
import lumaUrl from "../assets/styles/style-luma.css?url";
import seraUrl from "../assets/styles/style-sera.css?url";
import rheaUrl from "../assets/styles/style-rhea.css?url";
const styleUrls = {
vega: vegaUrl,
nova: novaUrl,
maia: maiaUrl,
lyra: lyraUrl,
mira: miraUrl,
luma: lumaUrl,
sera: seraUrl,
rhea: rheaUrl,
};
---
<style id="docs-style-gate" is:inline>
html[data-style-pending] body {
visibility: hidden;
}
</style>
<script is:inline define:vars={{ styleUrls }}>
(() => {
const styleGateId = "docs-style-gate";
const styleLinkId = "docs-style-variant";
const clearStylePending = () => {
document.documentElement.removeAttribute("data-style-pending");
document.getElementById(styleGateId)?.remove();
};
const getStoredStyle = () => {
try {
return localStorage.getItem("styleVariant") || "vega";
} catch (_) {
return "vega";
}
};
const getStyleHref = (style) => {
const href = styleUrls[style];
return href?.startsWith("/docs/src/assets/styles/") ? `${href}?direct` : href;
};
const style = getStoredStyle();
const nextStyle = Object.prototype.hasOwnProperty.call(styleUrls, style) ? style : "vega";
const href = getStyleHref(nextStyle);
document.documentElement.dataset.styleVariant = nextStyle;
if (!href) return;
document.documentElement.setAttribute("data-style-pending", "");
const existing = document.getElementById("docs-style-variant");
if (existing?.getAttribute("href") === href) {
clearStylePending();
return;
}
if (!existing && document.currentScript) {
document.write(
`<link id="${styleLinkId}" rel="stylesheet" href="${href}" onload="document.documentElement.removeAttribute('data-style-pending');document.getElementById('${styleGateId}')?.remove()" onerror="document.documentElement.removeAttribute('data-style-pending');document.getElementById('${styleGateId}')?.remove()">`,
);
return;
}
const link = existing || document.createElement("link");
link.id = styleLinkId;
link.rel = "stylesheet";
link.addEventListener("load", clearStylePending, { once: true });
link.addEventListener("error", clearStylePending, { once: true });
link.href = href;
if (!existing) document.head.appendChild(link);
})();
</script>
+230
View File
@@ -0,0 +1,230 @@
import "../../../src/js/basecoat.js";
import "../../../src/js/chart.js";
const visitorsByDay = [
{ date: "Apr 1", desktop: 222, mobile: 150 },
{ date: "Apr 5", desktop: 373, mobile: 290 },
{ date: "Apr 9", desktop: 59, mobile: 110 },
{ date: "Apr 13", desktop: 342, mobile: 380 },
{ date: "Apr 17", desktop: 446, mobile: 360 },
{ date: "Apr 21", desktop: 137, mobile: 200 },
{ date: "Apr 25", desktop: 215, mobile: 250 },
{ date: "Apr 29", desktop: 315, mobile: 240 },
{ date: "May 3", desktop: 247, mobile: 190 },
{ date: "May 7", desktop: 388, mobile: 300 },
{ date: "May 11", desktop: 335, mobile: 270 },
{ date: "May 15", desktop: 473, mobile: 380 },
{ date: "May 19", desktop: 235, mobile: 180 },
{ date: "May 23", desktop: 252, mobile: 290 },
{ date: "May 27", desktop: 420, mobile: 460 },
{ date: "May 31", desktop: 178, mobile: 230 },
{ date: "Jun 4", desktop: 439, mobile: 380 },
{ date: "Jun 8", desktop: 385, mobile: 320 },
{ date: "Jun 12", desktop: 492, mobile: 420 },
{ date: "Jun 16", desktop: 371, mobile: 310 },
{ date: "Jun 20", desktop: 408, mobile: 450 },
{ date: "Jun 24", desktop: 132, mobile: 180 },
{ date: "Jun 28", desktop: 149, mobile: 200 },
];
const monthlyVisitors = [
{ month: "Jan", desktop: 186, mobile: 80 },
{ month: "Feb", desktop: 305, mobile: 200 },
{ month: "Mar", desktop: 237, mobile: 120 },
{ month: "Apr", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "Jun", desktop: 214, mobile: 140 },
];
const defaultScales = {
y: { beginAtZero: true },
x: { grid: { display: false } },
};
const initChart = (id, config) => {
const canvas = document.getElementById(id);
if (!canvas || canvas.dataset.chartInitialized) return;
window.basecoat.chart(canvas, config);
};
const initChartExamples = () => {
if (!window.Chart || !window.basecoat?.chart) return;
initChart("chart-example-overview", {
type: "line",
labelKey: "date",
legend: true,
data: visitorsByDay,
series: {
desktop: {
label: "Desktop",
color: "var(--chart-2)",
surface: "gradient",
dataset: {
borderWidth: 1.5,
fill: true,
tension: 0.35,
pointRadius: 0,
},
},
mobile: {
label: "Mobile",
color: "var(--chart-1)",
surface: "gradient",
dataset: {
borderWidth: 1.5,
fill: true,
tension: 0.35,
pointRadius: 0,
},
},
},
options: {
interaction: { mode: "index", intersect: false },
scales: {
x: { grid: { display: false }, ticks: { maxTicksLimit: 6 } },
y: { beginAtZero: true, stacked: true, ticks: { display: false } },
},
},
});
initChart("chart-example-visitors", {
type: "bar",
labelKey: "month",
legend: true,
data: monthlyVisitors,
series: {
desktop: { label: "Desktop", color: "var(--chart-1)" },
mobile: { label: "Mobile", color: "var(--chart-2)" },
},
options: { scales: defaultScales },
});
initChart("chart-example-line", {
type: "line",
labelKey: "month",
data: [
{ month: "Jan", revenue: 42 },
{ month: "Feb", revenue: 68 },
{ month: "Mar", revenue: 61 },
{ month: "Apr", revenue: 90 },
{ month: "May", revenue: 112 },
{ month: "Jun", revenue: 128 },
],
series: {
revenue: {
label: "Revenue",
color: "var(--chart-1)",
dataset: { borderWidth: 1.5, tension: 0, pointRadius: 0, pointHoverRadius: 4 },
},
},
options: { scales: defaultScales },
});
initChart("chart-example-step", {
type: "line",
labelKey: "day",
data: [
{ day: "Mon", queued: 12 },
{ day: "Tue", queued: 18 },
{ day: "Wed", queued: 9 },
{ day: "Thu", queued: 24 },
{ day: "Fri", queued: 16 },
{ day: "Sat", queued: 14 },
],
series: {
queued: {
label: "Queued",
color: "var(--chart-1)",
dataset: { borderWidth: 1.5, stepped: true, pointRadius: 0, pointHoverRadius: 4 },
},
},
options: { scales: defaultScales },
});
initChart("chart-example-stacked", {
type: "bar",
labelKey: "quarter",
legend: true,
data: [
{ quarter: "Q1", won: 32, open: 44, lost: 12 },
{ quarter: "Q2", won: 48, open: 38, lost: 18 },
{ quarter: "Q3", won: 52, open: 46, lost: 16 },
{ quarter: "Q4", won: 61, open: 40, lost: 14 },
],
series: {
won: { label: "Won", color: "var(--chart-1)" },
open: { label: "Open", color: "var(--chart-2)" },
lost: { label: "Lost", color: "var(--chart-3)" },
},
options: {
scales: {
x: { stacked: true, grid: { display: false } },
y: { stacked: true, beginAtZero: true, ticks: { display: false } },
},
},
});
initChart("chart-example-donut", {
type: "doughnut",
labelKey: "source",
legend: true,
data: [
{ source: "Search", visitors: 275, color: "var(--chart-1)" },
{ source: "Direct", visitors: 200, color: "var(--chart-2)" },
{ source: "Social", visitors: 187, color: "var(--chart-3)" },
{ source: "Referral", visitors: 173, color: "var(--chart-4)" },
{ source: "Email", visitors: 90, color: "var(--chart-5)" },
],
series: {
visitors: { label: "Visitors" },
},
options: {
cutout: "62%",
},
});
initChart("chart-example-radar", {
type: "radar",
labelKey: "channel",
legend: true,
data: [
{ channel: "Acquisition", current: 82, previous: 64 },
{ channel: "Activation", current: 72, previous: 70 },
{ channel: "Retention", current: 66, previous: 58 },
{ channel: "Revenue", current: 88, previous: 75 },
{ channel: "Referral", current: 61, previous: 52 },
],
series: {
current: {
label: "Current",
color: "var(--chart-1)",
surface: { from: 0.22 },
dataset: { borderWidth: 1.5, pointRadius: 0, pointHoverRadius: 4 },
},
previous: {
label: "Previous",
color: "var(--chart-2)",
surface: { from: 0.18 },
dataset: { borderWidth: 1.5, pointRadius: 0, pointHoverRadius: 4 },
},
},
options: {
scales: {
r: {
beginAtZero: true,
grid: { color: "color-mix(in oklab, var(--border) 60%, transparent)" },
angleLines: { color: "color-mix(in oklab, var(--border) 60%, transparent)" },
pointLabels: { color: "var(--muted-foreground)" },
ticks: { display: false },
},
},
},
});
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initChartExamples, { once: true });
} else {
initChartExamples();
}
+2 -2
View File
@@ -25,11 +25,11 @@ Legend:
| dialog | done | `src/css/components/dialog.css` | `apps/v4/registry/bases/base/ui/dialog.tsx` | all 8 `style-*.css` | Shared CSS now keeps native dialog mechanics, backdrop/open transitions, centered content layout, structural header/body/footer, and close-button structure only. All eight style files map upstream overlay, content, close position, header, footer, title, and description visuals onto Basecoat `.dialog` selectors. Intentional divergence: Basecoat uses the native `<dialog>` element and `showModal()` instead of shadcn/ui's portalled Base UI Dialog. Docs aligned with upstream sections: composition, custom close button, no close button, sticky footer, scrollable content, RTL, and API guidance. |
| dropdown-menu | done | `src/css/components/dropdown-menu.css` | `apps/v4/registry/bases/base/ui/dropdown-menu.tsx` | all 8 `style-*.css` | Shared CSS now keeps structure, roles, disabled states, shortcut/indicator slots, and fallback hover only. All eight style files map upstream content, item, checkbox/radio item, indicator, label, separator, shortcut, destructive, and inset visuals onto Basecoat role/data selectors. Intentional divergence: Basecoat dropdown content remains inline-positioned and does not implement upstream submenus/avatar/complex nested examples yet. Docs aligned with upstream sections for basic, shortcuts, icons, checkboxes, radio, destructive, RTL, and API guidance. |
| empty | done | `src/css/components/empty.css` | `apps/v4/registry/bases/base/ui/empty.tsx` | all 8 `style-*.css` | Added lightweight `.empty` root API using semantic `header`, `figure`, `section`, and `footer` children instead of shadcn slot classes. All eight style files map upstream empty root, header, media/icon, title, description, and content visuals onto Basecoat selectors. Docs now treat Empty as a real Basecoat component and include outline, avatar, avatar group, input group, and RTL examples. |
| field | done | `src/css/components/field.css` | `apps/v4/registry/bases/base/ui/field.tsx` | all 8 `style-*.css` | Shared CSS now covers field, fieldset, semantic field groups, field-description, field-error, and field-separator structure. All eight style files map upstream field visuals onto existing Basecoat selectors, including `cn-field-label` checked/card behavior via `.field > label` and `label:has(> .field)`. Docs include semantic field groups, separator, choice card, responsive layout, and RTL. |
| field | done | `src/css/components/field.css` | `apps/v4/registry/bases/base/ui/field.tsx` | all 8 `style-*.css` | Shared CSS now covers field, fieldset, semantic field groups, semantic descriptions/errors, and field-separator structure. All eight style files map upstream field visuals onto existing Basecoat selectors, including `cn-field-label` checked/card behavior via `.field > label` and `label:has(> .field)`. Docs include semantic field groups, separator, choice card, responsive layout, and RTL. |
| input | done | `src/css/components/input.css` | `apps/v4/registry/bases/base/ui/input.tsx` | all 8 `style-*.css` | Shared CSS now keeps structural input behavior only. All eight style files map upstream input visuals onto Basecoat `.input` and `.field` selectors. Docs aligned with upstream sections where applicable: basic, field, field group, disabled, invalid, file, inline, grid, required, badge, input group, button group, and form. RTL is supported through native direction handling and logical utility usage in examples. |
| input-group | done | `src/css/components/input-group.css` | `apps/v4/styles/base-*/ui/input-group.tsx` | all 8 `style-*.css` | Added lightweight `.input-group` root API. Children remain semantic native controls and elements; optional `data-align="inline-start|inline-end|block-start|block-end"` maps upstream addon placement without shadcn slot classes. `data-control` is the escape hatch for custom non-native controls. |
| item | done | `src/css/components/item.css` | `apps/v4/registry/bases/base/ui/item.tsx` | all 8 `style-*.css` | Added lightweight `.item` and `.item-group` APIs using semantic `figure`, `section`, `aside`, `header`, and `footer` children instead of shadcn slot classes. All eight style files map upstream item variants, sizes, media, content, title, description, actions, header/footer, group, and separator visuals onto Basecoat selectors. Docs now use the component API and include variants, sizes, icon, avatar, image, group, header, link, and RTL examples. |
| kbd | done | `src/css/components/kbd.css` | `apps/v4/registry/bases/base/ui/kbd.tsx` | all 8 `style-*.css` | Added `kbd-group` and mapped upstream kbd/kbd-group styles across all eight style files. Docs include button, input-group, and RTL examples. |
| kbd | done | `src/css/components/kbd.css` | `apps/v4/registry/bases/base/ui/kbd.tsx` | all 8 `style-*.css` | Kept `kbd` as the public component and dropped `kbd-group`; use plain inline wrappers for grouped keys. Docs include button, input-group, and RTL examples. |
| label | done | `src/css/components/label.css` | `apps/v4/registry/bases/base/ui/label.tsx` | all 8 `style-*.css` | Shared CSS now keeps label structure and disabled behavior. All eight style files map upstream label visuals. Sera control-label exceptions are mapped with Basecoat native input selectors instead of shadcn peer data-slot selectors. Docs include RTL. |
| native-select | done | `src/css/components/native-select.css` | `apps/v4/registry/bases/base/ui/native-select.tsx` | all 8 `style-*.css` | Shared CSS now keeps native select structure only. All eight style files map upstream native select visuals onto Basecoat `select.select` and `.field select`, using a CSS background chevron instead of upstream's wrapper icon element. Intentional divergence: direct selects stay `w-fit` by default because Basecoat has no `w-fit` wrapper; width utilities remain supported. Docs split Native Select from custom Select and include groups, disabled, invalid, size, RTL, and API guidance. |
| popover | done | `src/css/components/popover.css` | `apps/v4/registry/bases/base/ui/popover.tsx` | all 8 `style-*.css` | Shared CSS now keeps positioning, visibility, side, and align behavior only. All eight style files map upstream popover content/header/title/description visuals onto Basecoat `[data-popover]` and semantic header selectors. Intentional divergence: Basecoat popover remains inline-positioned relative to `.popover` rather than portalled through Base UI; animation utilities from upstream are omitted because this Tailwind setup does not include shadcn animation utilities. Docs aligned with upstream sections: composition, basic, align, form, RTL, and API guidance. |
+8 -14
View File
@@ -1,12 +1,12 @@
{
"name": "basecoat",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "basecoat",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"license": "MIT",
"workspaces": [
"packages/*"
@@ -27,30 +27,24 @@
}
},
"../reallysimpledocs": {
"version": "1.0.0-beta.4",
"version": "1.0.0-beta.2",
"dev": true,
"dependencies": {
"@astrojs/markdown-remark": "^7.2.0",
"@astrojs/mdx": "^6.0.3",
"@tailwindcss/vite": "4.1.17",
"astro": "^6.4.6",
"github-slugger": "^2.0.0",
"lucide-static": "^0.552.0",
"lunr": "^2.3.9",
"marked": "^16.4.1",
"mdast-util-gfm": "^3.1.0",
"mdast-util-to-markdown": "^2.1.2",
"remark-gfm": "^4.0.1",
"remark-mdx": "^3.1.1",
"remark-parse": "^11.0.0",
"shiki": "^4.1.0",
"unified": "^11.0.5"
"shiki": "^4.1.0"
},
"devDependencies": {
"astro": "^6.4.6",
"basecoat-css": "1.0.0-beta.2",
"tailwindcss": "4.1.17"
},
"peerDependencies": {
"astro": "^6.4.6",
"basecoat-css": "^1.0.0-beta.2",
"tailwindcss": ">=4.1.17 <5"
}
@@ -8956,7 +8950,7 @@
},
"packages/cli": {
"name": "basecoat-cli",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"license": "MIT",
"dependencies": {
"commander": "^13.1.0",
@@ -8969,7 +8963,7 @@
},
"packages/css": {
"name": "basecoat-css",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"license": "MIT"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "basecoat",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"description": "shadcn/ui for the rest of us",
"author": "hunvreus",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "basecoat-cli",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"description": "Add Basecoat components to your project",
"author": "hunvreus",
"license": "MIT",
+5 -1
View File
@@ -1,6 +1,6 @@
{
"name": "basecoat-css",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"description": "Tailwind CSS for Basecoat components",
"author": {
"name": "hunvreus",
@@ -68,6 +68,10 @@
"./base": "./dist/basecoat-base.css",
"./base.css": "./dist/basecoat-base.css",
"./base.cdn.css": "./dist/basecoat-base.cdn.css",
"./compat": "./dist/basecoat-compat.css",
"./compat.css": "./dist/basecoat-compat.css",
"./compat.cdn.css": "./dist/basecoat-compat.cdn.css",
"./compat/legacy.css": "./dist/compat/legacy.css",
"./components": "./dist/basecoat-components.css",
"./components.css": "./dist/basecoat-components.css",
"./components/*": "./dist/components/*.css",
+6 -1
View File
@@ -80,6 +80,7 @@ async function build() {
const srcDir = path.join(projectRoot, 'src');
const srcCssDir = path.join(srcDir, 'css');
const srcCssStylesDir = path.join(srcCssDir, 'styles');
const srcCssCompatDir = path.join(srcCssDir, 'compat');
const srcJsDir = path.join(srcDir, 'js');
const srcNunjucksDir = path.join(srcDir, 'nunjucks');
const srcJinjaDir = path.join(srcDir, 'jinja');
@@ -156,6 +157,8 @@ async function build() {
await fs.copyFile(path.join(srcCssDir, 'basecoat.all.css'), path.join(cssDistDir, 'basecoat.all.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat-base.css'), path.join(cssDistDir, 'basecoat-base.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat-base.cdn.css'), path.join(cssDistDir, 'basecoat-base.cdn.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat-compat.css'), path.join(cssDistDir, 'basecoat-compat.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat-compat.cdn.css'), path.join(cssDistDir, 'basecoat-compat.cdn.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat-components.css'), path.join(cssDistDir, 'basecoat-components.css'));
for (const style of styles) {
await fs.copyFile(path.join(srcCssDir, `basecoat-${style}.css`), path.join(cssDistDir, `basecoat-${style}.css`));
@@ -169,13 +172,15 @@ async function build() {
const cssBaseDistDir = path.join(cssDistDir, 'base');
const cssComponentsDistDir = path.join(cssDistDir, 'components');
const cssStylesDistDir = path.join(cssDistDir, 'styles');
const cssCompatDistDir = path.join(cssDistDir, 'compat');
await copyDirRecursive(cssBaseSrcDir, cssBaseDistDir);
await copyDirRecursive(cssComponentsSrcDir, cssComponentsDistDir);
await copyDirRecursive(srcCssStylesDir, cssStylesDistDir);
await copyDirRecursive(srcCssCompatDir, cssCompatDistDir);
console.log(`Copied split CSS folders to ${cssDistDir}`);
// Create Tailwind CSS builds for the CSS package.
const cdnEntries = ['basecoat.cdn.css', 'basecoat-base.cdn.css', ...styles.map((style) => `basecoat-${style}.cdn.css`)];
const cdnEntries = ['basecoat.cdn.css', 'basecoat-base.cdn.css', 'basecoat-compat.cdn.css', ...styles.map((style) => `basecoat-${style}.cdn.css`)];
for (const entry of cdnEntries) {
const cdnCssSrc = path.join(srcCssDir, entry);
const baseName = path.basename(entry, '.css');
+4 -1
View File
@@ -22,12 +22,12 @@ const componentOrder = [
'field',
'checkbox',
'input',
'input-group',
'item',
'kbd',
'label',
'native-select',
'popover',
'progress',
'radio',
'range',
'select',
@@ -37,6 +37,7 @@ const componentOrder = [
'table',
'tabs',
'textarea',
'input-group',
'toast',
'tooltip',
];
@@ -93,6 +94,8 @@ export async function generateCssEntrypoints({ cssDir = path.resolve('src/css')
await writeIfChanged(path.join(cssDir, 'basecoat.css'), '@import "./basecoat-vega.css";\n');
await writeIfChanged(path.join(cssDir, 'basecoat.all.css'), '@import "./basecoat.css";\n');
await writeIfChanged(path.join(cssDir, 'basecoat.cdn.css'), '@import "tailwindcss" source(none);\n@import "./basecoat.css";\n');
await writeIfChanged(path.join(cssDir, 'basecoat-compat.css'), '@import "./compat/legacy.css";\n');
await writeIfChanged(path.join(cssDir, 'basecoat-compat.cdn.css'), '@import "tailwindcss" source(none);\n@reference "./basecoat.css";\n@import "./basecoat-compat.css";\n');
return { components, styles };
}
+10 -10
View File
@@ -20,11 +20,11 @@
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--chart-1: #93c5fd;
--chart-2: #3b82f6;
--chart-3: #2563eb;
--chart-4: #1d4ed8;
--chart-5: #1e40af;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
@@ -64,11 +64,11 @@
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--chart-1: #93c5fd;
--chart-2: #3b82f6;
--chart-3: #2563eb;
--chart-4: #1d4ed8;
--chart-5: #1e40af;
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
+3
View File
@@ -0,0 +1,3 @@
@import "tailwindcss" source(none);
@reference "./basecoat.css";
@import "./basecoat-compat.css";
+1
View File
@@ -0,0 +1 @@
@import "./compat/legacy.css";
+2 -1
View File
@@ -15,12 +15,12 @@
@import "./components/field.css";
@import "./components/checkbox.css";
@import "./components/input.css";
@import "./components/input-group.css";
@import "./components/item.css";
@import "./components/kbd.css";
@import "./components/label.css";
@import "./components/native-select.css";
@import "./components/popover.css";
@import "./components/progress.css";
@import "./components/radio.css";
@import "./components/range.css";
@import "./components/select.css";
@@ -30,5 +30,6 @@
@import "./components/table.css";
@import "./components/tabs.css";
@import "./components/textarea.css";
@import "./components/input-group.css";
@import "./components/toast.css";
@import "./components/tooltip.css";
+429
View File
@@ -0,0 +1,429 @@
/* Legacy Basecoat class API compatibility.
This layer preserves pre-1.0 visual classes for the default Basecoat style only. */
@layer components {
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive,
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive,
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive,
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive,
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive,
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
@apply inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0;
}
}
/* Button */
@layer components {
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive,
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive,
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive,
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive,
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive,
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
@apply rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*='size-'])]:size-4;
}
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive {
@apply h-9 gap-1.5 px-2.5;
}
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive {
@apply h-6 gap-1 px-2 text-xs rounded-[min(var(--radius-md),8px)] [&_svg:not([class*='size-'])]:size-3;
}
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive {
@apply h-8 gap-1 px-2.5 rounded-[min(var(--radius-md),10px)];
}
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive {
@apply h-10 gap-1.5 px-2.5;
}
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive {
@apply size-9;
}
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive {
@apply size-6 rounded-[min(var(--radius-md),8px)] [&_svg:not([class*='size-'])]:size-3;
}
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive {
@apply size-8 rounded-[min(var(--radius-md),10px)];
}
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
@apply size-10;
}
.btn-primary,
.btn-xs,
.btn-xs-primary,
.btn-sm,
.btn-sm-primary,
.btn-lg,
.btn-lg-primary,
.btn-icon,
.btn-icon-primary,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-lg-icon,
.btn-lg-icon-primary {
@apply bg-primary text-primary-foreground hover:bg-primary/80;
}
.btn-secondary,
.btn-xs-secondary,
.btn-sm-secondary,
.btn-lg-secondary,
.btn-icon-secondary,
.btn-xs-icon-secondary,
.btn-sm-icon-secondary,
.btn-lg-icon-secondary {
@apply bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground;
}
.btn-outline,
.btn-xs-outline,
.btn-sm-outline,
.btn-lg-outline,
.btn-icon-outline,
.btn-xs-icon-outline,
.btn-sm-icon-outline,
.btn-lg-icon-outline {
@apply border-border bg-background shadow-xs hover:bg-muted hover:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-ghost,
.btn-xs-ghost,
.btn-sm-ghost,
.btn-lg-ghost,
.btn-icon-ghost,
.btn-xs-icon-ghost,
.btn-sm-icon-ghost,
.btn-lg-icon-ghost {
@apply hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-link,
.btn-xs-link,
.btn-sm-link,
.btn-lg-link,
.btn-icon-link,
.btn-xs-icon-link,
.btn-sm-icon-link,
.btn-lg-icon-link {
@apply text-primary underline-offset-4 hover:underline;
}
.btn-destructive,
.btn-xs-destructive,
.btn-sm-destructive,
.btn-lg-destructive,
.btn-icon-destructive,
.btn-xs-icon-destructive,
.btn-sm-icon-destructive,
.btn-lg-icon-destructive {
@apply bg-destructive/10 text-destructive hover:bg-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40;
}
}
/* Badge legacy aliases */
@layer components {
.badge-primary,
.badge-secondary,
.badge-destructive,
.badge-outline,
.badge-ghost,
.badge-link {
@apply inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none;
@apply h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all [&>svg]:size-3!;
}
.badge-primary {
@apply bg-primary text-primary-foreground [a]:hover:bg-primary/80;
}
.badge-secondary {
@apply bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80;
}
.badge-outline {
@apply border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground;
}
.badge-destructive {
@apply bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20;
}
.badge-ghost {
@apply hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50;
}
.badge-link {
@apply text-primary underline-offset-4 hover:underline;
}
}
/* Alert legacy aliases */
@layer components {
.alert-destructive {
@apply relative w-full;
@apply grid gap-0.5 rounded-lg border px-4 py-3 text-start text-sm has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 [&>svg]:row-span-2 [&>svg]:translate-y-0.5 [&>svg]:text-current [&>svg:not([class*='size-'])]:size-4;
@apply text-destructive bg-card;
}
.alert-destructive:has(> footer) {
@apply pe-18;
}
.alert-destructive > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply min-h-4 font-medium;
}
.alert-destructive:has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply col-start-2;
}
.alert-destructive > section {
@apply grid justify-items-start gap-1 text-destructive/90 text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
ul {
@apply list-inside list-disc;
}
}
.alert-destructive > footer {
@apply absolute top-2.5 end-3;
}
}
/* Card legacy aliases */
@layer components {
.card-sm {
@apply flex flex-col;
@apply ring-foreground/10 bg-card text-card-foreground overflow-hidden rounded-xl text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 [&>img:first-child]:rounded-t-xl [&>img:last-child]:rounded-b-xl;
@apply gap-4 py-4;
> header {
@apply @container/card-header grid auto-rows-min items-start has-[>[data-slot=card-action],>.card-action]:grid-cols-[1fr_auto] has-[>p]:grid-rows-[auto_auto];
@apply gap-1 rounded-t-xl px-4 [.border-b]:pb-4;
}
> header > :is(h2, h3, [data-title], .card-title) {
@apply text-sm leading-normal font-medium;
}
> header > :is(p, [data-description], .card-description) {
@apply text-muted-foreground text-sm;
}
> header > :is([data-slot='card-action'], .card-action) {
@apply col-start-2 row-span-2 row-start-1 self-start justify-self-end;
}
> section {
@apply px-4;
}
> footer {
@apply flex items-center rounded-b-xl px-4 [.border-t]:pt-4;
}
}
}
/* Avatar legacy aliases */
@layer components {
.avatar-sm,
.avatar-lg {
@apply relative inline-flex shrink-0 select-none appearance-none border-0 bg-transparent p-0 align-middle outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 after:absolute after:inset-0 after:rounded-[inherit] after:border after:border-border after:mix-blend-darken dark:after:mix-blend-lighten;
@apply rounded-full after:rounded-[inherit];
}
.avatar-sm {
@apply size-6;
}
.avatar-lg {
@apply size-10;
}
.avatar-sm > img,
.avatar-lg > img {
@apply absolute inset-0 aspect-square size-full rounded-[inherit] object-cover;
}
.avatar-sm > span:not([data-badge]),
.avatar-lg > span:not([data-badge]) {
@apply flex size-full items-center justify-center rounded-[inherit] bg-muted text-muted-foreground;
}
.avatar-sm > span:not([data-badge]) {
@apply text-xs;
}
.avatar-sm > [data-badge],
.avatar-lg > [data-badge] {
@apply absolute end-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none;
@apply bg-primary text-primary-foreground ring-background;
}
.avatar-sm > [data-badge] {
@apply size-2 [&>svg]:hidden;
}
.avatar-lg > [data-badge] {
@apply size-3 [&>svg]:size-2;
}
.avatar-group > :is(.avatar-sm, .avatar-lg) {
@apply ring-2 ring-background;
}
}
+6 -11
View File
@@ -1,22 +1,18 @@
/* Alert */
@layer components {
.alert,
.alert-destructive {
.alert {
@apply relative w-full;
}
.alert:has(> footer),
.alert-destructive:has(> footer) {
@apply pe-18;
.alert:has(> footer) {
@apply pe-[4.5rem];
}
.alert > :is(h2, h3, h4, h5, h6, strong, [data-title]),
.alert-destructive > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply min-h-4;
}
.alert > section,
.alert-destructive > section {
.alert > section {
@apply grid justify-items-start gap-1;
ul {
@@ -24,8 +20,7 @@
}
}
.alert > footer,
.alert-destructive > footer {
.alert > footer {
@apply absolute;
}
}
+8 -16
View File
@@ -1,34 +1,26 @@
/* Avatar */
@layer components {
.avatar,
.avatar-sm,
.avatar-lg {
.avatar {
@apply relative inline-flex shrink-0 select-none appearance-none border-0 bg-transparent p-0 align-middle outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 after:absolute after:inset-0 after:rounded-[inherit] after:border after:border-border after:mix-blend-darken dark:after:mix-blend-lighten;
}
.avatar > img,
.avatar-sm > img,
.avatar-lg > img {
.avatar > img {
@apply absolute inset-0 aspect-square size-full rounded-[inherit] object-cover;
}
.avatar > span:not([data-badge]),
.avatar-sm > span:not([data-badge]),
.avatar-lg > span:not([data-badge]) {
.avatar > span:not([data-badge]) {
@apply flex size-full items-center justify-center rounded-[inherit] text-sm;
}
.avatar-sm > span:not([data-badge]) {
.avatar[data-size='sm'] > span:not([data-badge]) {
@apply text-xs;
}
.avatar > [data-badge],
.avatar-sm > [data-badge],
.avatar-lg > [data-badge] {
.avatar > [data-badge] {
@apply absolute end-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none;
}
.avatar-sm > [data-badge] {
.avatar[data-size='sm'] > [data-badge] {
@apply size-2 [&>svg]:hidden;
}
@@ -36,7 +28,7 @@
@apply size-2.5 [&>svg]:size-2;
}
.avatar-lg > [data-badge] {
.avatar[data-size='lg'] > [data-badge] {
@apply size-3 [&>svg]:size-2;
}
@@ -44,7 +36,7 @@
@apply flex -space-x-2;
}
.avatar-group > :is(.avatar, .avatar-sm, .avatar-lg),
.avatar-group > .avatar,
.avatar-group > [data-count] {
@apply ring-2 ring-background;
}
+1 -7
View File
@@ -1,12 +1,6 @@
/* Badge */
@layer components {
.badge,
.badge-primary,
.badge-secondary,
.badge-destructive,
.badge-outline,
.badge-ghost,
.badge-link {
.badge {
@apply inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none;
}
}
+2 -6
View File
@@ -16,7 +16,7 @@
@apply relative z-10;
}
> :is(hr[role='separator'], .button-group-separator) {
> hr[role='separator'] {
@apply relative w-px h-auto self-stretch shrink-0 border-0 my-px mx-0;
}
@@ -33,7 +33,7 @@
&[data-orientation='vertical'] {
@apply flex-col;
> :is(hr[role='separator'], .button-group-separator) {
> hr[role='separator'] {
@apply w-auto h-px mx-px my-0;
}
@@ -51,8 +51,4 @@
.button-group > :is(span, label, output) {
@apply flex items-center [&_svg]:pointer-events-none;
}
.button-group-separator {
@apply relative self-stretch shrink-0;
}
}
+1 -56
View File
@@ -1,61 +1,6 @@
/* Button */
@layer components {
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive,
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive,
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive,
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive,
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive,
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn {
@apply inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0;
}
}
+1 -2
View File
@@ -1,7 +1,6 @@
/* Card */
@layer components {
.card,
.card-sm {
.card {
@apply flex flex-col;
> header {
+9 -1
View File
@@ -24,8 +24,16 @@
@apply border-0;
}
.dropdown-menu :is([data-shortcut], kbd:last-child, .kbd:last-child) {
@apply ms-auto shrink-0 text-end;
}
.dropdown-menu [data-shortcut] {
@apply ms-auto;
@apply ps-8;
}
.dropdown-menu :is(kbd:last-child, .kbd:last-child):not([data-shortcut]) {
@apply ms-8;
}
.dropdown-menu [data-indicator] {
+1 -1
View File
@@ -20,7 +20,7 @@
@apply flex w-full max-w-sm min-w-0 flex-col items-center text-balance;
}
.empty p {
.empty > header > p {
@apply [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary;
}
}
+7 -11
View File
@@ -10,29 +10,26 @@
}
.field {
@apply flex w-full flex-col data-[invalid=true]:text-destructive [&>*]:w-full [&>.sr-only]:w-auto;
@apply flex w-full flex-col data-invalid:text-destructive data-[invalid=true]:text-destructive [&>*]:w-full [&>.sr-only]:w-auto;
&[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;
@apply flex-row items-center [&>*]:w-auto [&>label]:flex-auto has-[>section]:items-start has-[>section]:[&>section]:flex-1 has-[>section]:[&>[type=checkbox],[type=radio]]:mt-px [&_p]:text-balance;
}
&[data-orientation='responsive'] {
@apply flex-col @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto @md/field-group:has-[>section]:items-start @md/field-group:[&>label]:flex-auto @md/field-group:has-[>section]:[&>[type=checkbox],[type=radio]]:mt-px;
}
section,
.field-content {
section {
@apply flex flex-1 flex-col leading-snug;
}
h2,
h3,
.field-title {
h3 {
@apply flex w-fit items-center;
}
[role='alert'],
.field-error {
[role='alert'] {
@apply font-normal;
ul {
@@ -42,6 +39,7 @@
}
.field > label,
.field > section > label,
label:has(> .field) {
@apply w-fit;
}
@@ -59,7 +57,6 @@
@apply flex-col;
}
.field-description,
.fieldset legend + p,
.field > p,
.field section > p {
@@ -67,8 +64,7 @@
}
.field > p,
.field section > p,
.field-description {
.field section > p {
@apply last:mt-0 nth-last-2:-mt-1;
}
+30 -6
View File
@@ -1,15 +1,17 @@
/* Input Group */
@layer components {
.input-group {
@apply relative isolate flex w-full min-w-0 items-center overflow-hidden outline-none;
@apply relative isolate flex w-full min-w-0 items-center outline-none;
&[data-orientation='vertical'] {
&[data-orientation='vertical'],
&:has(> [data-align='block-start']),
&:has(> [data-align='block-end']) {
@apply h-auto flex-col items-stretch;
}
}
.input-group > :is(input, textarea, select, [data-control]) {
@apply text-foreground placeholder:text-muted-foreground block min-w-0 flex-1 appearance-none rounded-none border-0 bg-transparent shadow-none outline-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 disabled:bg-transparent;
@apply text-foreground placeholder:text-muted-foreground block min-w-0 flex-1 appearance-none rounded-none border-0 bg-transparent shadow-none outline-none ring-0 focus-visible:ring-0 aria-invalid:ring-0;
}
.input-group > :is(input, select, [data-control]) {
@@ -20,10 +22,20 @@
@apply w-full;
}
.input-group[data-orientation='vertical'] > :is(input, select, [data-control]) {
.input-group:is([data-orientation='vertical'], :has(> [data-align='block-start']), :has(> [data-align='block-end'])) > :is(input, select, [data-control]) {
@apply h-auto flex-none;
}
.input-group[data-orientation='vertical']:has(> footer[data-align='end']) > :is(input, select, [data-control]),
.input-group:has(> [data-align='block-end']) > :is(input, select, [data-control]) {
@apply pt-3;
}
.input-group[data-orientation='vertical']:has(> header[data-align='start']) > :is(input, select, [data-control]),
.input-group:has(> [data-align='block-start']) > :is(input, select, [data-control]) {
@apply pb-3;
}
.input-group > textarea {
@apply field-sizing-content min-h-20 w-full resize-none;
}
@@ -40,11 +52,23 @@
@apply pointer-events-none;
}
.input-group > [data-align='start'] {
.input-group > :is([data-align='start'], [data-align='inline-start'], [data-align='block-start']) {
@apply order-first;
}
.input-group > [data-align='end'] {
.input-group > :is([data-align='end'], [data-align='inline-end'], [data-align='block-end']) {
@apply order-last;
}
.input-group > :is([data-align='block-start'], [data-align='block-end']) {
@apply w-full justify-start;
}
.input-group > button:is([data-align='start'], [data-align='inline-start']) {
@apply ms-1;
}
.input-group > button:is([data-align='end'], [data-align='inline-end']) {
@apply me-1;
}
}
+1 -1
View File
@@ -20,7 +20,7 @@
@apply flex shrink-0 items-center justify-center [&_svg]:pointer-events-none;
}
.item > figure:has(img) {
.item > figure:has(> img) {
@apply overflow-hidden [&_img]:size-full [&_img]:object-cover;
}
-4
View File
@@ -3,8 +3,4 @@
.kbd {
@apply pointer-events-none inline-flex items-center justify-center select-none;
}
.kbd-group {
@apply inline-flex items-center;
}
}
+1
View File
@@ -1,6 +1,7 @@
/* Label */
@layer components {
.field > label,
.field > section > label,
.label {
@apply flex items-center select-none peer-disabled:pointer-events-none;
+14
View File
@@ -0,0 +1,14 @@
/* Progress */
@layer components {
.progress {
@apply flex w-full items-center overflow-hidden;
}
.progress:dir(rtl) {
@apply flex-row-reverse;
}
.progress > span {
@apply h-full transition-all;
}
}
+44
View File
@@ -0,0 +1,44 @@
@layer theme, base, components, utilities;
@import "tailwindcss" source("../pages");
.theme-neutral {
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
}
:root {
--landing-demo-surface-radius: 0.375rem;
--chart-bar-radius: 6px;
}
[data-style-variant="nova"] {
--landing-demo-surface-radius: 0.5rem;
}
[data-style-variant="maia"],
[data-style-variant="luma"],
[data-style-variant="rhea"] {
--landing-demo-surface-radius: 1rem;
}
[data-style-variant="lyra"],
[data-style-variant="sera"] {
--landing-demo-surface-radius: 0;
--chart-bar-radius: 0;
}
.landing-demo-surface {
border-radius: var(--landing-demo-surface-radius);
}
.theme-neutral .card > footer,
.theme-neutral hr {
border-color: var(--border);
}
.landing-demo-chart-bar {
border-start-start-radius: var(--chart-bar-radius);
border-start-end-radius: var(--chart-bar-radius);
}
+145 -299
View File
@@ -1,247 +1,64 @@
/* Button */
@layer components {
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive,
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive,
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive,
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive,
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive,
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn {
@apply rounded-4xl border border-transparent bg-clip-padding text-sm font-medium focus-visible:border-ring focus-visible:ring-ring/30 focus-visible:ring-[3px] aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*='size-'])]:size-4;
}
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive {
@apply h-9 gap-1.5 px-3;
&:has(> [data-icon=inline-start]) {
@apply ps-2.5;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2.5;
}
.btn:not([data-size]),
.btn[data-size='default'] {
@apply h-9 gap-1.5 px-3 has-[[data-icon='inline-end']]:pr-2.5 has-[[data-icon='inline-start']]:pl-2.5;
}
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive {
@apply h-6 gap-1 px-2.5 text-xs [&_svg:not([class*='size-'])]:size-3;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn[data-size='xs'] {
@apply h-6 gap-1 px-2.5 text-xs [&_svg:not([class*='size-'])]:size-3 has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive {
@apply h-8 gap-1 px-3;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn[data-size='sm'] {
@apply h-8 gap-1 px-3 has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive {
@apply h-10 gap-1.5 px-4;
&:has(> [data-icon=inline-start]) {
@apply ps-3;
}
&:has(> [data-icon=inline-end]) {
@apply pe-3;
}
.btn[data-size='lg'] {
@apply h-10 gap-1.5 px-4 has-[[data-icon='inline-end']]:pr-3 has-[[data-icon='inline-start']]:pl-3;
}
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive {
.btn[data-size='icon'] {
@apply size-9;
}
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive {
.btn[data-size='icon-xs'] {
@apply size-6 [&_svg:not([class*='size-'])]:size-3;
}
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive {
.btn[data-size='icon-sm'] {
@apply size-8;
}
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn[data-size='icon-lg'] {
@apply size-10;
}
.btn,
.btn-primary,
.btn-xs,
.btn-xs-primary,
.btn-sm,
.btn-sm-primary,
.btn-lg,
.btn-lg-primary,
.btn-icon,
.btn-icon-primary,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-lg-icon,
.btn-lg-icon-primary {
.btn:not([data-variant]),
.btn[data-variant='primary'] {
@apply bg-primary text-primary-foreground hover:bg-primary/80;
}
.btn-secondary,
.btn-xs-secondary,
.btn-sm-secondary,
.btn-lg-secondary,
.btn-icon-secondary,
.btn-xs-icon-secondary,
.btn-sm-icon-secondary,
.btn-lg-icon-secondary {
.btn[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground;
}
.btn-outline,
.btn-xs-outline,
.btn-sm-outline,
.btn-lg-outline,
.btn-icon-outline,
.btn-xs-icon-outline,
.btn-sm-icon-outline,
.btn-lg-icon-outline {
.btn[data-variant='outline'] {
@apply border-border bg-background hover:bg-muted hover:text-foreground dark:bg-transparent dark:hover:bg-input/30 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-ghost,
.btn-xs-ghost,
.btn-sm-ghost,
.btn-lg-ghost,
.btn-icon-ghost,
.btn-xs-icon-ghost,
.btn-sm-icon-ghost,
.btn-lg-icon-ghost {
.btn[data-variant='ghost'] {
@apply hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-link,
.btn-xs-link,
.btn-sm-link,
.btn-lg-link,
.btn-icon-link,
.btn-xs-icon-link,
.btn-sm-icon-link,
.btn-lg-icon-link {
.btn[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
.btn-destructive,
.btn-xs-destructive,
.btn-sm-destructive,
.btn-lg-destructive,
.btn-icon-destructive,
.btn-xs-icon-destructive,
.btn-sm-icon-destructive,
.btn-lg-icon-destructive {
.btn[data-variant='destructive'] {
@apply bg-destructive/10 text-destructive hover:bg-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40;
}
@@ -250,8 +67,8 @@
@apply has-[>.button-group]:gap-2;
}
.button-group:has(> :is(.btn-outline, .btn-icon-outline, .btn-xs-outline, .btn-xs-icon-outline, .btn-sm-outline, .btn-sm-icon-outline, .btn-lg-outline, .btn-lg-icon-outline)),
.button-group:has(> .select > button:is(.btn-outline, .btn-icon-outline, .btn-xs-outline, .btn-xs-icon-outline, .btn-sm-outline, .btn-sm-icon-outline, .btn-lg-outline, .btn-lg-icon-outline)) {
.button-group:has(> .btn[data-variant='outline']),
.button-group:has(> .select > button.btn[data-variant='outline']) {
> .input,
> .select > button {
@apply border-border focus-visible:border-ring;
@@ -276,8 +93,7 @@
@apply bg-muted gap-2 rounded-4xl border px-2.5 text-sm font-medium [&_svg:not([class*='size-'])]:size-4;
}
.button-group > hr[role='separator'],
.button-group-separator {
.button-group > hr[role='separator'] {
@apply bg-input;
}
@@ -389,8 +205,7 @@
}
/* Alert */
.alert,
.alert-destructive {
.alert {
@apply grid gap-0.5 rounded-2xl border px-4 py-3 text-start text-sm has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 [&>svg]:row-span-2 [&>svg]:translate-y-0.5 [&>svg]:text-current [&>svg:not([class*='size-'])]:size-4;
}
@@ -398,35 +213,33 @@
@apply bg-card text-card-foreground;
}
.alert-destructive {
.alert[data-variant='destructive'] {
@apply text-destructive bg-card [&>svg]:text-current;
}
.alert > :is(h2, h3, h4, h5, h6, strong, [data-title]),
.alert-destructive > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert[data-variant='destructive'] > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply font-medium;
}
:is(.alert, .alert-destructive):has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert:has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply col-start-2;
}
.alert > section {
@apply text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert-destructive > section {
.alert[data-variant='destructive'] > section {
@apply text-destructive/90 text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert > footer,
.alert-destructive > footer {
.alert[data-variant='destructive'] > footer {
@apply top-2.5 end-3;
}
/* Avatar */
.avatar,
.avatar-sm,
.avatar-lg {
.avatar {
@apply rounded-full after:rounded-[inherit];
}
@@ -434,29 +247,23 @@
@apply size-8;
}
.avatar-sm {
.avatar[data-size='sm'] {
@apply size-6;
}
.avatar-lg {
.avatar[data-size='lg'] {
@apply size-10;
}
.avatar > span:not([data-badge]),
.avatar-sm > span:not([data-badge]),
.avatar-lg > span:not([data-badge]) {
.avatar > span:not([data-badge]) {
@apply bg-muted text-muted-foreground rounded-[inherit];
}
.avatar > img,
.avatar-sm > img,
.avatar-lg > img {
.avatar > img {
@apply rounded-[inherit];
}
.avatar > [data-badge],
.avatar-sm > [data-badge],
.avatar-lg > [data-badge] {
.avatar > [data-badge] {
@apply bg-primary text-primary-foreground ring-background;
}
@@ -464,47 +271,41 @@
@apply bg-muted text-muted-foreground size-8 rounded-full text-sm [&>svg]:size-4;
}
.avatar-group:has(> .avatar-sm) > [data-count] {
.avatar-group:has(> .avatar[data-size='sm']) > [data-count] {
@apply size-6 [&>svg]:size-3;
}
.avatar-group:has(> .avatar-lg) > [data-count] {
.avatar-group:has(> .avatar[data-size='lg']) > [data-count] {
@apply size-10 [&>svg]:size-5;
}
/* Badge */
.badge,
.badge-primary,
.badge-secondary,
.badge-destructive,
.badge-outline,
.badge-ghost,
.badge-link {
@apply h-5 gap-1 rounded-3xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&>svg]:size-3!;
.badge {
@apply h-5 gap-1 rounded-3xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all [&>svg]:size-3!;
}
.badge,
.badge-primary {
.badge:not([data-variant]),
.badge[data-variant='primary'] {
@apply bg-primary text-primary-foreground [a]:hover:bg-primary/80;
}
.badge-secondary {
.badge[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80;
}
.badge-outline {
.badge[data-variant='outline'] {
@apply border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground;
}
.badge-destructive {
.badge[data-variant='destructive'] {
@apply bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20;
}
.badge-ghost {
.badge[data-variant='ghost'] {
@apply hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50;
}
.badge-link {
.badge[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
@@ -598,7 +399,7 @@
.combobox [role='option'].active,
.combobox [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
.combobox [role='group'] {
@@ -658,7 +459,7 @@
*:not(select).select [role='option'].active,
*:not(select).select [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
*:not(select).select [role='group'] {
@@ -716,7 +517,7 @@
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']).active {
@apply bg-accent text-accent-foreground;
@apply bg-muted text-foreground;
}
.dropdown-menu [data-variant='destructive'] {
@@ -752,11 +553,11 @@
@apply bg-border/50 -mx-1.5 my-1.5 h-px;
}
.dropdown-menu [data-shortcut] {
@apply text-muted-foreground text-xs tracking-widest;
.dropdown-menu :is([data-shortcut], kbd:last-child) {
@apply text-muted-foreground font-sans text-xs tracking-widest;
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) [data-shortcut] {
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) :is([data-shortcut], kbd:last-child) {
@apply text-accent-foreground;
}
@@ -780,8 +581,7 @@
}
/* Card */
.card,
.card-sm {
.card {
@apply bg-card text-card-foreground ring-foreground/5 dark:ring-foreground/10 overflow-hidden rounded-4xl text-sm shadow-md ring-1 has-[>img:first-child]:pt-0 [&>img:first-child]:rounded-t-4xl [&>img:last-child]:rounded-b-4xl;
}
@@ -789,48 +589,47 @@
@apply gap-6 py-6;
}
.card-sm {
.card[data-size='sm'] {
@apply gap-4 py-4;
}
.card > header,
.card-sm > header {
.card > header {
@apply gap-1.5 rounded-t-4xl px-6 [.border-b]:pb-6;
}
.card-sm > header {
.card[data-size='sm'] > header {
@apply px-4 [.border-b]:pb-4;
}
.card > header > :is(h2, h3, [data-title], .card-title),
.card-sm > header > :is(h2, h3, [data-title], .card-title) {
.card > header > :is(h2, h3, [data-title], .card-title) {
@apply text-base font-medium;
}
.card > header > :is(p, [data-description], .card-description),
.card-sm > header > :is(p, [data-description], .card-description) {
.card > header > :is(p, [data-description], .card-description) {
@apply text-muted-foreground text-sm;
}
.card > section,
.card-sm > section {
.card > section {
@apply px-6;
}
.card-sm > section {
.card[data-size='sm'] > section {
@apply px-4;
}
.card > footer,
.card-sm > footer {
.card > footer {
@apply rounded-b-4xl px-6 [.border-t]:pt-6;
}
.card-sm > footer {
.card[data-size='sm'] > footer {
@apply px-4 [.border-t]:pt-4;
}
/* Chart */
.chart {
--chart-bar-radius: 6px;
}
.chart-tooltip {
@apply border-border/50 bg-background text-foreground gap-1.5 rounded-2xl border px-2.5 py-1.5 text-xs shadow-xl;
}
@@ -923,8 +722,7 @@
@apply gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3;
}
.fieldset > legend,
.field-legend {
.fieldset > legend {
@apply mb-3 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base;
}
@@ -933,6 +731,8 @@
@apply gap-7;
&[role='radiogroup'],
&:has(> .field > input[type='checkbox']:not([role='switch'])),
&:has(> .field > input[type='radio']),
&[data-slot='checkbox-group'],
&[data-slot='radio-group'] {
@apply gap-3;
@@ -945,15 +745,15 @@
}
.field {
@apply data-[invalid=true]:text-destructive gap-3;
@apply data-invalid:text-destructive data-[invalid=true]:text-destructive gap-3;
}
.field > section,
.field-content {
.field > section {
@apply gap-1;
}
.field > label,
.field > section > label,
label:has(> .field) {
@apply has-[:checked]:bg-input/30 gap-2 leading-snug in-data-[disabled=true]:opacity-50;
}
@@ -976,18 +776,25 @@
@apply p-4;
}
.field h2,
.field h3,
.field-title {
.field h3 {
@apply gap-2 leading-snug text-sm font-medium in-data-[disabled=true]:opacity-50;
}
.fieldset legend + p,
.field > p,
.field section > p,
.field-description {
.field section > p {
@apply text-muted-foreground text-start text-sm [[data-variant=legend]+&]:-mt-1.5;
}
.fieldset > legend[data-variant='label'] {
@apply mb-0;
}
.fieldset > legend:not([data-variant]) + p,
.fieldset > legend[data-variant='legend'] + p {
@apply -mt-1.5;
}
.field-separator {
@apply -my-2 h-5 text-sm in-data-[variant=outline]:-mb-2;
}
@@ -996,8 +803,7 @@
@apply text-muted-foreground px-2;
}
.field [role="alert"],
.field-error {
.field [role="alert"] {
@apply text-destructive text-sm;
}
@@ -1006,23 +812,22 @@
@apply h-9 has-[>textarea]:h-auto rounded-4xl border border-transparent bg-input/50 transition-[color,box-shadow,background-color] focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/30 has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:ring-3 has-[[aria-invalid=true]]:ring-destructive/20 has-[>textarea]:rounded-2xl dark:has-[[aria-invalid=true]]:ring-destructive/40;
}
.input-group[data-orientation='vertical'] {
@apply rounded-3xl;
}
.input-group:has(> :is(input, textarea, select, [data-control]):disabled) {
@apply opacity-50;
}
.input-group[data-orientation='vertical'] {
@apply rounded-3xl;
}
.input-group > :is(input, select, [data-control]) {
@apply px-3 py-1 text-sm;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='start']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='start'], [data-align='inline-start'])) > :is(input, select, [data-control]) {
@apply ps-1.5;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='end']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='end'], [data-align='inline-end'])) > :is(input, select, [data-control]) {
@apply pe-1.5;
}
@@ -1035,33 +840,66 @@
@apply gap-2 py-2 text-sm font-medium text-muted-foreground select-none [&>svg:not([class*='size-'])]:size-4;
}
.input-group > [data-align='start'] {
.input-group > :is([data-align='start'], [data-align='inline-start']) {
@apply ps-3;
}
.input-group > [data-align='end'] {
.input-group > :is([data-align='end'], [data-align='inline-end']) {
@apply pe-3;
}
.input-group > [data-align='start']:has(> button) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> button) {
@apply -ms-1;
}
.input-group > [data-align='end']:has(> button) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> button) {
@apply -me-1;
}
.input-group > [data-align='start']:has(> kbd) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> kbd) {
@apply -ms-0.5;
}
.input-group > [data-align='end']:has(> kbd) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> kbd) {
@apply -me-0.5;
}
.input-group[data-orientation='vertical'] > :is(header, footer, [role='group'], [data-align]) {
@apply px-3;
}
.input-group > [data-align='block-start'] {
@apply px-3 pt-2;
}
.input-group > [data-align='block-end'] {
@apply px-3 pb-2;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']) {
@apply px-3 pt-3;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']) {
@apply px-3 pb-3;
}
.input-group[data-orientation='vertical']:has(> input) > :is(header[data-align='start'], [data-align='block-start']) {
@apply pt-3.5;
}
.input-group[data-orientation='vertical']:has(> input) > :is(footer[data-align='end'], [data-align='block-end']) {
@apply pb-3.5;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']).border-b {
@apply pb-3.5;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']).border-t {
@apply pt-3.5;
}
.input-group > :is(kbd, span > kbd, [role='group'] > kbd, header > kbd, footer > kbd) {
@apply rounded-3xl bg-muted-foreground/10 px-1.5;
}
@@ -1096,7 +934,7 @@
@apply text-lg font-medium tracking-tight;
}
.empty p {
.empty > header > p {
@apply text-muted-foreground text-sm/relaxed;
}
@@ -1147,15 +985,15 @@
@apply [&_svg:not([class*='size-'])]:size-4;
}
.item > figure:has(img) {
.item > figure:has(> img) {
@apply size-10 overflow-hidden rounded-xl;
}
.item[data-size='sm'] > figure:has(img) {
.item[data-size='sm'] > figure:has(> img) {
@apply size-8;
}
.item[data-size='xs'] > figure:has(img) {
.item[data-size='xs'] > figure:has(> img) {
@apply size-6 rounded-lg;
}
@@ -1205,16 +1043,16 @@
@apply bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 h-5.5 w-fit min-w-5.5 gap-1 rounded-lg px-1.5 font-sans text-xs font-medium [&_svg:not([class*='size-'])]:size-3;
}
.kbd-group {
@apply gap-1;
}
/* Label */
.field > label,
.label {
@apply gap-2 text-sm leading-none font-medium in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
.field > section > label {
@apply gap-2 text-sm leading-snug font-medium in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
/* Sidebar */
.sidebar nav {
@apply bg-sidebar text-sidebar-foreground;
@@ -1304,6 +1142,14 @@
.sidebar nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-xl px-3 text-sm focus-visible:ring-2 [&>svg]:size-4;
}
/* Progress */
.progress {
@apply bg-muted h-3 rounded-full;
}
.progress > span {
@apply bg-primary;
}
/* Skeleton */
.skeleton {
+150 -295
View File
@@ -1,247 +1,64 @@
/* Button */
@layer components {
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive,
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive,
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive,
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive,
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive,
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn {
@apply rounded-none border border-transparent bg-clip-padding text-xs font-medium focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[1px] aria-invalid:border-destructive aria-invalid:ring-[1px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*='size-'])]:size-4;
}
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive {
@apply h-8 gap-1.5 px-2.5;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn:not([data-size]),
.btn[data-size='default'] {
@apply h-8 gap-1.5 px-2.5 has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive {
@apply h-6 gap-1 px-2 text-xs rounded-none [&_svg:not([class*='size-'])]:size-3;
&:has(> [data-icon=inline-start]) {
@apply ps-1.5;
}
&:has(> [data-icon=inline-end]) {
@apply pe-1.5;
}
.btn[data-size='xs'] {
@apply h-6 gap-1 px-2 text-xs rounded-none [&_svg:not([class*='size-'])]:size-3 has-[[data-icon='inline-end']]:pr-1.5 has-[[data-icon='inline-start']]:pl-1.5;
}
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive {
@apply h-7 gap-1 px-2.5 rounded-none [&_svg:not([class*='size-'])]:size-3.5;
&:has(> [data-icon=inline-start]) {
@apply ps-1.5;
}
&:has(> [data-icon=inline-end]) {
@apply pe-1.5;
}
.btn[data-size='sm'] {
@apply h-7 gap-1 px-2.5 rounded-none [&_svg:not([class*='size-'])]:size-3.5 has-[[data-icon='inline-end']]:pr-1.5 has-[[data-icon='inline-start']]:pl-1.5;
}
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive {
@apply h-9 gap-1.5 px-2.5;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn[data-size='lg'] {
@apply h-9 gap-1.5 px-2.5 has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive {
.btn[data-size='icon'] {
@apply size-8;
}
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive {
.btn[data-size='icon-xs'] {
@apply size-6 rounded-none [&_svg:not([class*='size-'])]:size-3;
}
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive {
.btn[data-size='icon-sm'] {
@apply size-7 rounded-none;
}
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn[data-size='icon-lg'] {
@apply size-9;
}
.btn,
.btn-primary,
.btn-xs,
.btn-xs-primary,
.btn-sm,
.btn-sm-primary,
.btn-lg,
.btn-lg-primary,
.btn-icon,
.btn-icon-primary,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-lg-icon,
.btn-lg-icon-primary {
.btn:not([data-variant]),
.btn[data-variant='primary'] {
@apply bg-primary text-primary-foreground hover:bg-primary/80;
}
.btn-secondary,
.btn-xs-secondary,
.btn-sm-secondary,
.btn-lg-secondary,
.btn-icon-secondary,
.btn-xs-icon-secondary,
.btn-sm-icon-secondary,
.btn-lg-icon-secondary {
.btn[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground;
}
.btn-outline,
.btn-xs-outline,
.btn-sm-outline,
.btn-lg-outline,
.btn-icon-outline,
.btn-xs-icon-outline,
.btn-sm-icon-outline,
.btn-lg-icon-outline {
.btn[data-variant='outline'] {
@apply border-border bg-background hover:bg-muted hover:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-ghost,
.btn-xs-ghost,
.btn-sm-ghost,
.btn-lg-ghost,
.btn-icon-ghost,
.btn-xs-icon-ghost,
.btn-sm-icon-ghost,
.btn-lg-icon-ghost {
.btn[data-variant='ghost'] {
@apply hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-link,
.btn-xs-link,
.btn-sm-link,
.btn-lg-link,
.btn-icon-link,
.btn-xs-icon-link,
.btn-sm-icon-link,
.btn-lg-icon-link {
.btn[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
.btn-destructive,
.btn-xs-destructive,
.btn-sm-destructive,
.btn-lg-destructive,
.btn-icon-destructive,
.btn-xs-icon-destructive,
.btn-sm-icon-destructive,
.btn-lg-icon-destructive {
.btn[data-variant='destructive'] {
@apply bg-destructive/10 text-destructive hover:bg-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40;
}
@@ -268,8 +85,7 @@
@apply bg-muted gap-2 rounded-none border px-2.5 text-xs font-medium [&_svg:not([class*='size-'])]:size-4;
}
.button-group > hr[role='separator'],
.button-group-separator {
.button-group > hr[role='separator'] {
@apply bg-input;
}
@@ -381,8 +197,7 @@
}
/* Alert */
.alert,
.alert-destructive {
.alert {
@apply grid gap-0.5 rounded-none border px-2.5 py-2 text-start text-xs has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 [&>svg]:row-span-2 [&>svg]:translate-y-0 [&>svg]:text-current [&>svg:not([class*='size-'])]:size-4;
}
@@ -390,35 +205,33 @@
@apply bg-card text-card-foreground;
}
.alert-destructive {
.alert[data-variant='destructive'] {
@apply text-destructive bg-card [&>svg]:text-current;
}
.alert > :is(h2, h3, h4, h5, h6, strong, [data-title]),
.alert-destructive > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert[data-variant='destructive'] > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply font-medium;
}
:is(.alert, .alert-destructive):has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert:has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply col-start-2;
}
.alert > section {
@apply text-muted-foreground text-xs/relaxed text-balance md:text-pretty [&_p:not(:last-child)]:mb-2;
}
.alert-destructive > section {
.alert[data-variant='destructive'] > section {
@apply text-destructive/90 text-xs/relaxed text-balance md:text-pretty [&_p:not(:last-child)]:mb-2;
}
.alert > footer,
.alert-destructive > footer {
.alert[data-variant='destructive'] > footer {
@apply top-[calc(--spacing(1.25))] end-[calc(--spacing(1.25))];
}
/* Avatar */
.avatar,
.avatar-sm,
.avatar-lg {
.avatar {
@apply rounded-full after:rounded-[inherit];
}
@@ -426,29 +239,23 @@
@apply size-8;
}
.avatar-sm {
.avatar[data-size='sm'] {
@apply size-6;
}
.avatar-lg {
.avatar[data-size='lg'] {
@apply size-10;
}
.avatar > span:not([data-badge]),
.avatar-sm > span:not([data-badge]),
.avatar-lg > span:not([data-badge]) {
.avatar > span:not([data-badge]) {
@apply bg-muted text-muted-foreground rounded-[inherit];
}
.avatar > img,
.avatar-sm > img,
.avatar-lg > img {
.avatar > img {
@apply rounded-[inherit];
}
.avatar > [data-badge],
.avatar-sm > [data-badge],
.avatar-lg > [data-badge] {
.avatar > [data-badge] {
@apply bg-primary text-primary-foreground ring-background;
}
@@ -456,47 +263,41 @@
@apply bg-muted text-muted-foreground size-8 rounded-full text-xs [&>svg]:size-4;
}
.avatar-group:has(> .avatar-sm) > [data-count] {
.avatar-group:has(> .avatar[data-size='sm']) > [data-count] {
@apply size-6 [&>svg]:size-3;
}
.avatar-group:has(> .avatar-lg) > [data-count] {
.avatar-group:has(> .avatar[data-size='lg']) > [data-count] {
@apply size-10 [&>svg]:size-5;
}
/* Badge */
.badge,
.badge-primary,
.badge-secondary,
.badge-destructive,
.badge-outline,
.badge-ghost,
.badge-link {
@apply h-5 gap-1 rounded-none border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&>svg]:size-3!;
.badge {
@apply h-5 gap-1 rounded-none border border-transparent px-2 py-0.5 text-xs font-medium transition-all [&>svg]:size-3!;
}
.badge,
.badge-primary {
.badge:not([data-variant]),
.badge[data-variant='primary'] {
@apply bg-primary text-primary-foreground [a]:hover:bg-primary/80;
}
.badge-secondary {
.badge[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80;
}
.badge-outline {
.badge[data-variant='outline'] {
@apply border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground;
}
.badge-destructive {
.badge[data-variant='destructive'] {
@apply bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20;
}
.badge-ghost {
.badge[data-variant='ghost'] {
@apply hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50;
}
.badge-link {
.badge[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
@@ -590,7 +391,7 @@
.combobox [role='option'].active,
.combobox [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
.combobox [role='separator'] {
@@ -646,7 +447,7 @@
*:not(select).select [role='option'].active,
*:not(select).select [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
*:not(select).select [role='group'] {
@@ -700,7 +501,7 @@
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']).active {
@apply bg-accent text-accent-foreground;
@apply bg-muted text-foreground;
}
.dropdown-menu [data-variant='destructive'] {
@@ -736,11 +537,11 @@
@apply bg-border -mx-1 h-px;
}
.dropdown-menu [data-shortcut] {
@apply text-muted-foreground text-xs tracking-widest;
.dropdown-menu :is([data-shortcut], kbd:last-child) {
@apply text-muted-foreground font-sans text-xs tracking-widest;
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) [data-shortcut] {
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) :is([data-shortcut], kbd:last-child) {
@apply text-accent-foreground;
}
@@ -764,8 +565,7 @@
}
/* Card */
.card,
.card-sm {
.card {
@apply ring-foreground/10 bg-card text-card-foreground overflow-hidden rounded-none text-xs/relaxed ring-1 has-[>footer]:pb-0 has-[>img:first-child]:pt-0 [&>img:first-child]:rounded-none [&>img:last-child]:rounded-none;
}
@@ -773,52 +573,51 @@
@apply gap-4 py-4;
}
.card-sm {
.card[data-size='sm'] {
@apply gap-2 py-3;
}
.card > header,
.card-sm > header {
.card > header {
@apply gap-1 rounded-none px-4 [.border-b]:pb-4;
}
.card-sm > header {
.card[data-size='sm'] > header {
@apply px-3 [.border-b]:pb-3;
}
.card > header > :is(h2, h3, [data-title], .card-title),
.card-sm > header > :is(h2, h3, [data-title], .card-title) {
.card > header > :is(h2, h3, [data-title], .card-title) {
@apply text-sm font-medium;
}
.card-sm > header > :is(h2, h3, [data-title], .card-title) {
.card[data-size='sm'] > header > :is(h2, h3, [data-title], .card-title) {
@apply text-sm;
}
.card > header > :is(p, [data-description], .card-description),
.card-sm > header > :is(p, [data-description], .card-description) {
.card > header > :is(p, [data-description], .card-description) {
@apply text-muted-foreground text-xs/relaxed;
}
.card > section,
.card-sm > section {
.card > section {
@apply px-4;
}
.card-sm > section {
.card[data-size='sm'] > section {
@apply px-3;
}
.card > footer,
.card-sm > footer {
.card > footer {
@apply rounded-none border-t p-4;
}
.card-sm > footer {
.card[data-size='sm'] > footer {
@apply p-3;
}
/* Chart */
.chart {
--chart-bar-radius: 0;
}
.chart-tooltip {
@apply border-border/50 bg-background text-foreground gap-1.5 rounded-none border px-2.5 py-1.5 text-xs shadow-xl;
}
@@ -911,8 +710,7 @@
@apply gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3;
}
.fieldset > legend,
.field-legend {
.fieldset > legend {
@apply mb-2.5 font-medium data-[variant=label]:text-xs data-[variant=legend]:text-sm;
}
@@ -921,6 +719,8 @@
@apply gap-5;
&[role='radiogroup'],
&:has(> .field > input[type='checkbox']:not([role='switch'])),
&:has(> .field > input[type='radio']),
&[data-slot='checkbox-group'],
&[data-slot='radio-group'] {
@apply gap-3;
@@ -933,15 +733,15 @@
}
.field {
@apply data-[invalid=true]:text-destructive gap-2;
@apply data-invalid:text-destructive data-[invalid=true]:text-destructive gap-2;
}
.field > section,
.field-content {
.field > section {
@apply gap-0.5;
}
.field > label,
.field > section > label,
label:has(> .field) {
@apply has-[:checked]:bg-primary/5 has-[:checked]:border-primary/30 dark:has-[:checked]:border-primary/20 dark:has-[:checked]:bg-primary/10 gap-2 leading-snug in-data-[disabled=true]:opacity-50;
}
@@ -964,18 +764,25 @@
@apply p-2;
}
.field h2,
.field h3,
.field-title {
.field h3 {
@apply gap-2 text-xs/relaxed in-data-[disabled=true]:opacity-50;
}
.fieldset legend + p,
.field > p,
.field section > p,
.field-description {
.field section > p {
@apply text-muted-foreground text-start text-xs/relaxed [[data-variant=legend]+&]:-mt-1.5;
}
.fieldset > legend[data-variant='label'] {
@apply mb-0;
}
.fieldset > legend:not([data-variant]) + p,
.fieldset > legend[data-variant='legend'] + p {
@apply -mt-1.5;
}
.field-separator {
@apply -my-2 h-5 text-xs in-data-[variant=outline]:-mb-2;
}
@@ -984,8 +791,7 @@
@apply text-muted-foreground px-2;
}
.field [role="alert"],
.field-error {
.field [role="alert"] {
@apply text-destructive text-xs;
}
@@ -996,18 +802,18 @@
.input-group:has(> :is(input, textarea, select, [data-control]):disabled) {
@apply opacity-50;
@apply bg-input/50 opacity-50 dark:bg-input/80;
}
.input-group > :is(input, select, [data-control]) {
@apply px-2.5 py-1 text-xs;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='start']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='start'], [data-align='inline-start'])) > :is(input, select, [data-control]) {
@apply ps-1.5;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='end']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='end'], [data-align='inline-end'])) > :is(input, select, [data-control]) {
@apply pe-1.5;
}
@@ -1020,33 +826,74 @@
@apply gap-2 py-1.5 text-xs font-medium text-muted-foreground select-none [&>svg:not([class*='size-'])]:size-4;
}
.input-group > [data-align='start'] {
.input-group > :is([data-align='start'], [data-align='inline-start']) {
@apply ps-2;
}
.input-group > [data-align='end'] {
.input-group > :is([data-align='end'], [data-align='inline-end']) {
@apply pe-2;
}
.input-group > [data-align='start']:has(> button) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> button) {
@apply -ms-1;
}
.input-group > [data-align='end']:has(> button) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> button) {
@apply -me-1;
}
.input-group > [data-align='start']:has(> kbd) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> kbd) {
@apply -ms-0.5;
}
.input-group > [data-align='end']:has(> kbd) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> kbd) {
@apply -me-0.5;
}
.input-group > [data-align='block-start'] {
@apply px-2.5 pt-2;
}
.input-group > [data-align='block-end'] {
@apply px-2.5 pb-2;
}
.input-group > [data-align='block-start'].border-b {
@apply pb-2;
}
.input-group > [data-align='block-end'].border-t {
@apply pt-2;
}
.input-group[data-orientation='vertical'] > :is(header, footer, [role='group'], [data-align]) {
@apply px-2.5;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']) {
@apply px-2.5 pt-2;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']) {
@apply px-2.5 pb-2;
}
.input-group[data-orientation='vertical']:has(> input) > :is(header[data-align='start'], [data-align='block-start']) {
@apply pt-2;
}
.input-group[data-orientation='vertical']:has(> input) > :is(footer[data-align='end'], [data-align='block-end']) {
@apply pb-2;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']).border-b {
@apply pb-2;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']).border-t {
@apply pt-2;
}
.input-group > :is(kbd, span > kbd, [role='group'] > kbd, header > kbd, footer > kbd) {
@apply rounded-none;
}
@@ -1081,7 +928,7 @@
@apply text-sm font-medium;
}
.empty p {
.empty > header > p {
@apply text-muted-foreground text-xs/relaxed;
}
@@ -1132,15 +979,15 @@
@apply [&_svg:not([class*='size-'])]:size-4;
}
.item > figure:has(img) {
.item > figure:has(> img) {
@apply size-10 overflow-hidden rounded-none;
}
.item[data-size='sm'] > figure:has(img) {
.item[data-size='sm'] > figure:has(> img) {
@apply size-8;
}
.item[data-size='xs'] > figure:has(img) {
.item[data-size='xs'] > figure:has(> img) {
@apply size-6;
}
@@ -1190,16 +1037,16 @@
@apply bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 h-5 w-fit min-w-5 gap-1 rounded-none px-1 font-sans text-xs font-medium [&_svg:not([class*='size-'])]:size-3;
}
.kbd-group {
@apply gap-1;
}
/* Label */
.field > label,
.label {
@apply gap-2 text-xs leading-none in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
.field > section > label {
@apply gap-2 text-xs leading-snug in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
/* Sidebar */
.sidebar nav {
@apply bg-sidebar text-sidebar-foreground;
@@ -1289,6 +1136,14 @@
.sidebar nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-none px-2 text-xs focus-visible:ring-2 [&>svg]:size-4;
}
/* Progress */
.progress {
@apply bg-muted h-1 rounded-none;
}
.progress > span {
@apply bg-primary;
}
/* Skeleton */
.skeleton {
+143 -297
View File
@@ -1,247 +1,64 @@
/* Button */
@layer components {
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive,
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive,
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive,
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive,
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive,
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn {
@apply rounded-4xl border border-transparent bg-clip-padding text-sm font-medium focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*='size-'])]:size-4;
}
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive {
@apply h-9 gap-1.5 px-3;
&:has(> [data-icon=inline-start]) {
@apply ps-2.5;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2.5;
}
.btn:not([data-size]),
.btn[data-size='default'] {
@apply h-9 gap-1.5 px-3 has-[[data-icon='inline-end']]:pr-2.5 has-[[data-icon='inline-start']]:pl-2.5;
}
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive {
@apply h-6 gap-1 px-2.5 text-xs [&_svg:not([class*='size-'])]:size-3;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn[data-size='xs'] {
@apply h-6 gap-1 px-2.5 text-xs [&_svg:not([class*='size-'])]:size-3 has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive {
@apply h-8 gap-1 px-3;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn[data-size='sm'] {
@apply h-8 gap-1 px-3 has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive {
@apply h-10 gap-1.5 px-4;
&:has(> [data-icon=inline-start]) {
@apply ps-3;
}
&:has(> [data-icon=inline-end]) {
@apply pe-3;
}
.btn[data-size='lg'] {
@apply h-10 gap-1.5 px-4 has-[[data-icon='inline-end']]:pr-3 has-[[data-icon='inline-start']]:pl-3;
}
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive {
.btn[data-size='icon'] {
@apply size-9;
}
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive {
.btn[data-size='icon-xs'] {
@apply size-6 [&_svg:not([class*='size-'])]:size-3;
}
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive {
.btn[data-size='icon-sm'] {
@apply size-8;
}
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn[data-size='icon-lg'] {
@apply size-10;
}
.btn,
.btn-primary,
.btn-xs,
.btn-xs-primary,
.btn-sm,
.btn-sm-primary,
.btn-lg,
.btn-lg-primary,
.btn-icon,
.btn-icon-primary,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-lg-icon,
.btn-lg-icon-primary {
.btn:not([data-variant]),
.btn[data-variant='primary'] {
@apply bg-primary text-primary-foreground hover:bg-primary/80;
}
.btn-secondary,
.btn-xs-secondary,
.btn-sm-secondary,
.btn-lg-secondary,
.btn-icon-secondary,
.btn-xs-icon-secondary,
.btn-sm-icon-secondary,
.btn-lg-icon-secondary {
.btn[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground;
}
.btn-outline,
.btn-xs-outline,
.btn-sm-outline,
.btn-lg-outline,
.btn-icon-outline,
.btn-xs-icon-outline,
.btn-sm-icon-outline,
.btn-lg-icon-outline {
.btn[data-variant='outline'] {
@apply border-border bg-input/30 hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-ghost,
.btn-xs-ghost,
.btn-sm-ghost,
.btn-lg-ghost,
.btn-icon-ghost,
.btn-xs-icon-ghost,
.btn-sm-icon-ghost,
.btn-lg-icon-ghost {
.btn[data-variant='ghost'] {
@apply hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-link,
.btn-xs-link,
.btn-sm-link,
.btn-lg-link,
.btn-icon-link,
.btn-xs-icon-link,
.btn-sm-icon-link,
.btn-lg-icon-link {
.btn[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
.btn-destructive,
.btn-xs-destructive,
.btn-sm-destructive,
.btn-lg-destructive,
.btn-icon-destructive,
.btn-xs-icon-destructive,
.btn-sm-icon-destructive,
.btn-lg-icon-destructive {
.btn[data-variant='destructive'] {
@apply bg-destructive/10 text-destructive hover:bg-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40;
}
@@ -268,8 +85,7 @@
@apply bg-muted gap-2 rounded-4xl border px-2.5 text-sm font-medium [&_svg:not([class*='size-'])]:size-4;
}
.button-group > hr[role='separator'],
.button-group-separator {
.button-group > hr[role='separator'] {
@apply bg-input;
}
@@ -381,8 +197,7 @@
}
/* Alert */
.alert,
.alert-destructive {
.alert {
@apply grid gap-0.5 rounded-lg border px-4 py-3 text-start text-sm has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 [&>svg]:row-span-2 [&>svg]:translate-y-0.5 [&>svg]:text-current [&>svg:not([class*='size-'])]:size-4;
}
@@ -390,35 +205,33 @@
@apply bg-card text-card-foreground;
}
.alert-destructive {
.alert[data-variant='destructive'] {
@apply text-destructive bg-card [&>svg]:text-current;
}
.alert > :is(h2, h3, h4, h5, h6, strong, [data-title]),
.alert-destructive > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert[data-variant='destructive'] > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply font-medium;
}
:is(.alert, .alert-destructive):has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert:has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply col-start-2;
}
.alert > section {
@apply text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert-destructive > section {
.alert[data-variant='destructive'] > section {
@apply text-destructive/90 text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert > footer,
.alert-destructive > footer {
.alert[data-variant='destructive'] > footer {
@apply top-2.5 end-3;
}
/* Avatar */
.avatar,
.avatar-sm,
.avatar-lg {
.avatar {
@apply rounded-full after:rounded-[inherit];
}
@@ -426,29 +239,23 @@
@apply size-8;
}
.avatar-sm {
.avatar[data-size='sm'] {
@apply size-6;
}
.avatar-lg {
.avatar[data-size='lg'] {
@apply size-10;
}
.avatar > span:not([data-badge]),
.avatar-sm > span:not([data-badge]),
.avatar-lg > span:not([data-badge]) {
.avatar > span:not([data-badge]) {
@apply bg-muted text-muted-foreground rounded-[inherit];
}
.avatar > img,
.avatar-sm > img,
.avatar-lg > img {
.avatar > img {
@apply rounded-[inherit];
}
.avatar > [data-badge],
.avatar-sm > [data-badge],
.avatar-lg > [data-badge] {
.avatar > [data-badge] {
@apply bg-primary text-primary-foreground ring-background;
}
@@ -456,47 +263,41 @@
@apply bg-muted text-muted-foreground size-8 rounded-full text-sm [&>svg]:size-4;
}
.avatar-group:has(> .avatar-sm) > [data-count] {
.avatar-group:has(> .avatar[data-size='sm']) > [data-count] {
@apply size-6 [&>svg]:size-3;
}
.avatar-group:has(> .avatar-lg) > [data-count] {
.avatar-group:has(> .avatar[data-size='lg']) > [data-count] {
@apply size-10 [&>svg]:size-5;
}
/* Badge */
.badge,
.badge-primary,
.badge-secondary,
.badge-destructive,
.badge-outline,
.badge-ghost,
.badge-link {
@apply h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&>svg]:size-3!;
.badge {
@apply h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all [&>svg]:size-3!;
}
.badge,
.badge-primary {
.badge:not([data-variant]),
.badge[data-variant='primary'] {
@apply bg-primary text-primary-foreground [a]:hover:bg-primary/80;
}
.badge-secondary {
.badge[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80;
}
.badge-outline {
.badge[data-variant='outline'] {
@apply border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground bg-input/30;
}
.badge-destructive {
.badge[data-variant='destructive'] {
@apply bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20;
}
.badge-ghost {
.badge[data-variant='ghost'] {
@apply hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50;
}
.badge-link {
.badge[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
@@ -590,7 +391,7 @@
.combobox [role='option'].active,
.combobox [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
.combobox [role='group'] {
@@ -650,7 +451,7 @@
*:not(select).select [role='option'].active,
*:not(select).select [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
*:not(select).select [role='group'] {
@@ -704,7 +505,7 @@
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']).active {
@apply bg-accent text-accent-foreground;
@apply bg-muted text-foreground;
}
.dropdown-menu [data-variant='destructive'] {
@@ -740,11 +541,11 @@
@apply bg-border/50 -mx-1 my-1 h-px;
}
.dropdown-menu [data-shortcut] {
@apply text-muted-foreground text-xs tracking-widest;
.dropdown-menu :is([data-shortcut], kbd:last-child) {
@apply text-muted-foreground font-sans text-xs tracking-widest;
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) [data-shortcut] {
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) :is([data-shortcut], kbd:last-child) {
@apply text-accent-foreground;
}
@@ -768,8 +569,7 @@
}
/* Card */
.card,
.card-sm {
.card {
@apply ring-foreground/10 bg-card text-card-foreground overflow-hidden rounded-2xl text-sm ring-1 has-[>img:first-child]:pt-0 [&>img:first-child]:rounded-t-xl [&>img:last-child]:rounded-b-xl;
}
@@ -777,48 +577,47 @@
@apply gap-6 py-6;
}
.card-sm {
.card[data-size='sm'] {
@apply gap-4 py-4;
}
.card > header,
.card-sm > header {
.card > header {
@apply gap-2 rounded-t-xl px-6 [.border-b]:pb-6;
}
.card-sm > header {
.card[data-size='sm'] > header {
@apply px-4 [.border-b]:pb-4;
}
.card > header > :is(h2, h3, [data-title], .card-title),
.card-sm > header > :is(h2, h3, [data-title], .card-title) {
.card > header > :is(h2, h3, [data-title], .card-title) {
@apply text-base font-medium;
}
.card > header > :is(p, [data-description], .card-description),
.card-sm > header > :is(p, [data-description], .card-description) {
.card > header > :is(p, [data-description], .card-description) {
@apply text-muted-foreground text-sm;
}
.card > section,
.card-sm > section {
.card > section {
@apply px-6;
}
.card-sm > section {
.card[data-size='sm'] > section {
@apply px-4;
}
.card > footer,
.card-sm > footer {
.card > footer {
@apply rounded-b-xl px-6 [.border-t]:pt-6;
}
.card-sm > footer {
.card[data-size='sm'] > footer {
@apply px-4 [.border-t]:pt-4;
}
/* Chart */
.chart {
--chart-bar-radius: 6px;
}
.chart-tooltip {
@apply border-border/50 bg-background text-foreground gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl;
}
@@ -911,8 +710,7 @@
@apply gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3;
}
.fieldset > legend,
.field-legend {
.fieldset > legend {
@apply mb-3 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base;
}
@@ -921,6 +719,8 @@
@apply gap-7;
&[role='radiogroup'],
&:has(> .field > input[type='checkbox']:not([role='switch'])),
&:has(> .field > input[type='radio']),
&[data-slot='checkbox-group'],
&[data-slot='radio-group'] {
@apply gap-3;
@@ -933,15 +733,15 @@
}
.field {
@apply data-[invalid=true]:text-destructive gap-3;
@apply data-invalid:text-destructive data-[invalid=true]:text-destructive gap-3;
}
.field > section,
.field-content {
.field > section {
@apply gap-1;
}
.field > label,
.field > section > label,
label:has(> .field) {
@apply has-[:checked]:bg-primary/5 has-[:checked]:border-primary/30 dark:has-[:checked]:border-primary/20 dark:has-[:checked]:bg-primary/10 gap-2 leading-snug in-data-[disabled=true]:opacity-50;
}
@@ -964,18 +764,25 @@
@apply p-4;
}
.field h2,
.field h3,
.field-title {
.field h3 {
@apply gap-2 leading-snug text-sm font-medium in-data-[disabled=true]:opacity-50;
}
.fieldset legend + p,
.field > p,
.field section > p,
.field-description {
.field section > p {
@apply text-muted-foreground text-start text-sm [[data-variant=legend]+&]:-mt-1.5;
}
.fieldset > legend[data-variant='label'] {
@apply mb-0;
}
.fieldset > legend:not([data-variant]) + p,
.fieldset > legend[data-variant='legend'] + p {
@apply -mt-1.5;
}
.field-separator {
@apply -my-2 h-5 text-sm in-data-[variant=outline]:-mb-2;
}
@@ -984,8 +791,7 @@
@apply text-muted-foreground px-2;
}
.field [role="alert"],
.field-error {
.field [role="alert"] {
@apply text-destructive text-sm;
}
@@ -994,23 +800,22 @@
@apply h-9 has-[>textarea]:h-auto rounded-4xl border border-input bg-input/30 transition-colors focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:ring-[3px] has-[[aria-invalid=true]]:ring-destructive/20 has-[>textarea]:rounded-xl dark:has-[[aria-invalid=true]]:ring-destructive/40;
}
.input-group[data-orientation='vertical'] {
@apply rounded-2xl;
}
.input-group:has(> :is(input, textarea, select, [data-control]):disabled) {
@apply opacity-50;
}
.input-group[data-orientation='vertical'] {
@apply rounded-2xl;
}
.input-group > :is(input, select, [data-control]) {
@apply px-3 py-1 text-sm;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='start']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='start'], [data-align='inline-start'])) > :is(input, select, [data-control]) {
@apply ps-1.5;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='end']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='end'], [data-align='inline-end'])) > :is(input, select, [data-control]) {
@apply pe-1.5;
}
@@ -1023,33 +828,66 @@
@apply gap-2 py-2 text-sm font-medium text-muted-foreground select-none [&>svg:not([class*='size-'])]:size-4;
}
.input-group > [data-align='start'] {
.input-group > :is([data-align='start'], [data-align='inline-start']) {
@apply ps-3;
}
.input-group > [data-align='end'] {
.input-group > :is([data-align='end'], [data-align='inline-end']) {
@apply pe-3;
}
.input-group > [data-align='start']:has(> button) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> button) {
@apply -ms-1;
}
.input-group > [data-align='end']:has(> button) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> button) {
@apply -me-1;
}
.input-group > [data-align='start']:has(> kbd) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> kbd) {
@apply -ms-0.5;
}
.input-group > [data-align='end']:has(> kbd) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> kbd) {
@apply -me-0.5;
}
.input-group[data-orientation='vertical'] > :is(header, footer, [role='group'], [data-align]) {
@apply px-3;
}
.input-group > [data-align='block-start'] {
@apply px-3 pt-2;
}
.input-group > [data-align='block-end'] {
@apply px-3 pb-2;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']) {
@apply px-3 pt-3;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']) {
@apply px-3 pb-3;
}
.input-group[data-orientation='vertical']:has(> input) > :is(header[data-align='start'], [data-align='block-start']) {
@apply pt-3;
}
.input-group[data-orientation='vertical']:has(> input) > :is(footer[data-align='end'], [data-align='block-end']) {
@apply pb-3;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']).border-b {
@apply pb-3;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']).border-t {
@apply pt-3;
}
.input-group > :is(kbd, span > kbd, [role='group'] > kbd, header > kbd, footer > kbd) {
@apply rounded-4xl bg-muted-foreground/10 px-1.5;
}
@@ -1084,7 +922,7 @@
@apply text-lg font-medium tracking-tight;
}
.empty p {
.empty > header > p {
@apply text-muted-foreground text-sm/relaxed;
}
@@ -1135,15 +973,15 @@
@apply [&_svg:not([class*='size-'])]:size-4;
}
.item > figure:has(img) {
.item > figure:has(> img) {
@apply size-10 overflow-hidden rounded-lg;
}
.item[data-size='sm'] > figure:has(img) {
.item[data-size='sm'] > figure:has(> img) {
@apply size-8;
}
.item[data-size='xs'] > figure:has(img) {
.item[data-size='xs'] > figure:has(> img) {
@apply size-6 rounded-md;
}
@@ -1193,16 +1031,16 @@
@apply bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 h-5 w-fit min-w-5 gap-1 rounded-sm px-1 font-sans text-xs font-medium [&_svg:not([class*='size-'])]:size-3;
}
.kbd-group {
@apply gap-1;
}
/* Label */
.field > label,
.label {
@apply gap-2 text-sm leading-none font-medium in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
.field > section > label {
@apply gap-2 text-sm leading-snug font-medium in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
/* Sidebar */
.sidebar nav {
@apply bg-sidebar text-sidebar-foreground;
@@ -1292,6 +1130,14 @@
.sidebar nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 text-sm focus-visible:ring-2 [&>svg]:size-4;
}
/* Progress */
.progress {
@apply bg-muted h-3 rounded-4xl;
}
.progress > span {
@apply bg-primary;
}
/* Skeleton */
.skeleton {
+148 -294
View File
@@ -1,247 +1,64 @@
/* Button */
@layer components {
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive,
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive,
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive,
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive,
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive,
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn {
@apply rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:border-ring focus-visible:ring-ring/30 focus-visible:ring-[2px] aria-invalid:border-destructive aria-invalid:ring-[2px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*='size-'])]:size-4;
}
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive {
@apply h-7 gap-1 px-2 text-xs/relaxed;
&:has(> [data-icon=inline-start]) {
@apply ps-1.5;
}
&:has(> [data-icon=inline-end]) {
@apply pe-1.5;
}
.btn:not([data-size]),
.btn[data-size='default'] {
@apply h-7 gap-1 px-2 text-xs/relaxed has-[[data-icon='inline-end']]:pr-1.5 has-[[data-icon='inline-start']]:pl-1.5;
}
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive {
@apply h-5 gap-1 px-2 text-[0.625rem] rounded-sm [&_svg:not([class*='size-'])]:size-2.5;
&:has(> [data-icon=inline-start]) {
@apply ps-1.5;
}
&:has(> [data-icon=inline-end]) {
@apply pe-1.5;
}
.btn[data-size='xs'] {
@apply h-5 gap-1 px-2 text-[0.625rem] rounded-sm [&_svg:not([class*='size-'])]:size-2.5 has-[[data-icon='inline-end']]:pr-1.5 has-[[data-icon='inline-start']]:pl-1.5;
}
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive {
@apply h-6 gap-1 px-2 text-xs/relaxed [&_svg:not([class*='size-'])]:size-3;
&:has(> [data-icon=inline-start]) {
@apply ps-1.5;
}
&:has(> [data-icon=inline-end]) {
@apply pe-1.5;
}
.btn[data-size='sm'] {
@apply h-6 gap-1 px-2 text-xs/relaxed [&_svg:not([class*='size-'])]:size-3 has-[[data-icon='inline-end']]:pr-1.5 has-[[data-icon='inline-start']]:pl-1.5;
}
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive {
@apply h-8 gap-1 px-2.5 text-xs/relaxed;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn[data-size='lg'] {
@apply h-8 gap-1 px-2.5 text-xs/relaxed has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive {
.btn[data-size='icon'] {
@apply size-7 [&_svg:not([class*='size-'])]:size-3.5;
}
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive {
.btn[data-size='icon-xs'] {
@apply size-5 rounded-sm [&_svg:not([class*='size-'])]:size-2.5;
}
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive {
.btn[data-size='icon-sm'] {
@apply size-6 [&_svg:not([class*='size-'])]:size-3;
}
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn[data-size='icon-lg'] {
@apply size-8 [&_svg:not([class*='size-'])]:size-4;
}
.btn,
.btn-primary,
.btn-xs,
.btn-xs-primary,
.btn-sm,
.btn-sm-primary,
.btn-lg,
.btn-lg-primary,
.btn-icon,
.btn-icon-primary,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-lg-icon,
.btn-lg-icon-primary {
.btn:not([data-variant]),
.btn[data-variant='primary'] {
@apply bg-primary text-primary-foreground hover:bg-primary/80;
}
.btn-secondary,
.btn-xs-secondary,
.btn-sm-secondary,
.btn-lg-secondary,
.btn-icon-secondary,
.btn-xs-icon-secondary,
.btn-sm-icon-secondary,
.btn-lg-icon-secondary {
.btn[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground;
}
.btn-outline,
.btn-xs-outline,
.btn-sm-outline,
.btn-lg-outline,
.btn-icon-outline,
.btn-xs-icon-outline,
.btn-sm-icon-outline,
.btn-lg-icon-outline {
.btn[data-variant='outline'] {
@apply border-border hover:bg-input/50 hover:text-foreground dark:border-input dark:bg-input/30 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-ghost,
.btn-xs-ghost,
.btn-sm-ghost,
.btn-lg-ghost,
.btn-icon-ghost,
.btn-xs-icon-ghost,
.btn-sm-icon-ghost,
.btn-lg-icon-ghost {
.btn[data-variant='ghost'] {
@apply hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-link,
.btn-xs-link,
.btn-sm-link,
.btn-lg-link,
.btn-icon-link,
.btn-xs-icon-link,
.btn-sm-icon-link,
.btn-lg-icon-link {
.btn[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
.btn-destructive,
.btn-xs-destructive,
.btn-sm-destructive,
.btn-lg-destructive,
.btn-icon-destructive,
.btn-xs-icon-destructive,
.btn-sm-icon-destructive,
.btn-lg-icon-destructive {
.btn[data-variant='destructive'] {
@apply bg-destructive/10 text-destructive hover:bg-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40;
}
@@ -268,8 +85,7 @@
@apply bg-muted gap-2 rounded-md border px-2.5 text-xs/relaxed font-medium [&_svg:not([class*='size-'])]:size-4;
}
.button-group > hr[role='separator'],
.button-group-separator {
.button-group > hr[role='separator'] {
@apply bg-input;
}
@@ -381,8 +197,7 @@
}
/* Alert */
.alert,
.alert-destructive {
.alert {
@apply grid gap-0.5 rounded-lg border px-2 py-1.5 text-start text-xs/relaxed has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-1.5 [&>svg]:row-span-2 [&>svg]:translate-y-0.5 [&>svg]:text-current [&>svg:not([class*='size-'])]:size-3.5;
}
@@ -390,35 +205,33 @@
@apply bg-card text-card-foreground;
}
.alert-destructive {
.alert[data-variant='destructive'] {
@apply text-destructive bg-card [&>svg]:text-current;
}
.alert > :is(h2, h3, h4, h5, h6, strong, [data-title]),
.alert-destructive > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert[data-variant='destructive'] > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply font-medium;
}
:is(.alert, .alert-destructive):has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert:has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply col-start-2;
}
.alert > section {
@apply text-muted-foreground text-xs/relaxed text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert-destructive > section {
.alert[data-variant='destructive'] > section {
@apply text-destructive/90 text-xs/relaxed text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert > footer,
.alert-destructive > footer {
.alert[data-variant='destructive'] > footer {
@apply top-1.5 end-2;
}
/* Avatar */
.avatar,
.avatar-sm,
.avatar-lg {
.avatar {
@apply rounded-full after:rounded-[inherit];
}
@@ -426,29 +239,23 @@
@apply size-8;
}
.avatar-sm {
.avatar[data-size='sm'] {
@apply size-6;
}
.avatar-lg {
.avatar[data-size='lg'] {
@apply size-10;
}
.avatar > span:not([data-badge]),
.avatar-sm > span:not([data-badge]),
.avatar-lg > span:not([data-badge]) {
.avatar > span:not([data-badge]) {
@apply bg-muted text-muted-foreground rounded-[inherit];
}
.avatar > img,
.avatar-sm > img,
.avatar-lg > img {
.avatar > img {
@apply rounded-[inherit];
}
.avatar > [data-badge],
.avatar-sm > [data-badge],
.avatar-lg > [data-badge] {
.avatar > [data-badge] {
@apply bg-primary text-primary-foreground ring-background;
}
@@ -456,47 +263,41 @@
@apply bg-muted text-muted-foreground size-8 rounded-full text-xs/relaxed [&>svg]:size-4;
}
.avatar-group:has(> .avatar-sm) > [data-count] {
.avatar-group:has(> .avatar[data-size='sm']) > [data-count] {
@apply size-6 [&>svg]:size-3;
}
.avatar-group:has(> .avatar-lg) > [data-count] {
.avatar-group:has(> .avatar[data-size='lg']) > [data-count] {
@apply size-10 [&>svg]:size-5;
}
/* Badge */
.badge,
.badge-primary,
.badge-secondary,
.badge-destructive,
.badge-outline,
.badge-ghost,
.badge-link {
@apply h-5 gap-1 rounded-full border border-transparent px-2 py-0.5 text-[0.625rem] font-medium transition-all has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&>svg]:size-2.5!;
.badge {
@apply h-5 gap-1 rounded-full border border-transparent px-2 py-0.5 text-[0.625rem] font-medium transition-all [&>svg]:size-2.5!;
}
.badge,
.badge-primary {
.badge:not([data-variant]),
.badge[data-variant='primary'] {
@apply bg-primary text-primary-foreground [a]:hover:bg-primary/80;
}
.badge-secondary {
.badge[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80;
}
.badge-outline {
.badge[data-variant='outline'] {
@apply border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground bg-input/20 dark:bg-input/30;
}
.badge-destructive {
.badge[data-variant='destructive'] {
@apply bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20;
}
.badge-ghost {
.badge[data-variant='ghost'] {
@apply hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50;
}
.badge-link {
.badge[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
@@ -590,7 +391,7 @@
.combobox [role='option'].active,
.combobox [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
.combobox [role='group'] {
@@ -654,7 +455,7 @@
*:not(select).select [role='option'].active,
*:not(select).select [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
*:not(select).select [role='group'] {
@@ -708,7 +509,7 @@
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']).active {
@apply bg-accent text-accent-foreground;
@apply bg-muted text-foreground;
}
.dropdown-menu [data-variant='destructive'] {
@@ -744,11 +545,11 @@
@apply bg-border/50 -mx-1 my-1 h-px;
}
.dropdown-menu [data-shortcut] {
@apply text-muted-foreground text-[0.625rem] tracking-widest;
.dropdown-menu :is([data-shortcut], kbd:last-child) {
@apply text-muted-foreground font-sans text-[0.625rem] tracking-widest;
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) [data-shortcut] {
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) :is([data-shortcut], kbd:last-child) {
@apply text-accent-foreground;
}
@@ -772,8 +573,7 @@
}
/* Card */
.card,
.card-sm {
.card {
@apply ring-foreground/10 bg-card text-card-foreground overflow-hidden rounded-lg text-xs/relaxed ring-1 has-[>img:first-child]:pt-0 [&>img:first-child]:rounded-t-lg [&>img:last-child]:rounded-b-lg;
}
@@ -781,48 +581,47 @@
@apply gap-4 py-4;
}
.card-sm {
.card[data-size='sm'] {
@apply gap-3 py-3;
}
.card > header,
.card-sm > header {
.card > header {
@apply gap-1 rounded-t-lg px-4 [.border-b]:pb-4;
}
.card-sm > header {
.card[data-size='sm'] > header {
@apply px-3 [.border-b]:pb-3;
}
.card > header > :is(h2, h3, [data-title], .card-title),
.card-sm > header > :is(h2, h3, [data-title], .card-title) {
.card > header > :is(h2, h3, [data-title], .card-title) {
@apply text-sm font-medium;
}
.card > header > :is(p, [data-description], .card-description),
.card-sm > header > :is(p, [data-description], .card-description) {
.card > header > :is(p, [data-description], .card-description) {
@apply text-muted-foreground text-xs/relaxed;
}
.card > section,
.card-sm > section {
.card > section {
@apply px-4;
}
.card-sm > section {
.card[data-size='sm'] > section {
@apply px-3;
}
.card > footer,
.card-sm > footer {
.card > footer {
@apply rounded-b-lg px-4 [.border-t]:pt-4;
}
.card-sm > footer {
.card[data-size='sm'] > footer {
@apply px-3 [.border-t]:pt-3;
}
/* Chart */
.chart {
--chart-bar-radius: 6px;
}
.chart-tooltip {
@apply border-border/50 bg-background text-foreground gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl;
}
@@ -915,8 +714,7 @@
@apply gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3;
}
.fieldset > legend,
.field-legend {
.fieldset > legend {
@apply mb-2 font-medium data-[variant=label]:text-xs/relaxed data-[variant=legend]:text-sm;
}
@@ -925,6 +723,8 @@
@apply gap-4;
&[role='radiogroup'],
&:has(> .field > input[type='checkbox']:not([role='switch'])),
&:has(> .field > input[type='radio']),
&[data-slot='checkbox-group'],
&[data-slot='radio-group'] {
@apply gap-3;
@@ -937,15 +737,15 @@
}
.field {
@apply data-[invalid=true]:text-destructive gap-2;
@apply data-invalid:text-destructive data-[invalid=true]:text-destructive gap-2;
}
.field > section,
.field-content {
.field > section {
@apply gap-0.5;
}
.field > label,
.field > section > label,
label:has(> .field) {
@apply has-[:checked]:bg-primary/5 dark:has-[:checked]:bg-primary/10 gap-2 leading-snug in-data-[disabled=true]:opacity-50;
}
@@ -968,18 +768,25 @@
@apply p-2;
}
.field h2,
.field h3,
.field-title {
.field h3 {
@apply gap-2 text-xs/relaxed font-medium in-data-[disabled=true]:opacity-50;
}
.fieldset legend + p,
.field > p,
.field section > p,
.field-description {
.field section > p {
@apply text-muted-foreground text-start text-xs/relaxed [[data-variant=legend]+&]:-mt-1.5;
}
.fieldset > legend[data-variant='label'] {
@apply mb-0;
}
.fieldset > legend:not([data-variant]) + p,
.fieldset > legend[data-variant='legend'] + p {
@apply -mt-1.5;
}
.field-separator {
@apply -my-2 h-5 text-xs/relaxed in-data-[variant=outline]:-mb-2;
}
@@ -988,8 +795,7 @@
@apply text-muted-foreground px-2;
}
.field [role="alert"],
.field-error {
.field [role="alert"] {
@apply text-destructive text-xs/relaxed;
}
@@ -1002,16 +808,15 @@
.input-group:has(> :is(input, textarea, select, [data-control]):disabled) {
@apply opacity-50;
}
.input-group > :is(input, select, [data-control]) {
@apply px-2 py-0.5 text-xs/relaxed;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='start']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='start'], [data-align='inline-start'])) > :is(input, select, [data-control]) {
@apply ps-1.5;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='end']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='end'], [data-align='inline-end'])) > :is(input, select, [data-control]) {
@apply pe-1.5;
}
@@ -1024,33 +829,74 @@
@apply gap-1 py-2 text-xs/relaxed font-medium text-muted-foreground select-none [&>svg:not([class*='size-'])]:size-3.5;
}
.input-group > [data-align='start'] {
.input-group > :is([data-align='start'], [data-align='inline-start']) {
@apply ps-2;
}
.input-group > [data-align='end'] {
.input-group > :is([data-align='end'], [data-align='inline-end']) {
@apply pe-2;
}
.input-group > [data-align='start']:has(> button) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> button) {
@apply -ms-1;
}
.input-group > [data-align='end']:has(> button) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> button) {
@apply -me-1;
}
.input-group > [data-align='start']:has(> kbd) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> kbd) {
@apply -ms-0.5;
}
.input-group > [data-align='end']:has(> kbd) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> kbd) {
@apply -me-0.5;
}
.input-group > [data-align='block-start'] {
@apply px-2.5 pt-2;
}
.input-group > [data-align='block-end'] {
@apply px-2.5 pb-2;
}
.input-group > [data-align='block-start'].border-b {
@apply pb-2;
}
.input-group > [data-align='block-end'].border-t {
@apply pt-2;
}
.input-group[data-orientation='vertical'] > :is(header, footer, [role='group'], [data-align]) {
@apply px-2;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']) {
@apply px-2 pt-2;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']) {
@apply px-2 pb-2;
}
.input-group[data-orientation='vertical']:has(> input) > :is(header[data-align='start'], [data-align='block-start']) {
@apply pt-2;
}
.input-group[data-orientation='vertical']:has(> input) > :is(footer[data-align='end'], [data-align='block-end']) {
@apply pb-2;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']).border-b {
@apply pb-2;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']).border-t {
@apply pt-2;
}
.input-group > :is(kbd, span > kbd, [role='group'] > kbd, header > kbd, footer > kbd) {
@apply rounded-[calc(var(--radius-sm)-2px)] bg-muted-foreground/10 px-1 text-[0.625rem];
}
@@ -1085,7 +931,7 @@
@apply text-sm font-medium tracking-tight;
}
.empty p {
.empty > header > p {
@apply text-muted-foreground text-xs/relaxed;
}
@@ -1136,15 +982,15 @@
@apply [&_svg:not([class*='size-'])]:size-4;
}
.item > figure:has(img) {
.item > figure:has(> img) {
@apply size-8 overflow-hidden rounded-sm;
}
.item[data-size='sm'] > figure:has(img) {
.item[data-size='sm'] > figure:has(> img) {
@apply size-8;
}
.item[data-size='xs'] > figure:has(img) {
.item[data-size='xs'] > figure:has(> img) {
@apply size-6;
}
@@ -1194,16 +1040,16 @@
@apply bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 h-5 w-fit min-w-5 gap-1 rounded-xs px-1 font-sans text-[0.625rem] font-medium [&_svg:not([class*='size-'])]:size-3;
}
.kbd-group {
@apply gap-1;
}
/* Label */
.field > label,
.label {
@apply gap-2 text-xs/relaxed leading-none font-medium in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
.field > section > label {
@apply gap-2 text-xs/relaxed font-medium in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
/* Sidebar */
.sidebar nav {
@apply bg-sidebar text-sidebar-foreground;
@@ -1293,6 +1139,14 @@
.sidebar nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 text-xs focus-visible:ring-2 [&>svg]:size-4;
}
/* Progress */
.progress {
@apply bg-muted h-1 rounded-md;
}
.progress > span {
@apply bg-primary;
}
/* Skeleton */
.skeleton {
+150 -295
View File
@@ -1,247 +1,64 @@
/* Button */
@layer components {
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive,
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive,
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive,
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive,
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive,
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn {
@apply rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*='size-'])]:size-4;
}
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive {
@apply h-8 gap-1.5 px-2.5;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn:not([data-size]),
.btn[data-size='default'] {
@apply h-8 gap-1.5 px-2.5 has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive {
@apply h-6 gap-1 px-2 text-xs rounded-[min(var(--radius-md),10px)] [&_svg:not([class*='size-'])]:size-3;
&:has(> [data-icon=inline-start]) {
@apply ps-1.5;
}
&:has(> [data-icon=inline-end]) {
@apply pe-1.5;
}
.btn[data-size='xs'] {
@apply h-6 gap-1 px-2 text-xs rounded-[min(var(--radius-md),10px)] [&_svg:not([class*='size-'])]:size-3 has-[[data-icon='inline-end']]:pr-1.5 has-[[data-icon='inline-start']]:pl-1.5;
}
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive {
@apply h-7 gap-1 px-2.5 text-[0.8rem] rounded-[min(var(--radius-md),12px)] [&_svg:not([class*='size-'])]:size-3.5;
&:has(> [data-icon=inline-start]) {
@apply ps-1.5;
}
&:has(> [data-icon=inline-end]) {
@apply pe-1.5;
}
.btn[data-size='sm'] {
@apply h-7 gap-1 px-2.5 text-[0.8rem] rounded-[min(var(--radius-md),12px)] [&_svg:not([class*='size-'])]:size-3.5 has-[[data-icon='inline-end']]:pr-1.5 has-[[data-icon='inline-start']]:pl-1.5;
}
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive {
@apply h-9 gap-1.5 px-2.5;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn[data-size='lg'] {
@apply h-9 gap-1.5 px-2.5 has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive {
.btn[data-size='icon'] {
@apply size-8;
}
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive {
.btn[data-size='icon-xs'] {
@apply size-6 rounded-[min(var(--radius-md),10px)] [&_svg:not([class*='size-'])]:size-3;
}
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive {
.btn[data-size='icon-sm'] {
@apply size-7 rounded-[min(var(--radius-md),12px)];
}
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn[data-size='icon-lg'] {
@apply size-9;
}
.btn,
.btn-primary,
.btn-xs,
.btn-xs-primary,
.btn-sm,
.btn-sm-primary,
.btn-lg,
.btn-lg-primary,
.btn-icon,
.btn-icon-primary,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-lg-icon,
.btn-lg-icon-primary {
.btn:not([data-variant]),
.btn[data-variant='primary'] {
@apply bg-primary text-primary-foreground hover:bg-primary/80;
}
.btn-secondary,
.btn-xs-secondary,
.btn-sm-secondary,
.btn-lg-secondary,
.btn-icon-secondary,
.btn-xs-icon-secondary,
.btn-sm-icon-secondary,
.btn-lg-icon-secondary {
.btn[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground;
}
.btn-outline,
.btn-xs-outline,
.btn-sm-outline,
.btn-lg-outline,
.btn-icon-outline,
.btn-xs-icon-outline,
.btn-sm-icon-outline,
.btn-lg-icon-outline {
.btn[data-variant='outline'] {
@apply border-border bg-background hover:bg-muted hover:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-ghost,
.btn-xs-ghost,
.btn-sm-ghost,
.btn-lg-ghost,
.btn-icon-ghost,
.btn-xs-icon-ghost,
.btn-sm-icon-ghost,
.btn-lg-icon-ghost {
.btn[data-variant='ghost'] {
@apply hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-link,
.btn-xs-link,
.btn-sm-link,
.btn-lg-link,
.btn-icon-link,
.btn-xs-icon-link,
.btn-sm-icon-link,
.btn-lg-icon-link {
.btn[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
.btn-destructive,
.btn-xs-destructive,
.btn-sm-destructive,
.btn-lg-destructive,
.btn-icon-destructive,
.btn-xs-icon-destructive,
.btn-sm-icon-destructive,
.btn-lg-icon-destructive {
.btn[data-variant='destructive'] {
@apply bg-destructive/10 text-destructive hover:bg-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40;
}
@@ -268,8 +85,7 @@
@apply bg-muted gap-2 rounded-lg border px-2.5 text-sm font-medium [&_svg:not([class*='size-'])]:size-4;
}
.button-group > hr[role='separator'],
.button-group-separator {
.button-group > hr[role='separator'] {
@apply bg-input;
}
@@ -381,8 +197,7 @@
}
/* Alert */
.alert,
.alert-destructive {
.alert {
@apply grid gap-0.5 rounded-lg border px-2.5 py-2 text-start text-sm has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 [&>svg]:row-span-2 [&>svg]:translate-y-0.5 [&>svg]:text-current [&>svg:not([class*='size-'])]:size-4;
}
@@ -390,35 +205,33 @@
@apply bg-card text-card-foreground;
}
.alert-destructive {
.alert[data-variant='destructive'] {
@apply text-destructive bg-card [&>svg]:text-current;
}
.alert > :is(h2, h3, h4, h5, h6, strong, [data-title]),
.alert-destructive > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert[data-variant='destructive'] > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply font-medium;
}
:is(.alert, .alert-destructive):has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert:has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply col-start-2;
}
.alert > section {
@apply text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert-destructive > section {
.alert[data-variant='destructive'] > section {
@apply text-destructive/90 text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert > footer,
.alert-destructive > footer {
.alert[data-variant='destructive'] > footer {
@apply top-2 end-2;
}
/* Avatar */
.avatar,
.avatar-sm,
.avatar-lg {
.avatar {
@apply rounded-full after:rounded-[inherit];
}
@@ -426,29 +239,23 @@
@apply size-8;
}
.avatar-sm {
.avatar[data-size='sm'] {
@apply size-6;
}
.avatar-lg {
.avatar[data-size='lg'] {
@apply size-10;
}
.avatar > span:not([data-badge]),
.avatar-sm > span:not([data-badge]),
.avatar-lg > span:not([data-badge]) {
.avatar > span:not([data-badge]) {
@apply bg-muted text-muted-foreground rounded-[inherit];
}
.avatar > img,
.avatar-sm > img,
.avatar-lg > img {
.avatar > img {
@apply rounded-[inherit];
}
.avatar > [data-badge],
.avatar-sm > [data-badge],
.avatar-lg > [data-badge] {
.avatar > [data-badge] {
@apply bg-primary text-primary-foreground ring-background;
}
@@ -456,47 +263,41 @@
@apply bg-muted text-muted-foreground size-8 rounded-full text-sm [&>svg]:size-4;
}
.avatar-group:has(> .avatar-sm) > [data-count] {
.avatar-group:has(> .avatar[data-size='sm']) > [data-count] {
@apply size-6 [&>svg]:size-3;
}
.avatar-group:has(> .avatar-lg) > [data-count] {
.avatar-group:has(> .avatar[data-size='lg']) > [data-count] {
@apply size-10 [&>svg]:size-5;
}
/* Badge */
.badge,
.badge-primary,
.badge-secondary,
.badge-destructive,
.badge-outline,
.badge-ghost,
.badge-link {
@apply h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&>svg]:size-3!;
.badge {
@apply h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all [&>svg]:size-3!;
}
.badge,
.badge-primary {
.badge:not([data-variant]),
.badge[data-variant='primary'] {
@apply bg-primary text-primary-foreground [a]:hover:bg-primary/80;
}
.badge-secondary {
.badge[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80;
}
.badge-outline {
.badge[data-variant='outline'] {
@apply border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground;
}
.badge-destructive {
.badge[data-variant='destructive'] {
@apply bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20;
}
.badge-ghost {
.badge[data-variant='ghost'] {
@apply hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50;
}
.badge-link {
.badge[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
@@ -590,7 +391,7 @@
.combobox [role='option'].active,
.combobox [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
.combobox [role='group'] {
@@ -650,7 +451,7 @@
*:not(select).select [role='option'].active,
*:not(select).select [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
*:not(select).select [role='group'] {
@@ -704,7 +505,7 @@
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']).active {
@apply bg-accent text-accent-foreground;
@apply bg-muted text-foreground;
}
.dropdown-menu [data-variant='destructive'] {
@@ -740,11 +541,11 @@
@apply bg-border -mx-1 my-1 h-px;
}
.dropdown-menu [data-shortcut] {
@apply text-muted-foreground text-xs tracking-widest;
.dropdown-menu :is([data-shortcut], kbd:last-child) {
@apply text-muted-foreground font-sans text-xs tracking-widest;
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) [data-shortcut] {
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) :is([data-shortcut], kbd:last-child) {
@apply text-accent-foreground;
}
@@ -768,8 +569,7 @@
}
/* Card */
.card,
.card-sm {
.card {
@apply ring-foreground/10 bg-card text-card-foreground overflow-hidden rounded-xl text-sm ring-1 has-[>footer]:pb-0 has-[>img:first-child]:pt-0 [&>img:first-child]:rounded-t-xl [&>img:last-child]:rounded-b-xl;
}
@@ -777,52 +577,51 @@
@apply gap-4 py-4;
}
.card-sm {
.card[data-size='sm'] {
@apply gap-3 py-3;
}
.card > header,
.card-sm > header {
.card > header {
@apply gap-1 rounded-t-xl px-4 [.border-b]:pb-4;
}
.card-sm > header {
.card[data-size='sm'] > header {
@apply px-3 [.border-b]:pb-3;
}
.card > header > :is(h2, h3, [data-title], .card-title),
.card-sm > header > :is(h2, h3, [data-title], .card-title) {
.card > header > :is(h2, h3, [data-title], .card-title) {
@apply text-base leading-snug font-medium;
}
.card-sm > header > :is(h2, h3, [data-title], .card-title) {
.card[data-size='sm'] > header > :is(h2, h3, [data-title], .card-title) {
@apply text-sm;
}
.card > header > :is(p, [data-description], .card-description),
.card-sm > header > :is(p, [data-description], .card-description) {
.card > header > :is(p, [data-description], .card-description) {
@apply text-muted-foreground text-sm;
}
.card > section,
.card-sm > section {
.card > section {
@apply px-4;
}
.card-sm > section {
.card[data-size='sm'] > section {
@apply px-3;
}
.card > footer,
.card-sm > footer {
.card > footer {
@apply bg-muted/50 rounded-b-xl border-t p-4;
}
.card-sm > footer {
.card[data-size='sm'] > footer {
@apply p-3;
}
/* Chart */
.chart {
--chart-bar-radius: 6px;
}
.chart-tooltip {
@apply border-border/50 bg-background text-foreground gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl;
}
@@ -915,8 +714,7 @@
@apply gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3;
}
.fieldset > legend,
.field-legend {
.fieldset > legend {
@apply mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base;
}
@@ -925,6 +723,8 @@
@apply gap-5;
&[role='radiogroup'],
&:has(> .field > input[type='checkbox']:not([role='switch'])),
&:has(> .field > input[type='radio']),
&[data-slot='checkbox-group'],
&[data-slot='radio-group'] {
@apply gap-3;
@@ -937,15 +737,15 @@
}
.field {
@apply data-[invalid=true]:text-destructive gap-2;
@apply data-invalid:text-destructive data-[invalid=true]:text-destructive gap-2;
}
.field > section,
.field-content {
.field > section {
@apply gap-0.5;
}
.field > label,
.field > section > label,
label:has(> .field) {
@apply has-[:checked]:bg-primary/5 has-[:checked]:border-primary/30 dark:has-[:checked]:border-primary/20 dark:has-[:checked]:bg-primary/10 gap-2 leading-snug in-data-[disabled=true]:opacity-50;
}
@@ -968,18 +768,25 @@
@apply p-2.5;
}
.field h2,
.field h3,
.field-title {
.field h3 {
@apply gap-2 leading-snug text-sm font-medium in-data-[disabled=true]:opacity-50;
}
.fieldset legend + p,
.field > p,
.field section > p,
.field-description {
.field section > p {
@apply text-muted-foreground text-start text-sm [[data-variant=legend]+&]:-mt-1.5;
}
.fieldset > legend[data-variant='label'] {
@apply mb-0;
}
.fieldset > legend:not([data-variant]) + p,
.fieldset > legend[data-variant='legend'] + p {
@apply -mt-1.5;
}
.field-separator {
@apply -my-2 h-5 text-sm in-data-[variant=outline]:-mb-2;
}
@@ -988,8 +795,7 @@
@apply text-muted-foreground px-2;
}
.field [role="alert"],
.field-error {
.field [role="alert"] {
@apply text-destructive text-sm;
}
@@ -1000,18 +806,18 @@
.input-group:has(> :is(input, textarea, select, [data-control]):disabled) {
@apply opacity-50;
@apply bg-input/50 opacity-50 dark:bg-input/80;
}
.input-group > :is(input, select, [data-control]) {
@apply px-2.5 py-1 text-sm;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='start']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='start'], [data-align='inline-start'])) > :is(input, select, [data-control]) {
@apply ps-1.5;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='end']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='end'], [data-align='inline-end'])) > :is(input, select, [data-control]) {
@apply pe-1.5;
}
@@ -1024,33 +830,74 @@
@apply gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none [&>svg:not([class*='size-'])]:size-4;
}
.input-group > [data-align='start'] {
.input-group > :is([data-align='start'], [data-align='inline-start']) {
@apply ps-2;
}
.input-group > [data-align='end'] {
.input-group > :is([data-align='end'], [data-align='inline-end']) {
@apply pe-2;
}
.input-group > [data-align='start']:has(> button) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> button) {
@apply -ms-1;
}
.input-group > [data-align='end']:has(> button) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> button) {
@apply -me-1;
}
.input-group > [data-align='start']:has(> kbd) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> kbd) {
@apply -ms-0.5;
}
.input-group > [data-align='end']:has(> kbd) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> kbd) {
@apply -me-0.5;
}
.input-group > [data-align='block-start'] {
@apply px-2.5 pt-2;
}
.input-group > [data-align='block-end'] {
@apply px-2.5 pb-2;
}
.input-group > [data-align='block-start'].border-b {
@apply pb-2;
}
.input-group > [data-align='block-end'].border-t {
@apply pt-2;
}
.input-group[data-orientation='vertical'] > :is(header, footer, [role='group'], [data-align]) {
@apply px-2.5;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']) {
@apply px-2.5 pt-2;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']) {
@apply px-2.5 pb-2;
}
.input-group[data-orientation='vertical']:has(> input) > :is(header[data-align='start'], [data-align='block-start']) {
@apply pt-2;
}
.input-group[data-orientation='vertical']:has(> input) > :is(footer[data-align='end'], [data-align='block-end']) {
@apply pb-2;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']).border-b {
@apply pb-2;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']).border-t {
@apply pt-2;
}
.input-group > :is(kbd, span > kbd, [role='group'] > kbd, header > kbd, footer > kbd) {
@apply rounded-[calc(var(--radius)-5px)];
}
@@ -1085,7 +932,7 @@
@apply text-sm font-medium tracking-tight;
}
.empty p {
.empty > header > p {
@apply text-muted-foreground text-sm/relaxed;
}
@@ -1136,15 +983,15 @@
@apply [&_svg:not([class*='size-'])]:size-4;
}
.item > figure:has(img) {
.item > figure:has(> img) {
@apply size-10 overflow-hidden rounded-sm;
}
.item[data-size='sm'] > figure:has(img) {
.item[data-size='sm'] > figure:has(> img) {
@apply size-8;
}
.item[data-size='xs'] > figure:has(img) {
.item[data-size='xs'] > figure:has(> img) {
@apply size-6;
}
@@ -1194,16 +1041,16 @@
@apply bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 h-5 w-fit min-w-5 gap-1 rounded-sm px-1 font-sans text-xs font-medium [&_svg:not([class*='size-'])]:size-3;
}
.kbd-group {
@apply gap-1;
}
/* Label */
.field > label,
.label {
@apply gap-2 text-sm leading-none font-medium in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
.field > section > label {
@apply gap-2 text-sm leading-snug font-medium in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
/* Sidebar */
.sidebar nav {
@apply bg-sidebar text-sidebar-foreground;
@@ -1293,6 +1140,14 @@
.sidebar nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 text-sm focus-visible:ring-2 [&>svg]:size-4;
}
/* Progress */
.progress {
@apply bg-muted h-1 rounded-full;
}
.progress > span {
@apply bg-primary;
}
/* Skeleton */
.skeleton {
+150 -296
View File
@@ -1,247 +1,64 @@
/* Button */
@layer components {
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive,
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive,
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive,
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive,
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive,
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn {
@apply rounded-2xl border border-transparent bg-clip-padding text-sm font-medium focus-visible:border-ring focus-visible:ring-ring/30 focus-visible:ring-[3px] aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*='size-'])]:size-4;
}
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive {
@apply h-8 gap-1.5 px-3;
&:has(> [data-icon=inline-start]) {
@apply ps-2.5;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2.5;
}
.btn:not([data-size]),
.btn[data-size='default'] {
@apply h-8 gap-1.5 px-3 has-[[data-icon='inline-end']]:pr-2.5 has-[[data-icon='inline-start']]:pl-2.5;
}
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive {
@apply h-6 gap-1 px-2.5 text-xs [&_svg:not([class*='size-'])]:size-3;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn[data-size='xs'] {
@apply h-6 gap-1 px-2.5 text-xs [&_svg:not([class*='size-'])]:size-3 has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive {
@apply h-7 gap-1 px-3;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn[data-size='sm'] {
@apply h-7 gap-1 px-3 has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive {
@apply h-9 gap-1.5 px-4;
&:has(> [data-icon=inline-start]) {
@apply ps-3;
}
&:has(> [data-icon=inline-end]) {
@apply pe-3;
}
.btn[data-size='lg'] {
@apply h-9 gap-1.5 px-4 has-[[data-icon='inline-end']]:pr-3 has-[[data-icon='inline-start']]:pl-3;
}
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive {
.btn[data-size='icon'] {
@apply size-8;
}
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive {
.btn[data-size='icon-xs'] {
@apply size-6 [&_svg:not([class*='size-'])]:size-3;
}
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive {
.btn[data-size='icon-sm'] {
@apply size-7;
}
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn[data-size='icon-lg'] {
@apply size-9;
}
.btn,
.btn-primary,
.btn-xs,
.btn-xs-primary,
.btn-sm,
.btn-sm-primary,
.btn-lg,
.btn-lg-primary,
.btn-icon,
.btn-icon-primary,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-lg-icon,
.btn-lg-icon-primary {
.btn:not([data-variant]),
.btn[data-variant='primary'] {
@apply bg-primary text-primary-foreground hover:bg-primary/80;
}
.btn-secondary,
.btn-xs-secondary,
.btn-sm-secondary,
.btn-lg-secondary,
.btn-icon-secondary,
.btn-xs-icon-secondary,
.btn-sm-icon-secondary,
.btn-lg-icon-secondary {
.btn[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground;
}
.btn-outline,
.btn-xs-outline,
.btn-sm-outline,
.btn-lg-outline,
.btn-icon-outline,
.btn-xs-icon-outline,
.btn-sm-icon-outline,
.btn-lg-icon-outline {
.btn[data-variant='outline'] {
@apply border-border bg-background hover:bg-muted hover:text-foreground dark:bg-transparent dark:hover:bg-input/30 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-ghost,
.btn-xs-ghost,
.btn-sm-ghost,
.btn-lg-ghost,
.btn-icon-ghost,
.btn-xs-icon-ghost,
.btn-sm-icon-ghost,
.btn-lg-icon-ghost {
.btn[data-variant='ghost'] {
@apply hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-link,
.btn-xs-link,
.btn-sm-link,
.btn-lg-link,
.btn-icon-link,
.btn-xs-icon-link,
.btn-sm-icon-link,
.btn-lg-icon-link {
.btn[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
.btn-destructive,
.btn-xs-destructive,
.btn-sm-destructive,
.btn-lg-destructive,
.btn-icon-destructive,
.btn-xs-icon-destructive,
.btn-sm-icon-destructive,
.btn-lg-icon-destructive {
.btn[data-variant='destructive'] {
@apply bg-destructive/10 text-destructive hover:bg-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40;
}
@@ -250,8 +67,8 @@
@apply has-[>.button-group]:gap-2;
}
.button-group:has(> :is(.btn-outline, .btn-icon-outline, .btn-xs-outline, .btn-xs-icon-outline, .btn-sm-outline, .btn-sm-icon-outline, .btn-lg-outline, .btn-lg-icon-outline)),
.button-group:has(> .select > button:is(.btn-outline, .btn-icon-outline, .btn-xs-outline, .btn-xs-icon-outline, .btn-sm-outline, .btn-sm-icon-outline, .btn-lg-outline, .btn-lg-icon-outline)) {
.button-group:has(> .btn[data-variant='outline']),
.button-group:has(> .select > button.btn[data-variant='outline']) {
> .input,
> .select > button {
@apply border-border focus-visible:border-ring;
@@ -276,8 +93,7 @@
@apply bg-muted gap-2 rounded-2xl border px-2.5 text-sm font-medium [&_svg:not([class*='size-'])]:size-4;
}
.button-group > hr[role='separator'],
.button-group-separator {
.button-group > hr[role='separator'] {
@apply bg-input;
}
@@ -389,8 +205,7 @@
}
/* Alert */
.alert,
.alert-destructive {
.alert {
@apply grid gap-0.5 rounded-2xl border px-4 py-3 text-start text-sm has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 [&>svg]:row-span-2 [&>svg]:translate-y-0.5 [&>svg]:text-current [&>svg:not([class*='size-'])]:size-4;
}
@@ -398,35 +213,33 @@
@apply bg-card text-card-foreground;
}
.alert-destructive {
.alert[data-variant='destructive'] {
@apply text-destructive bg-card [&>svg]:text-current;
}
.alert > :is(h2, h3, h4, h5, h6, strong, [data-title]),
.alert-destructive > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert[data-variant='destructive'] > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply font-medium;
}
:is(.alert, .alert-destructive):has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert:has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply col-start-2;
}
.alert > section {
@apply text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert-destructive > section {
.alert[data-variant='destructive'] > section {
@apply text-destructive/90 text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert > footer,
.alert-destructive > footer {
.alert[data-variant='destructive'] > footer {
@apply top-2.5 end-3;
}
/* Avatar */
.avatar,
.avatar-sm,
.avatar-lg {
.avatar {
@apply rounded-full after:rounded-[inherit];
}
@@ -434,29 +247,23 @@
@apply size-8;
}
.avatar-sm {
.avatar[data-size='sm'] {
@apply size-6;
}
.avatar-lg {
.avatar[data-size='lg'] {
@apply size-10;
}
.avatar > span:not([data-badge]),
.avatar-sm > span:not([data-badge]),
.avatar-lg > span:not([data-badge]) {
.avatar > span:not([data-badge]) {
@apply bg-muted text-muted-foreground rounded-[inherit];
}
.avatar > img,
.avatar-sm > img,
.avatar-lg > img {
.avatar > img {
@apply rounded-[inherit];
}
.avatar > [data-badge],
.avatar-sm > [data-badge],
.avatar-lg > [data-badge] {
.avatar > [data-badge] {
@apply bg-primary text-primary-foreground ring-background;
}
@@ -464,47 +271,41 @@
@apply bg-muted text-muted-foreground size-8 rounded-full text-sm [&>svg]:size-4;
}
.avatar-group:has(> .avatar-sm) > [data-count] {
.avatar-group:has(> .avatar[data-size='sm']) > [data-count] {
@apply size-6 [&>svg]:size-3;
}
.avatar-group:has(> .avatar-lg) > [data-count] {
.avatar-group:has(> .avatar[data-size='lg']) > [data-count] {
@apply size-10 [&>svg]:size-5;
}
/* Badge */
.badge,
.badge-primary,
.badge-secondary,
.badge-destructive,
.badge-outline,
.badge-ghost,
.badge-link {
@apply h-5 gap-1 rounded-2xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&>svg]:size-3!;
.badge {
@apply h-5 gap-1 rounded-2xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all [&>svg]:size-3!;
}
.badge,
.badge-primary {
.badge:not([data-variant]),
.badge[data-variant='primary'] {
@apply bg-primary text-primary-foreground [a]:hover:bg-primary/80;
}
.badge-secondary {
.badge[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80;
}
.badge-outline {
.badge[data-variant='outline'] {
@apply border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground;
}
.badge-destructive {
.badge[data-variant='destructive'] {
@apply bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20;
}
.badge-ghost {
.badge[data-variant='ghost'] {
@apply hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50;
}
.badge-link {
.badge[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
@@ -598,7 +399,7 @@
.combobox [role='option'].active,
.combobox [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
.combobox [role='group'] {
@@ -658,7 +459,7 @@
*:not(select).select [role='option'].active,
*:not(select).select [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
*:not(select).select [role='group'] {
@@ -716,7 +517,7 @@
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']).active {
@apply bg-accent text-accent-foreground;
@apply bg-muted text-foreground;
}
.dropdown-menu [data-variant='destructive'] {
@@ -752,11 +553,11 @@
@apply bg-border/50 -mx-1 my-1 h-px;
}
.dropdown-menu [data-shortcut] {
@apply text-muted-foreground text-xs tracking-widest;
.dropdown-menu :is([data-shortcut], kbd:last-child) {
@apply text-muted-foreground font-sans text-xs tracking-widest;
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) [data-shortcut] {
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) :is([data-shortcut], kbd:last-child) {
@apply text-accent-foreground;
}
@@ -780,8 +581,7 @@
}
/* Card */
.card,
.card-sm {
.card {
@apply bg-card text-card-foreground ring-foreground/5 dark:ring-foreground/10 overflow-hidden rounded-[min(var(--radius-4xl),24px)] text-sm shadow-sm ring-1 has-[>img:first-child]:pt-0 [&>img:first-child]:rounded-t-[min(var(--radius-4xl),24px)] [&>img:last-child]:rounded-b-[min(var(--radius-4xl),24px)];
}
@@ -789,48 +589,47 @@
@apply gap-5 py-5;
}
.card-sm {
.card[data-size='sm'] {
@apply gap-4 py-4;
}
.card > header,
.card-sm > header {
.card > header {
@apply gap-1.5 rounded-t-[min(var(--radius-4xl),24px)] px-5 [.border-b]:pb-5;
}
.card-sm > header {
.card[data-size='sm'] > header {
@apply px-4 [.border-b]:pb-4;
}
.card > header > :is(h2, h3, [data-title], .card-title),
.card-sm > header > :is(h2, h3, [data-title], .card-title) {
.card > header > :is(h2, h3, [data-title], .card-title) {
@apply text-base font-medium;
}
.card > header > :is(p, [data-description], .card-description),
.card-sm > header > :is(p, [data-description], .card-description) {
.card > header > :is(p, [data-description], .card-description) {
@apply text-muted-foreground text-sm;
}
.card > section,
.card-sm > section {
.card > section {
@apply px-5;
}
.card-sm > section {
.card[data-size='sm'] > section {
@apply px-4;
}
.card > footer,
.card-sm > footer {
.card > footer {
@apply rounded-b-[min(var(--radius-4xl),24px)] px-5 [.border-t]:pt-5;
}
.card-sm > footer {
.card[data-size='sm'] > footer {
@apply px-4 [.border-t]:pt-4;
}
/* Chart */
.chart {
--chart-bar-radius: 6px;
}
.chart-tooltip {
@apply border-border/50 bg-background text-foreground gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl;
}
@@ -923,8 +722,7 @@
@apply gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3;
}
.fieldset > legend,
.field-legend {
.fieldset > legend {
@apply mb-3 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base;
}
@@ -933,6 +731,8 @@
@apply gap-6;
&[role='radiogroup'],
&:has(> .field > input[type='checkbox']:not([role='switch'])),
&:has(> .field > input[type='radio']),
&[data-slot='checkbox-group'],
&[data-slot='radio-group'] {
@apply gap-3;
@@ -945,15 +745,15 @@
}
.field {
@apply data-[invalid=true]:text-destructive gap-3;
@apply data-invalid:text-destructive data-[invalid=true]:text-destructive gap-3;
}
.field > section,
.field-content {
.field > section {
@apply gap-1;
}
.field > label,
.field > section > label,
label:has(> .field) {
@apply has-[:checked]:bg-input/30 gap-2 leading-snug in-data-[disabled=true]:opacity-50;
}
@@ -976,18 +776,25 @@
@apply p-4;
}
.field h2,
.field h3,
.field-title {
.field h3 {
@apply gap-2 leading-snug text-sm font-medium in-data-[disabled=true]:opacity-50;
}
.fieldset legend + p,
.field > p,
.field section > p,
.field-description {
.field section > p {
@apply text-muted-foreground text-start text-sm [[data-variant=legend]+&]:-mt-1.5;
}
.fieldset > legend[data-variant='label'] {
@apply mb-0;
}
.fieldset > legend:not([data-variant]) + p,
.fieldset > legend[data-variant='legend'] + p {
@apply -mt-1.5;
}
.field-separator {
@apply -my-2 h-5 text-sm in-data-[variant=outline]:-mb-2;
}
@@ -996,8 +803,7 @@
@apply text-muted-foreground px-2;
}
.field [role="alert"],
.field-error {
.field [role="alert"] {
@apply text-destructive text-sm;
}
@@ -1010,16 +816,15 @@
.input-group:has(> :is(input, textarea, select, [data-control]):disabled) {
@apply opacity-50;
}
.input-group > :is(input, select, [data-control]) {
@apply px-2.5 py-1 text-sm;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='start']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='start'], [data-align='inline-start'])) > :is(input, select, [data-control]) {
@apply ps-1.5;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='end']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='end'], [data-align='inline-end'])) > :is(input, select, [data-control]) {
@apply pe-1.5;
}
@@ -1032,33 +837,74 @@
@apply gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none [&>svg:not([class*='size-'])]:size-4;
}
.input-group > [data-align='start'] {
.input-group > :is([data-align='start'], [data-align='inline-start']) {
@apply ps-2;
}
.input-group > [data-align='end'] {
.input-group > :is([data-align='end'], [data-align='inline-end']) {
@apply pe-2;
}
.input-group > [data-align='start']:has(> button) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> button) {
@apply -ms-1;
}
.input-group > [data-align='end']:has(> button) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> button) {
@apply -me-1;
}
.input-group > [data-align='start']:has(> kbd) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> kbd) {
@apply -ms-0.5;
}
.input-group > [data-align='end']:has(> kbd) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> kbd) {
@apply -me-0.5;
}
.input-group > [data-align='block-start'] {
@apply px-2.5 pt-2;
}
.input-group > [data-align='block-end'] {
@apply px-2.5 pb-2;
}
.input-group > [data-align='block-start'].border-b {
@apply pb-2;
}
.input-group > [data-align='block-end'].border-t {
@apply pt-2;
}
.input-group[data-orientation='vertical'] > :is(header, footer, [role='group'], [data-align]) {
@apply px-2.5;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']) {
@apply px-2.5 pt-2;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']) {
@apply px-2.5 pb-2;
}
.input-group[data-orientation='vertical']:has(> input) > :is(header[data-align='start'], [data-align='block-start']) {
@apply pt-2;
}
.input-group[data-orientation='vertical']:has(> input) > :is(footer[data-align='end'], [data-align='block-end']) {
@apply pb-2;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']).border-b {
@apply pb-2;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']).border-t {
@apply pt-2;
}
.input-group > :is(kbd, span > kbd, [role='group'] > kbd, header > kbd, footer > kbd) {
@apply rounded-2xl bg-muted-foreground/10 px-1.5;
}
@@ -1093,7 +939,7 @@
@apply text-lg font-medium tracking-tight;
}
.empty p {
.empty > header > p {
@apply text-muted-foreground text-sm/relaxed;
}
@@ -1144,15 +990,15 @@
@apply [&_svg:not([class*='size-'])]:size-4;
}
.item > figure:has(img) {
.item > figure:has(> img) {
@apply size-10 overflow-hidden rounded-xl;
}
.item[data-size='sm'] > figure:has(img) {
.item[data-size='sm'] > figure:has(> img) {
@apply size-8;
}
.item[data-size='xs'] > figure:has(img) {
.item[data-size='xs'] > figure:has(> img) {
@apply size-6 rounded-lg;
}
@@ -1202,16 +1048,16 @@
@apply bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 h-5 w-fit min-w-5 gap-1 rounded-lg px-1 font-sans text-xs font-medium [&_svg:not([class*='size-'])]:size-3;
}
.kbd-group {
@apply gap-1;
}
/* Label */
.field > label,
.label {
@apply gap-2 text-sm leading-none font-medium in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
.field > section > label {
@apply gap-2 text-sm leading-snug font-medium in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
/* Sidebar */
.sidebar nav {
@apply bg-sidebar text-sidebar-foreground;
@@ -1301,6 +1147,14 @@
.sidebar nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-xl px-3 text-sm focus-visible:ring-3 [&>svg]:size-4;
}
/* Progress */
.progress {
@apply bg-muted h-2 rounded-2xl;
}
.progress > span {
@apply bg-primary;
}
/* Skeleton */
.skeleton {
+144 -298
View File
@@ -1,247 +1,64 @@
/* Button */
@layer components {
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive,
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive,
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive,
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive,
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive,
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn {
@apply rounded-none border border-transparent bg-clip-padding text-xs font-semibold uppercase tracking-widest focus-visible:border-ring focus-visible:ring-ring/30 focus-visible:ring-[2px] aria-invalid:border-destructive aria-invalid:ring-[2px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*='size-'])]:size-3.5;
}
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive {
@apply h-10 gap-1.5 px-6;
&:has(> [data-icon=inline-start]) {
@apply ps-4;
}
&:has(> [data-icon=inline-end]) {
@apply pe-4;
}
.btn:not([data-size]),
.btn[data-size='default'] {
@apply h-10 gap-1.5 px-6 has-[[data-icon='inline-end']]:pr-4 has-[[data-icon='inline-start']]:pl-4;
}
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive {
@apply h-7 gap-1 px-3 [&_svg:not([class*='size-'])]:size-3;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn[data-size='xs'] {
@apply h-7 gap-1 px-3 [&_svg:not([class*='size-'])]:size-3 has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive {
@apply h-9 gap-1 px-4;
&:has(> [data-icon=inline-start]) {
@apply ps-3;
}
&:has(> [data-icon=inline-end]) {
@apply pe-3;
}
.btn[data-size='sm'] {
@apply h-9 gap-1 px-4 has-[[data-icon='inline-end']]:pr-3 has-[[data-icon='inline-start']]:pl-3;
}
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive {
@apply h-11 gap-1.5 px-8;
&:has(> [data-icon=inline-start]) {
@apply ps-5;
}
&:has(> [data-icon=inline-end]) {
@apply pe-5;
}
.btn[data-size='lg'] {
@apply h-11 gap-1.5 px-8 has-[[data-icon='inline-end']]:pr-5 has-[[data-icon='inline-start']]:pl-5;
}
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive {
.btn[data-size='icon'] {
@apply size-10;
}
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive {
.btn[data-size='icon-xs'] {
@apply size-7 [&_svg:not([class*='size-'])]:size-3;
}
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive {
.btn[data-size='icon-sm'] {
@apply size-9;
}
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn[data-size='icon-lg'] {
@apply size-11;
}
.btn,
.btn-primary,
.btn-xs,
.btn-xs-primary,
.btn-sm,
.btn-sm-primary,
.btn-lg,
.btn-lg-primary,
.btn-icon,
.btn-icon-primary,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-lg-icon,
.btn-lg-icon-primary {
.btn:not([data-variant]),
.btn[data-variant='primary'] {
@apply bg-primary text-primary-foreground hover:bg-primary/80;
}
.btn-secondary,
.btn-xs-secondary,
.btn-sm-secondary,
.btn-lg-secondary,
.btn-icon-secondary,
.btn-xs-icon-secondary,
.btn-sm-icon-secondary,
.btn-lg-icon-secondary {
.btn[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground;
}
.btn-outline,
.btn-xs-outline,
.btn-sm-outline,
.btn-lg-outline,
.btn-icon-outline,
.btn-xs-icon-outline,
.btn-sm-icon-outline,
.btn-lg-icon-outline {
.btn[data-variant='outline'] {
@apply border-border bg-transparent hover:bg-muted hover:text-foreground dark:hover:bg-input/30 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-ghost,
.btn-xs-ghost,
.btn-sm-ghost,
.btn-lg-ghost,
.btn-icon-ghost,
.btn-xs-icon-ghost,
.btn-sm-icon-ghost,
.btn-lg-icon-ghost {
.btn[data-variant='ghost'] {
@apply hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-link,
.btn-xs-link,
.btn-sm-link,
.btn-lg-link,
.btn-icon-link,
.btn-xs-icon-link,
.btn-sm-icon-link,
.btn-lg-icon-link {
.btn[data-variant='link'] {
@apply text-primary underline underline-offset-4 hover:underline;
}
.btn-destructive,
.btn-xs-destructive,
.btn-sm-destructive,
.btn-lg-destructive,
.btn-icon-destructive,
.btn-xs-icon-destructive,
.btn-sm-icon-destructive,
.btn-lg-icon-destructive {
.btn[data-variant='destructive'] {
@apply bg-destructive/10 text-destructive hover:bg-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40;
}
@@ -254,8 +71,8 @@
}
}
.button-group:has(> :is(.btn-outline, .btn-icon-outline, .btn-xs-outline, .btn-xs-icon-outline, .btn-sm-outline, .btn-sm-icon-outline, .btn-lg-outline, .btn-lg-icon-outline)),
.button-group:has(> .select > button:is(.btn-outline, .btn-icon-outline, .btn-xs-outline, .btn-xs-icon-outline, .btn-sm-outline, .btn-sm-icon-outline, .btn-lg-outline, .btn-lg-icon-outline)) {
.button-group:has(> .btn[data-variant='outline']),
.button-group:has(> .select > button.btn[data-variant='outline']) {
> .input,
> .select > button {
@apply border-border focus-visible:border-ring;
@@ -284,8 +101,7 @@
@apply gap-2 border border-transparent border-b-input bg-transparent px-2.5 text-xs font-semibold uppercase [&_svg:not([class*='size-'])]:size-3.5;
}
.button-group > hr[role='separator'],
.button-group-separator {
.button-group > hr[role='separator'] {
@apply bg-input;
}
@@ -397,8 +213,7 @@
}
/* Alert */
.alert,
.alert-destructive {
.alert {
@apply grid gap-1 bg-background border px-4 py-3 text-start text-sm has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 [&>svg]:row-span-2 [&>svg]:translate-y-0.5 [&>svg]:text-current [&>svg:not([class*='size-'])]:size-4 relative after:-inset-y-px after:-start-px after:w-0.5 after:absolute;
}
@@ -406,35 +221,33 @@
@apply bg-card text-card-foreground after:bg-foreground;
}
.alert-destructive {
.alert[data-variant='destructive'] {
@apply text-destructive bg-card [&>svg]:text-current after:bg-destructive;
}
.alert > :is(h2, h3, h4, h5, h6, strong, [data-title]),
.alert-destructive > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert[data-variant='destructive'] > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply font-semibold text-sm;
}
:is(.alert, .alert-destructive):has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert:has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply col-start-2;
}
.alert > section {
@apply text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert-destructive > section {
.alert[data-variant='destructive'] > section {
@apply text-destructive/90 text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert > footer,
.alert-destructive > footer {
.alert[data-variant='destructive'] > footer {
@apply top-2.5 end-3;
}
/* Avatar */
.avatar,
.avatar-sm,
.avatar-lg {
.avatar {
@apply rounded-full after:rounded-[inherit];
}
@@ -442,29 +255,23 @@
@apply size-8;
}
.avatar-sm {
.avatar[data-size='sm'] {
@apply size-6;
}
.avatar-lg {
.avatar[data-size='lg'] {
@apply size-10;
}
.avatar > span:not([data-badge]),
.avatar-sm > span:not([data-badge]),
.avatar-lg > span:not([data-badge]) {
.avatar > span:not([data-badge]) {
@apply bg-muted text-muted-foreground rounded-[inherit];
}
.avatar > img,
.avatar-sm > img,
.avatar-lg > img {
.avatar > img {
@apply rounded-[inherit];
}
.avatar > [data-badge],
.avatar-sm > [data-badge],
.avatar-lg > [data-badge] {
.avatar > [data-badge] {
@apply bg-primary text-primary-foreground ring-background;
}
@@ -472,47 +279,41 @@
@apply bg-muted text-muted-foreground size-8 rounded-full text-sm [&>svg]:size-4;
}
.avatar-group:has(> .avatar-sm) > [data-count] {
.avatar-group:has(> .avatar[data-size='sm']) > [data-count] {
@apply size-6 [&>svg]:size-3;
}
.avatar-group:has(> .avatar-lg) > [data-count] {
.avatar-group:has(> .avatar[data-size='lg']) > [data-count] {
@apply size-10 [&>svg]:size-5;
}
/* Badge */
.badge,
.badge-primary,
.badge-secondary,
.badge-destructive,
.badge-outline,
.badge-ghost,
.badge-link {
@apply gap-1.5 rounded-none border-0 bg-transparent px-0 py-0 text-[0.625rem] font-semibold uppercase tracking-widest transition-colors has-data-[icon=inline-end]:pe-0 has-data-[icon=inline-start]:ps-0 [&>svg]:size-3!;
.badge {
@apply gap-1.5 rounded-none border-0 bg-transparent px-0 py-0 text-[0.625rem] font-semibold uppercase tracking-widest transition-colors [&>svg]:size-3!;
}
.badge,
.badge-primary {
.badge:not([data-variant]),
.badge[data-variant='primary'] {
@apply text-foreground [a]:hover:text-foreground/70;
}
.badge-secondary {
.badge[data-variant='secondary'] {
@apply text-muted-foreground [a]:hover:text-foreground;
}
.badge-outline {
.badge[data-variant='outline'] {
@apply text-foreground [a]:hover:text-foreground/70;
}
.badge-destructive {
.badge[data-variant='destructive'] {
@apply text-destructive [a]:hover:text-destructive/70 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40;
}
.badge-ghost {
.badge[data-variant='ghost'] {
@apply text-muted-foreground hover:text-foreground;
}
.badge-link {
.badge[data-variant='link'] {
@apply text-foreground underline-offset-4 hover:underline;
}
@@ -606,7 +407,7 @@
.combobox [role='option'].active,
.combobox [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
.combobox [role='group'] {
@@ -666,7 +467,7 @@
*:not(select).select [role='option'].active,
*:not(select).select [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
*:not(select).select [role='group'] {
@@ -724,7 +525,7 @@
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']).active {
@apply bg-accent text-accent-foreground;
@apply bg-muted text-foreground;
}
.dropdown-menu [data-variant='destructive'] {
@@ -760,11 +561,11 @@
@apply bg-border/50 -mx-1.5 my-1.5 h-px;
}
.dropdown-menu [data-shortcut] {
@apply text-muted-foreground text-xs tracking-widest;
.dropdown-menu :is([data-shortcut], kbd:last-child) {
@apply text-muted-foreground font-sans text-xs tracking-widest;
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) [data-shortcut] {
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) :is([data-shortcut], kbd:last-child) {
@apply text-accent-foreground;
}
@@ -788,8 +589,7 @@
}
/* Card */
.card,
.card-sm {
.card {
@apply bg-card text-card-foreground ring-foreground/5 overflow-hidden text-sm shadow-sm ring-1 has-[>img:first-child]:pt-0 [&>img:first-child]:rounded-none [&>img:last-child]:rounded-none;
}
@@ -797,48 +597,47 @@
@apply gap-8 py-8;
}
.card-sm {
.card[data-size='sm'] {
@apply gap-5 py-5;
}
.card > header,
.card-sm > header {
.card > header {
@apply gap-1.5 rounded-none px-8 [.border-b]:pb-8;
}
.card-sm > header {
.card[data-size='sm'] > header {
@apply px-5 [.border-b]:pb-5;
}
.card > header > :is(h2, h3, [data-title], .card-title),
.card-sm > header > :is(h2, h3, [data-title], .card-title) {
.card > header > :is(h2, h3, [data-title], .card-title) {
@apply text-lg font-semibold tracking-wider uppercase;
}
.card > header > :is(p, [data-description], .card-description),
.card-sm > header > :is(p, [data-description], .card-description) {
.card > header > :is(p, [data-description], .card-description) {
@apply text-muted-foreground text-sm leading-relaxed;
}
.card > section,
.card-sm > section {
.card > section {
@apply px-8;
}
.card-sm > section {
.card[data-size='sm'] > section {
@apply px-5;
}
.card > footer,
.card-sm > footer {
.card > footer {
@apply px-8 [.border-t]:pt-8;
}
.card-sm > footer {
.card[data-size='sm'] > footer {
@apply px-5 [.border-t]:pt-5;
}
/* Chart */
.chart {
--chart-bar-radius: 0;
}
.chart-tooltip {
@apply border-border/50 bg-background text-foreground gap-1.5 rounded-none border px-2.5 py-1.5 text-xs shadow-xl;
}
@@ -935,8 +734,7 @@
@apply gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3;
}
.fieldset > legend,
.field-legend {
.fieldset > legend {
@apply mb-3 font-semibold uppercase data-[variant=label]:text-xs data-[variant=legend]:text-xs;
}
@@ -945,6 +743,8 @@
@apply gap-10;
&[role='radiogroup'],
&:has(> .field > input[type='checkbox']:not([role='switch'])),
&:has(> .field > input[type='radio']),
&[data-slot='checkbox-group'],
&[data-slot='radio-group'] {
@apply gap-3;
@@ -957,15 +757,15 @@
}
.field {
@apply data-[invalid=true]:text-destructive gap-3;
@apply data-invalid:text-destructive data-[invalid=true]:text-destructive gap-3;
}
.field > section,
.field-content {
.field > section {
@apply gap-1;
}
.field > label,
.field > section > label,
label:has(> .field) {
@apply has-[:checked]:bg-primary/5 has-[:checked]:border-primary/30 dark:has-[:checked]:border-primary/20 dark:has-[:checked]:bg-primary/10 gap-2 in-data-[disabled=true]:opacity-50 leading-relaxed;
}
@@ -988,18 +788,25 @@
@apply p-4;
}
.field h2,
.field h3,
.field-title {
.field h3 {
@apply gap-2 in-data-[disabled=true]:opacity-50 leading-normal font-semibold uppercase text-xs;
}
.fieldset legend + p,
.field > p,
.field section > p,
.field-description {
.field section > p {
@apply text-muted-foreground text-start text-sm [[data-variant=legend]+&]:-mt-1.5 normal-case tracking-normal;
}
.fieldset > legend[data-variant='label'] {
@apply mb-0;
}
.fieldset > legend:not([data-variant]) + p,
.fieldset > legend[data-variant='legend'] + p {
@apply -mt-1.5;
}
.field-separator {
@apply -my-2 h-5 text-sm in-data-[variant=outline]:-mb-2;
}
@@ -1008,8 +815,7 @@
@apply text-muted-foreground px-2;
}
.field [role="alert"],
.field-error {
.field [role="alert"] {
@apply text-destructive text-sm;
}
@@ -1022,16 +828,15 @@
.input-group:has(> :is(input, textarea, select, [data-control]):disabled) {
@apply opacity-50;
}
.input-group > :is(input, select, [data-control]) {
@apply px-0 py-1 text-sm;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='start']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='start'], [data-align='inline-start'])) > :is(input, select, [data-control]) {
@apply ps-2;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='end']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='end'], [data-align='inline-end'])) > :is(input, select, [data-control]) {
@apply pe-2;
}
@@ -1044,25 +849,58 @@
@apply gap-2 py-2 text-sm font-medium text-muted-foreground select-none [&>svg:not([class*='size-'])]:size-3.5;
}
.input-group > [data-align='start']:has(> button) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> button) {
@apply -ms-1;
}
.input-group > [data-align='end']:has(> button) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> button) {
@apply -me-1;
}
.input-group > [data-align='start']:has(> kbd) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> kbd) {
@apply -ms-0.5;
}
.input-group > [data-align='end']:has(> kbd) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> kbd) {
@apply -me-0.5;
}
.input-group[data-orientation='vertical'] > :is(header, footer, [role='group'], [data-align]) {
@apply px-0;
}
.input-group > [data-align='block-start'] {
@apply px-0 pt-2;
}
.input-group > [data-align='block-end'] {
@apply px-0 pb-2;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']) {
@apply px-0 pt-3;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']) {
@apply px-0 pb-3;
}
.input-group[data-orientation='vertical']:has(> input) > :is(header[data-align='start'], [data-align='block-start']) {
@apply pt-3.5;
}
.input-group[data-orientation='vertical']:has(> input) > :is(footer[data-align='end'], [data-align='block-end']) {
@apply pb-3.5;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']).border-b {
@apply pb-3.5;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']).border-t {
@apply pt-3.5;
}
.input-group > :is(kbd, span > kbd, [role='group'] > kbd, header > kbd, footer > kbd) {
@apply rounded-none bg-muted-foreground/10 px-1.5;
}
@@ -1097,7 +935,7 @@
@apply text-lg font-semibold tracking-wider uppercase;
}
.empty p {
.empty > header > p {
@apply text-muted-foreground mt-0.5 text-sm/relaxed;
}
@@ -1148,15 +986,15 @@
@apply [&_svg:not([class*='size-'])]:size-4;
}
.item > figure:has(img) {
.item > figure:has(> img) {
@apply size-10 overflow-hidden rounded-none;
}
.item[data-size='sm'] > figure:has(img) {
.item[data-size='sm'] > figure:has(> img) {
@apply size-8;
}
.item[data-size='xs'] > figure:has(img) {
.item[data-size='xs'] > figure:has(> img) {
@apply size-6 rounded-none;
}
@@ -1206,20 +1044,20 @@
@apply bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 h-5.5 w-fit min-w-5.5 gap-1 rounded-none px-1.5 font-sans text-xs font-medium [&_svg:not([class*='size-'])]:size-3;
}
.kbd-group {
@apply gap-1;
}
/* Label */
.field > label,
.label {
@apply gap-2 font-semibold leading-relaxed in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 uppercase text-xs tracking-wide;
}
:is(.field > label, .label):has(input[type='checkbox']),
:is(.field > label, .label):has(input[type='radio']),
:is(.field > label, .label):has(input[role='switch']),
:is(input[type='checkbox'], input[type='radio'], input[role='switch']) + :is(.field > label, .label) {
.field > section > label {
@apply gap-2 font-semibold leading-relaxed in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 uppercase text-xs tracking-wide;
}
:is(.field > label, .field > section > label, .label):has(input[type='checkbox']),
:is(.field > label, .field > section > label, .label):has(input[type='radio']),
:is(.field > label, .field > section > label, .label):has(input[role='switch']),
:is(input[type='checkbox'], input[type='radio'], input[role='switch']) + :is(.field > label, .field > section > label, .label) {
@apply text-sm font-normal normal-case tracking-normal;
}
@@ -1312,6 +1150,14 @@
.sidebar nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-none px-3 text-sm focus-visible:ring-2 [&>svg]:size-3.5;
}
/* Progress */
.progress {
@apply bg-muted h-0.5 rounded-none;
}
.progress > span {
@apply bg-primary;
}
/* Skeleton */
.skeleton {
+149 -295
View File
@@ -1,247 +1,64 @@
/* Button */
@layer components {
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive,
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive,
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive,
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive,
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive,
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn {
@apply rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*='size-'])]:size-4;
}
.btn,
.btn-primary,
.btn-secondary,
.btn-outline,
.btn-ghost,
.btn-link,
.btn-destructive {
@apply h-9 gap-1.5 px-2.5;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn:not([data-size]),
.btn[data-size='default'] {
@apply h-9 gap-1.5 px-2.5 has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-xs,
.btn-xs-primary,
.btn-xs-secondary,
.btn-xs-outline,
.btn-xs-ghost,
.btn-xs-link,
.btn-xs-destructive {
@apply h-6 gap-1 px-2 text-xs rounded-[min(var(--radius-md),8px)] [&_svg:not([class*='size-'])]:size-3;
&:has(> [data-icon=inline-start]) {
@apply ps-1.5;
}
&:has(> [data-icon=inline-end]) {
@apply pe-1.5;
}
.btn[data-size='xs'] {
@apply h-6 gap-1 px-2 text-xs rounded-[min(var(--radius-md),8px)] [&_svg:not([class*='size-'])]:size-3 has-[[data-icon='inline-end']]:pr-1.5 has-[[data-icon='inline-start']]:pl-1.5;
}
.btn-sm,
.btn-sm-primary,
.btn-sm-secondary,
.btn-sm-outline,
.btn-sm-ghost,
.btn-sm-link,
.btn-sm-destructive {
@apply h-8 gap-1 px-2.5 rounded-[min(var(--radius-md),10px)];
&:has(> [data-icon=inline-start]) {
@apply ps-1.5;
}
&:has(> [data-icon=inline-end]) {
@apply pe-1.5;
}
.btn[data-size='sm'] {
@apply h-8 gap-1 px-2.5 rounded-[min(var(--radius-md),10px)] has-[[data-icon='inline-end']]:pr-1.5 has-[[data-icon='inline-start']]:pl-1.5;
}
.btn-lg,
.btn-lg-primary,
.btn-lg-secondary,
.btn-lg-outline,
.btn-lg-ghost,
.btn-lg-link,
.btn-lg-destructive {
@apply h-10 gap-1.5 px-2.5;
&:has(> [data-icon=inline-start]) {
@apply ps-2;
}
&:has(> [data-icon=inline-end]) {
@apply pe-2;
}
.btn[data-size='lg'] {
@apply h-10 gap-1.5 px-2.5 has-[[data-icon='inline-end']]:pr-2 has-[[data-icon='inline-start']]:pl-2;
}
.btn-icon,
.btn-icon-primary,
.btn-icon-secondary,
.btn-icon-outline,
.btn-icon-ghost,
.btn-icon-link,
.btn-icon-destructive {
.btn[data-size='icon'] {
@apply size-9;
}
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-xs-icon-secondary,
.btn-xs-icon-outline,
.btn-xs-icon-ghost,
.btn-xs-icon-link,
.btn-xs-icon-destructive {
.btn[data-size='icon-xs'] {
@apply size-6 rounded-[min(var(--radius-md),8px)] [&_svg:not([class*='size-'])]:size-3;
}
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-sm-icon-secondary,
.btn-sm-icon-outline,
.btn-sm-icon-ghost,
.btn-sm-icon-link,
.btn-sm-icon-destructive {
.btn[data-size='icon-sm'] {
@apply size-8 rounded-[min(var(--radius-md),10px)];
}
.btn-lg-icon,
.btn-lg-icon-primary,
.btn-lg-icon-secondary,
.btn-lg-icon-outline,
.btn-lg-icon-ghost,
.btn-lg-icon-link,
.btn-lg-icon-destructive {
.btn[data-size='icon-lg'] {
@apply size-10;
}
.btn,
.btn-primary,
.btn-xs,
.btn-xs-primary,
.btn-sm,
.btn-sm-primary,
.btn-lg,
.btn-lg-primary,
.btn-icon,
.btn-icon-primary,
.btn-xs-icon,
.btn-xs-icon-primary,
.btn-sm-icon,
.btn-sm-icon-primary,
.btn-lg-icon,
.btn-lg-icon-primary {
.btn:not([data-variant]),
.btn[data-variant='primary'] {
@apply bg-primary text-primary-foreground hover:bg-primary/80;
}
.btn-secondary,
.btn-xs-secondary,
.btn-sm-secondary,
.btn-lg-secondary,
.btn-icon-secondary,
.btn-xs-icon-secondary,
.btn-sm-icon-secondary,
.btn-lg-icon-secondary {
.btn[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground;
}
.btn-outline,
.btn-xs-outline,
.btn-sm-outline,
.btn-lg-outline,
.btn-icon-outline,
.btn-xs-icon-outline,
.btn-sm-icon-outline,
.btn-lg-icon-outline {
.btn[data-variant='outline'] {
@apply border-border bg-background shadow-xs hover:bg-muted hover:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-ghost,
.btn-xs-ghost,
.btn-sm-ghost,
.btn-lg-ghost,
.btn-icon-ghost,
.btn-xs-icon-ghost,
.btn-sm-icon-ghost,
.btn-lg-icon-ghost {
.btn[data-variant='ghost'] {
@apply hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground;
}
.btn-link,
.btn-xs-link,
.btn-sm-link,
.btn-lg-link,
.btn-icon-link,
.btn-xs-icon-link,
.btn-sm-icon-link,
.btn-lg-icon-link {
.btn[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
.btn-destructive,
.btn-xs-destructive,
.btn-sm-destructive,
.btn-lg-destructive,
.btn-icon-destructive,
.btn-xs-icon-destructive,
.btn-sm-icon-destructive,
.btn-lg-icon-destructive {
.btn[data-variant='destructive'] {
@apply bg-destructive/10 text-destructive hover:bg-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40;
}
@@ -268,8 +85,7 @@
@apply bg-muted gap-2 rounded-md border px-2.5 text-sm font-medium shadow-xs [&_svg:not([class*='size-'])]:size-4;
}
.button-group > hr[role='separator'],
.button-group-separator {
.button-group > hr[role='separator'] {
@apply bg-input;
}
@@ -381,8 +197,7 @@
}
/* Alert */
.alert,
.alert-destructive {
.alert {
@apply grid gap-0.5 rounded-lg border px-4 py-3 text-start text-sm has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 [&>svg]:row-span-2 [&>svg]:translate-y-0.5 [&>svg]:text-current [&>svg:not([class*='size-'])]:size-4;
}
@@ -390,35 +205,33 @@
@apply bg-card text-card-foreground;
}
.alert-destructive {
.alert[data-variant='destructive'] {
@apply text-destructive bg-card [&>svg]:text-current;
}
.alert > :is(h2, h3, h4, h5, h6, strong, [data-title]),
.alert-destructive > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert[data-variant='destructive'] > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply font-medium;
}
:is(.alert, .alert-destructive):has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
.alert:has(> svg) > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
@apply col-start-2;
}
.alert > section {
@apply text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert-destructive > section {
.alert[data-variant='destructive'] > section {
@apply text-destructive/90 text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4;
}
.alert > footer,
.alert-destructive > footer {
.alert[data-variant='destructive'] > footer {
@apply top-2.5 end-3;
}
/* Avatar */
.avatar,
.avatar-sm,
.avatar-lg {
.avatar {
@apply rounded-full after:rounded-[inherit];
}
@@ -426,29 +239,23 @@
@apply size-8;
}
.avatar-sm {
.avatar[data-size='sm'] {
@apply size-6;
}
.avatar-lg {
.avatar[data-size='lg'] {
@apply size-10;
}
.avatar > span:not([data-badge]),
.avatar-sm > span:not([data-badge]),
.avatar-lg > span:not([data-badge]) {
.avatar > span:not([data-badge]) {
@apply bg-muted text-muted-foreground rounded-[inherit];
}
.avatar > img,
.avatar-sm > img,
.avatar-lg > img {
.avatar > img {
@apply rounded-[inherit];
}
.avatar > [data-badge],
.avatar-sm > [data-badge],
.avatar-lg > [data-badge] {
.avatar > [data-badge] {
@apply bg-primary text-primary-foreground ring-background;
}
@@ -456,47 +263,41 @@
@apply bg-muted text-muted-foreground size-8 rounded-full text-sm [&>svg]:size-4;
}
.avatar-group:has(> .avatar-sm) > [data-count] {
.avatar-group:has(> .avatar[data-size='sm']) > [data-count] {
@apply size-6 [&>svg]:size-3;
}
.avatar-group:has(> .avatar-lg) > [data-count] {
.avatar-group:has(> .avatar[data-size='lg']) > [data-count] {
@apply size-10 [&>svg]:size-5;
}
/* Badge */
.badge,
.badge-primary,
.badge-secondary,
.badge-destructive,
.badge-outline,
.badge-ghost,
.badge-link {
@apply h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&>svg]:size-3!;
.badge {
@apply h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all [&>svg]:size-3!;
}
.badge,
.badge-primary {
.badge:not([data-variant]),
.badge[data-variant='primary'] {
@apply bg-primary text-primary-foreground [a]:hover:bg-primary/80;
}
.badge-secondary {
.badge[data-variant='secondary'] {
@apply bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80;
}
.badge-outline {
.badge[data-variant='outline'] {
@apply border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground;
}
.badge-destructive {
.badge[data-variant='destructive'] {
@apply bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20;
}
.badge-ghost {
.badge[data-variant='ghost'] {
@apply hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50;
}
.badge-link {
.badge[data-variant='link'] {
@apply text-primary underline-offset-4 hover:underline;
}
@@ -590,7 +391,7 @@
.combobox [role='option'].active,
.combobox [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
.combobox [role='group'] {
@@ -650,7 +451,7 @@
*:not(select).select [role='option'].active,
*:not(select).select [role='option']:focus-visible {
@apply bg-accent text-accent-foreground [&_svg]:text-accent-foreground;
@apply bg-muted text-foreground [&_svg]:text-foreground;
}
*:not(select).select [role='group'] {
@@ -704,7 +505,7 @@
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']).active {
@apply bg-accent text-accent-foreground;
@apply bg-muted text-foreground;
}
.dropdown-menu [data-variant='destructive'] {
@@ -740,11 +541,11 @@
@apply bg-border -mx-1 my-1 h-px;
}
.dropdown-menu [data-shortcut] {
@apply text-muted-foreground text-xs tracking-widest;
.dropdown-menu :is([data-shortcut], kbd:last-child) {
@apply text-muted-foreground font-sans text-xs tracking-widest;
}
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) [data-shortcut] {
.dropdown-menu :is([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']):is(:focus-visible, .active) :is([data-shortcut], kbd:last-child) {
@apply text-accent-foreground;
}
@@ -768,8 +569,7 @@
}
/* Card */
.card,
.card-sm {
.card {
@apply ring-foreground/10 bg-card text-card-foreground overflow-hidden rounded-xl text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 [&>img:first-child]:rounded-t-xl [&>img:last-child]:rounded-b-xl;
}
@@ -777,52 +577,51 @@
@apply gap-6 py-6;
}
.card-sm {
.card[data-size='sm'] {
@apply gap-4 py-4;
}
.card > header,
.card-sm > header {
.card > header {
@apply gap-1 rounded-t-xl px-6 [.border-b]:pb-6;
}
.card-sm > header {
.card[data-size='sm'] > header {
@apply px-4 [.border-b]:pb-4;
}
.card > header > :is(h2, h3, [data-title], .card-title),
.card-sm > header > :is(h2, h3, [data-title], .card-title) {
.card > header > :is(h2, h3, [data-title], .card-title) {
@apply text-base leading-normal font-medium;
}
.card-sm > header > :is(h2, h3, [data-title], .card-title) {
.card[data-size='sm'] > header > :is(h2, h3, [data-title], .card-title) {
@apply text-sm;
}
.card > header > :is(p, [data-description], .card-description),
.card-sm > header > :is(p, [data-description], .card-description) {
.card > header > :is(p, [data-description], .card-description) {
@apply text-muted-foreground text-sm;
}
.card > section,
.card-sm > section {
.card > section {
@apply px-6;
}
.card-sm > section {
.card[data-size='sm'] > section {
@apply px-4;
}
.card > footer,
.card-sm > footer {
.card > footer {
@apply rounded-b-xl px-6 [.border-t]:pt-6;
}
.card-sm > footer {
.card[data-size='sm'] > footer {
@apply px-4 [.border-t]:pt-4;
}
/* Chart */
.chart {
--chart-bar-radius: 6px;
}
.chart-tooltip {
@apply border-border/50 bg-background text-foreground gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl;
}
@@ -915,8 +714,7 @@
@apply gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3;
}
.fieldset > legend,
.field-legend {
.fieldset > legend {
@apply mb-3 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base;
}
@@ -925,6 +723,8 @@
@apply gap-7;
&[role='radiogroup'],
&:has(> .field > input[type='checkbox']:not([role='switch'])),
&:has(> .field > input[type='radio']),
&[data-slot='checkbox-group'],
&[data-slot='radio-group'] {
@apply gap-3;
@@ -937,15 +737,15 @@
}
.field {
@apply data-[invalid=true]:text-destructive gap-3;
@apply data-invalid:text-destructive data-[invalid=true]:text-destructive gap-3;
}
.field > section,
.field-content {
.field > section {
@apply gap-1;
}
.field > label,
.field > section > label,
label:has(> .field) {
@apply has-[:checked]:bg-primary/5 has-[:checked]:border-primary/30 dark:has-[:checked]:border-primary/20 dark:has-[:checked]:bg-primary/10 gap-2 leading-snug in-data-[disabled=true]:opacity-50;
}
@@ -968,18 +768,25 @@
@apply p-3;
}
.field h2,
.field h3,
.field-title {
.field h3 {
@apply gap-2 leading-snug text-sm font-medium in-data-[disabled=true]:opacity-50;
}
.fieldset legend + p,
.field > p,
.field section > p,
.field-description {
.field section > p {
@apply text-muted-foreground text-start text-sm [[data-variant=legend]+&]:-mt-1.5;
}
.fieldset > legend[data-variant='label'] {
@apply mb-0;
}
.fieldset > legend:not([data-variant]) + p,
.fieldset > legend[data-variant='legend'] + p {
@apply -mt-1.5;
}
.field-separator {
@apply -my-2 h-5 text-sm in-data-[variant=outline]:-mb-2;
}
@@ -988,8 +795,7 @@
@apply text-muted-foreground px-2;
}
.field [role="alert"],
.field-error {
.field [role="alert"] {
@apply text-destructive text-sm;
}
@@ -1002,16 +808,15 @@
.input-group:has(> :is(input, textarea, select, [data-control]):disabled) {
@apply opacity-50;
}
.input-group > :is(input, select, [data-control]) {
@apply px-2.5 py-1 text-sm;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='start']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='start'], [data-align='inline-start'])) > :is(input, select, [data-control]) {
@apply ps-1.5;
}
.input-group:not([data-orientation='vertical']):has(> [data-align='end']) > :is(input, select, [data-control]) {
.input-group:not([data-orientation='vertical']):has(> :is([data-align='end'], [data-align='inline-end'])) > :is(input, select, [data-control]) {
@apply pe-1.5;
}
@@ -1024,33 +829,74 @@
@apply gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none [&>svg:not([class*='size-'])]:size-4;
}
.input-group > [data-align='start'] {
.input-group > :is([data-align='start'], [data-align='inline-start']) {
@apply ps-2;
}
.input-group > [data-align='end'] {
.input-group > :is([data-align='end'], [data-align='inline-end']) {
@apply pe-2;
}
.input-group > [data-align='start']:has(> button) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> button) {
@apply -ms-1;
}
.input-group > [data-align='end']:has(> button) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> button) {
@apply -me-1;
}
.input-group > [data-align='start']:has(> kbd) {
.input-group > :is([data-align='start'], [data-align='inline-start']):has(> kbd) {
@apply -ms-0.5;
}
.input-group > [data-align='end']:has(> kbd) {
.input-group > :is([data-align='end'], [data-align='inline-end']):has(> kbd) {
@apply -me-0.5;
}
.input-group > [data-align='block-start'] {
@apply px-2.5 pt-2;
}
.input-group > [data-align='block-end'] {
@apply px-2.5 pb-2;
}
.input-group > [data-align='block-start'].border-b {
@apply pb-2;
}
.input-group > [data-align='block-end'].border-t {
@apply pt-2;
}
.input-group[data-orientation='vertical'] > :is(header, footer, [role='group'], [data-align]) {
@apply px-2.5;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']) {
@apply px-2.5 pt-2;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']) {
@apply px-2.5 pb-2;
}
.input-group[data-orientation='vertical']:has(> input) > :is(header[data-align='start'], [data-align='block-start']) {
@apply pt-2;
}
.input-group[data-orientation='vertical']:has(> input) > :is(footer[data-align='end'], [data-align='block-end']) {
@apply pb-2;
}
.input-group[data-orientation='vertical'] > :is(header[data-align='start'], [data-align='block-start']).border-b {
@apply pb-2;
}
.input-group[data-orientation='vertical'] > :is(footer[data-align='end'], [data-align='block-end']).border-t {
@apply pt-2;
}
.input-group > :is(kbd, span > kbd, [role='group'] > kbd, header > kbd, footer > kbd) {
@apply rounded-[calc(var(--radius)-5px)];
}
@@ -1085,7 +931,7 @@
@apply text-lg font-medium tracking-tight;
}
.empty p {
.empty > header > p {
@apply text-muted-foreground text-sm/relaxed;
}
@@ -1136,15 +982,15 @@
@apply [&_svg:not([class*='size-'])]:size-4;
}
.item > figure:has(img) {
.item > figure:has(> img) {
@apply size-10 overflow-hidden rounded-sm;
}
.item[data-size='sm'] > figure:has(img) {
.item[data-size='sm'] > figure:has(> img) {
@apply size-8;
}
.item[data-size='xs'] > figure:has(img) {
.item[data-size='xs'] > figure:has(> img) {
@apply size-6;
}
@@ -1194,16 +1040,16 @@
@apply bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 h-5 w-fit min-w-5 gap-1 rounded-sm px-1 font-sans text-xs font-medium [&_svg:not([class*='size-'])]:size-3;
}
.kbd-group {
@apply gap-1;
}
/* Label */
.field > label,
.label {
@apply gap-2 text-sm leading-none font-medium in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
.field > section > label {
@apply gap-2 text-sm leading-snug font-medium in-data-[disabled=true]:opacity-50 peer-disabled:opacity-50;
}
/* Sidebar */
.sidebar nav {
@apply bg-sidebar text-sidebar-foreground;
@@ -1293,6 +1139,14 @@
.sidebar nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 text-sm focus-visible:ring-2 [&>svg]:size-4;
}
/* Progress */
.progress {
@apply bg-muted h-1.5 rounded-full;
}
.progress > span {
@apply bg-primary;
}
/* Skeleton */
.skeleton {
+1 -1
View File
@@ -83,7 +83,7 @@
</footer>
{% endif %}
{% if close_button %}
<button type="button" class="btn-sm-icon-ghost" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" 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>
{% endif %}
+2 -2
View File
@@ -90,7 +90,7 @@
{% if action.href %}
<a
href="{{ action.href }}"
class="btn-sm"
class="btn" data-size="sm"
data-toast-action
>{{ action.label }}</a>
{% else %}
@@ -105,7 +105,7 @@
{% if cancel %}
<button
type="button"
class="btn-sm-outline"
class="btn" data-variant="outline" data-size="sm"
data-toast-cancel
{% if cancel.onclick %}onclick="{{ cancel.onclick }}"{% endif %}
>{{ cancel.label }}</button>
+1
View File
@@ -129,6 +129,7 @@
const dark = mode === 'dark';
document.documentElement.classList.toggle('dark', dark);
try { localStorage.setItem('themeMode', dark ? 'dark' : 'light'); } catch (_) {}
document.dispatchEvent(new CustomEvent('basecoat:themechange', { detail: { mode: dark ? 'dark' : 'light' } }));
};
const getTheme = () => document.documentElement.classList.contains('dark') ? 'dark' : 'light';
+112 -9
View File
@@ -1,6 +1,8 @@
(() => {
const instances = new WeakMap();
const activeCanvases = new Set();
const defaultColors = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)'];
let refreshFrame = null;
const isChartJsData = (data) => {
return data && typeof data === 'object' && Array.isArray(data.datasets);
@@ -54,6 +56,32 @@
const colorForIndex = (index) => defaultColors[index % defaultColors.length];
const cloneChartData = (chartData) => {
if (!isChartJsData(chartData)) return chartData;
return {
...chartData,
labels: Array.isArray(chartData.labels) ? [...chartData.labels] : chartData.labels,
datasets: chartData.datasets.map((dataset) => ({
...dataset,
data: Array.isArray(dataset.data) ? [...dataset.data] : dataset.data,
backgroundColor: Array.isArray(dataset.backgroundColor) ? [...dataset.backgroundColor] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? [...dataset.borderColor] : dataset.borderColor,
hoverBackgroundColor: Array.isArray(dataset.hoverBackgroundColor) ? [...dataset.hoverBackgroundColor] : dataset.hoverBackgroundColor,
hoverBorderColor: Array.isArray(dataset.hoverBorderColor) ? [...dataset.hoverBorderColor] : dataset.hoverBorderColor,
})),
};
};
const clonePlainObject = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
key,
item && typeof item === 'object' && !Array.isArray(item) ? clonePlainObject(item) : item,
]));
};
const resolveColor = (color, element) => {
if (Array.isArray(color)) return color.map(item => resolveColor(item, element));
if (typeof color !== 'string') return color;
@@ -148,15 +176,25 @@
const usesCartesianScales = (type) => !['pie', 'doughnut', 'polarArea', 'radar'].includes(type);
const moveCanvasClassesToContainer = (canvas, container) => {
const classes = Array.from(canvas.classList).filter((className) => className !== 'chart');
if (classes.length === 0) return;
container.classList.add(...classes);
canvas.classList.remove(...classes);
};
const ensureContainer = (canvas) => {
if (canvas.parentElement?.classList.contains('chart')) {
canvas.parentElement.dataset.basecoatChartContainer = 'true';
moveCanvasClassesToContainer(canvas, canvas.parentElement);
return canvas.parentElement;
}
const container = document.createElement('div');
container.className = 'chart';
container.dataset.basecoatChartContainer = 'true';
moveCanvasClassesToContainer(canvas, container);
canvas.insertAdjacentElement('beforebegin', container);
container.append(canvas);
return container;
@@ -170,7 +208,7 @@
};
const toChartData = ({ type, labelKey, data, series }) => {
if (isChartJsData(data)) return data;
if (isChartJsData(data)) return cloneChartData(data);
const rows = Array.isArray(data) ? data : [];
const entries = seriesEntries(series);
@@ -223,6 +261,23 @@
return typeof value === 'number' ? value.toLocaleString() : String(value);
};
const colorForTooltipItem = (item) => {
const elementColor = item.element?.options?.backgroundColor || item.element?.options?.borderColor;
if (typeof elementColor === 'string') return elementColor;
const backgroundColor = Array.isArray(item.dataset.backgroundColor)
? item.dataset.backgroundColor[item.dataIndex]
: item.dataset.backgroundColor;
if (typeof backgroundColor === 'string') return backgroundColor;
const borderColor = Array.isArray(item.dataset.borderColor)
? item.dataset.borderColor[item.dataIndex]
: item.dataset.borderColor;
if (typeof borderColor === 'string') return borderColor;
return colorForIndex(item.datasetIndex);
};
const ensureTooltip = (canvas) => {
let tooltip = canvas._basecoatChartTooltip;
if (tooltip) return tooltip;
@@ -258,7 +313,7 @@
const items = document.createElement('div');
items.className = 'chart-tooltip-items';
tooltip.dataPoints.forEach((item) => {
const color = item.dataset.borderColor || item.dataset.backgroundColor || colorForIndex(item.datasetIndex);
const color = colorForTooltipItem(item);
const value = item.formattedValue || formatValue(item.raw);
const row = document.createElement('div');
@@ -284,8 +339,8 @@
const rect = chart.canvas.getBoundingClientRect();
element.replaceChildren(...children);
element.hidden = false;
element.style.left = `${rect.left + window.scrollX + tooltip.caretX}px`;
element.style.top = `${rect.top + window.scrollY + tooltip.caretY}px`;
element.style.left = `${rect.left + tooltip.caretX}px`;
element.style.top = `${rect.top + tooltip.caretY}px`;
};
const createLegendPlugin = (canvas, enabled) => ({
@@ -308,7 +363,12 @@
const indicator = document.createElement('span');
indicator.className = 'chart-legend-indicator';
indicator.style.setProperty('--chart-indicator-color', item.fillStyle || item.strokeStyle || colorForIndex(item.datasetIndex || item.index || 0));
const indicatorColor = typeof item.fillStyle === 'string'
? item.fillStyle
: typeof item.strokeStyle === 'string'
? item.strokeStyle
: colorForIndex(item.datasetIndex || item.index || 0);
indicator.style.setProperty('--chart-indicator-color', indicatorColor);
const label = document.createElement('span');
label.textContent = item.text;
@@ -345,7 +405,7 @@
const tooltip = readBoolean(canvas, config, 'tooltip', 'chartTooltip', true);
const data = config.data || readJsonAttribute(canvas, 'data-chart-data') || [];
const series = config.series || readJsonAttribute(canvas, 'data-chart-series') || {};
const chartData = config.chartData || toChartData({ type, labelKey, data, series });
const chartData = config.chartData ? cloneChartData(config.chartData) : toChartData({ type, labelKey, data, series });
const userOptions = config.options || {};
const legendPlugin = createLegendPlugin(canvas, legend);
const container = ensureContainer(canvas);
@@ -359,19 +419,21 @@
border: { display: false, color: borderColor },
grid: { display: false, color: gridColor },
ticks: { color: mutedColor, padding: 8 },
...(userOptions.scales?.x || {}),
...clonePlainObject(userOptions.scales?.x || {}),
},
y: {
border: { display: false, color: borderColor },
grid: { color: gridColor },
ticks: { display: false, color: mutedColor },
...(userOptions.scales?.y || {}),
...clonePlainObject(userOptions.scales?.y || {}),
},
}
: {};
const scales = {
...defaultScales,
...(Object.fromEntries(Object.entries(userOptions.scales || {}).filter(([key]) => key !== 'x' && key !== 'y'))),
...(Object.fromEntries(Object.entries(userOptions.scales || {})
.filter(([key]) => key !== 'x' && key !== 'y')
.map(([key, value]) => [key, clonePlainObject(value)]))),
};
canvas.classList.remove('chart');
@@ -407,14 +469,18 @@
});
instances.set(canvas, chart);
activeCanvases.add(canvas);
canvas.dataset.chartInitialized = 'true';
canvas.chart = chart;
canvas._basecoatChartConfig = config;
canvas._destroy = () => {
chart.destroy();
removeGeneratedElements(canvas);
instances.delete(canvas);
activeCanvases.delete(canvas);
canvas.removeAttribute('data-chart-initialized');
delete canvas._basecoatChartContainer;
delete canvas._basecoatChartConfig;
delete canvas.chart;
delete canvas._destroy;
};
@@ -428,7 +494,44 @@
return charts.length === 1 ? charts[0] : charts;
};
const refreshCharts = () => {
refreshFrame = null;
Array.from(activeCanvases).forEach((canvas) => {
if (!canvas.isConnected) {
canvas._destroy?.();
activeCanvases.delete(canvas);
return;
}
initChart(canvas, canvas._basecoatChartConfig || {});
});
};
const scheduleRefresh = () => {
if (refreshFrame !== null) return;
refreshFrame = requestAnimationFrame(refreshCharts);
};
const observeTokenChanges = () => {
const observer = new MutationObserver((mutations) => {
if (mutations.some((mutation) => mutation.attributeName === 'class' || mutation.attributeName === 'data-style-variant')) {
scheduleRefresh();
}
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'data-style-variant'],
});
document.addEventListener('basecoat:themechange', scheduleRefresh);
document.addEventListener('basecoat:stylechange', scheduleRefresh);
};
if (window.basecoat) {
window.basecoat.chart = chart;
}
observeTokenChanges();
})();
+1 -1
View File
@@ -151,7 +151,7 @@
? `<button type="button" class="btn" data-toast-action onclick="${action.onclick}">${action.label}</button>`
: '';
const cancelHtml = cancel
? `<button type="button" class="btn-outline h-6 text-xs px-2.5 rounded-sm" data-toast-cancel onclick="${cancel?.onclick || ''}">${cancel.label}</button>`
? `<button type="button" class="btn h-6 text-xs px-2.5 rounded-sm" data-variant="outline" data-toast-cancel onclick="${cancel?.onclick || ''}">${cancel.label}</button>`
: '';
const footerHtml = actionHtml || cancelHtml ? `<footer>${actionHtml}${cancelHtml}</footer>` : '';
+1 -1
View File
@@ -83,7 +83,7 @@
</footer>
{% endif %}
{% if close_button %}
<button type="button" class="btn-sm-icon-ghost" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" 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>
{% endif %}
+2 -2
View File
@@ -90,7 +90,7 @@
{% if action.href %}
<a
href="{{ action.href }}"
class="btn-sm"
class="btn" data-size="sm"
data-toast-action
>{{ action.label }}</a>
{% else %}
@@ -105,7 +105,7 @@
{% if cancel %}
<button
type="button"
class="btn-sm-outline"
class="btn" data-variant="outline" data-size="sm"
data-toast-cancel
{% if cancel.onclick %}onclick="{{ cancel.onclick }}"{% endif %}
>{{ cancel.label }}</button>
+730
View File
@@ -0,0 +1,730 @@
---
import "../css/landing.css";
import basecoatJsUrl from "basecoat-css/basecoat.min?url";
import dropdownMenuJsUrl from "basecoat-css/dropdown-menu.min?url";
import vegaUrl from "../../docs/src/assets/styles/style-vega.css?url";
import novaUrl from "../../docs/src/assets/styles/style-nova.css?url";
import maiaUrl from "../../docs/src/assets/styles/style-maia.css?url";
import lyraUrl from "../../docs/src/assets/styles/style-lyra.css?url";
import miraUrl from "../../docs/src/assets/styles/style-mira.css?url";
import lumaUrl from "../../docs/src/assets/styles/style-luma.css?url";
import seraUrl from "../../docs/src/assets/styles/style-sera.css?url";
import rheaUrl from "../../docs/src/assets/styles/style-rhea.css?url";
import socialImage from "../../docs/src/assets/social.png";
import favicon from "../../docs/src/assets/favicon.svg";
import ArrowRight from "lucide-static/icons/arrow-right.svg?raw";
import Activity from "lucide-static/icons/activity.svg?raw";
import BanknoteArrowUp from "lucide-static/icons/banknote-arrow-up.svg?raw";
import Bell from "lucide-static/icons/bell.svg?raw";
import BookOpen from "lucide-static/icons/book-open.svg?raw";
import Calendar from "lucide-static/icons/calendar-days.svg?raw";
import Chart from "lucide-static/icons/chart-no-axes-combined.svg?raw";
import ChevronRight from "lucide-static/icons/chevron-right.svg?raw";
import ChevronUp from "lucide-static/icons/chevron-up.svg?raw";
import Code from "lucide-static/icons/code.svg?raw";
import CreditCard from "lucide-static/icons/credit-card.svg?raw";
import Ellipsis from "lucide-static/icons/ellipsis.svg?raw";
import FileText from "lucide-static/icons/file-text.svg?raw";
import Globe from "lucide-static/icons/globe.svg?raw";
import HelpCircle from "lucide-static/icons/circle-help.svg?raw";
import Info from "lucide-static/icons/info.svg?raw";
import Landmark from "lucide-static/icons/landmark.svg?raw";
import Lock from "lucide-static/icons/lock.svg?raw";
import MessageCircle from "lucide-static/icons/message-circle.svg?raw";
import Moon from "lucide-static/icons/moon.svg?raw";
import Paintbrush from "lucide-static/icons/paintbrush.svg?raw";
import PieChart from "lucide-static/icons/chart-pie.svg?raw";
import Plus from "lucide-static/icons/plus.svg?raw";
import Refresh from "lucide-static/icons/refresh-cw.svg?raw";
import Search from "lucide-static/icons/search.svg?raw";
import Shield from "lucide-static/icons/shield.svg?raw";
import Settings from "lucide-static/icons/settings.svg?raw";
import Target from "lucide-static/icons/target.svg?raw";
import TrendingUp from "lucide-static/icons/trending-up.svg?raw";
import User from "lucide-static/icons/user.svg?raw";
import Wallet from "lucide-static/icons/wallet.svg?raw";
import Sun from "lucide-static/icons/sun.svg?raw";
const title = "All of the shadcn/ui magic, none of the React";
const description = "A component library built with Tailwind CSS that works with any web stack.";
const styleUrls = {
vega: vegaUrl,
nova: novaUrl,
maia: maiaUrl,
lyra: lyraUrl,
mira: miraUrl,
luma: lumaUrl,
sera: seraUrl,
rhea: rheaUrl,
};
const logoSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true"><rect width="256" height="256" fill="none"></rect><line x1="208" y1="128" x2="128" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line><line x1="192" y1="40" x2="40" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line></svg>`;
const qrRows = [
"111111100101101111111",
"100000101001001000001",
"101110101111101011101",
"101110100100001011101",
"101110101010101011101",
"100000100111001000001",
"111111101010101111111",
"000000001101000000000",
"101011111001111010110",
"010100001110010101001",
"111010111011101111010",
"001101000101000010101",
"110111101111010111011",
"000000001001010001010",
"111111101101111101001",
"100000100010001001111",
"101110101011101110100",
"101110100110100010011",
"101110101000111101110",
"100000101101000011001",
"111111101011101101111",
];
const holdings = [
["Vanguard", "450 Shares", "$1,842.10", [58, 64, 60, 100]],
["S&P 500 VOO", "112 Shares", "$928.40", [56, 66, 100, 68]],
["Apple AAPL", "85 Shares", "$340.00", [50, 58, 100, 75]],
["Realty Income", "320 Shares", "$1,139.50", [67, 72, 78, 100]],
];
const sidebarSections = [
{
label: "Overview",
className: "xl:col-start-1 xl:row-start-2",
active: "Analytics",
items: [
["Analytics", Chart],
["Transactions", BanknoteArrowUp],
["Investments", TrendingUp],
["Accounts", Landmark],
["Spending", PieChart],
],
},
{
label: "Planning",
className: "xl:col-start-1 xl:row-start-1",
items: [
["Documents", FileText],
["Budget", Wallet],
["Reports", Chart],
["Goals", Target],
["Calendar", Calendar],
],
},
{
label: "Support",
className: "xl:col-start-2 xl:row-start-1",
items: [
["Help Center", HelpCircle],
["Docs", BookOpen],
["Contact Us", MessageCircle],
["Status", Activity],
["Community", Globe],
],
},
{
label: "Account",
className: "xl:col-start-2 xl:row-start-2",
active: "Billing",
items: [
["Profile", User],
["Billing", CreditCard],
["Notifications", Bell],
["Security", Shield],
["Appearance", Paintbrush],
],
},
];
---
<!doctype html>
<html lang="en">
<head>
<script is:inline>
(() => {
const media = matchMedia("(prefers-color-scheme: dark)");
const getStoredTheme = () => {
try {
return localStorage.getItem("themeMode");
} catch (_) {
return null;
}
};
const syncTheme = () => {
const stored = getStoredTheme();
document.documentElement.classList.toggle("dark", stored ? stored === "dark" : media.matches);
};
try {
syncTheme();
} catch (_) {}
media.addEventListener("change", () => {
if (!getStoredTheme()) syncTheme();
});
document.addEventListener("basecoat:theme", (event) => {
const mode = event.detail?.mode;
if (mode === "system") {
try {
localStorage.removeItem("themeMode");
} catch (_) {}
syncTheme();
return;
}
const dark = mode === "dark" || (!mode && !document.documentElement.classList.contains("dark"));
document.documentElement.classList.toggle("dark", dark);
try {
localStorage.setItem("themeMode", dark ? "dark" : "light");
} catch (_) {}
document.dispatchEvent(new CustomEvent("basecoat:themechange", { detail: { mode: dark ? "dark" : "light" } }));
});
})();
</script>
<style id="docs-style-gate" is:inline>
html[data-style-pending] body {
visibility: hidden;
}
</style>
<script is:inline define:vars={{ styleUrls }}>
(() => {
const styleGateId = "docs-style-gate";
const styleLinkId = "docs-style-variant";
const getStoredStyle = () => {
try {
return localStorage.getItem("styleVariant") || "vega";
} catch (_) {
return "vega";
}
};
const getStyleHref = (style) => {
const href = styleUrls[style];
return href?.startsWith("/docs/src/assets/styles/") ? `${href}?direct` : href;
};
const nextStyle = Object.prototype.hasOwnProperty.call(styleUrls, getStoredStyle()) ? getStoredStyle() : "vega";
const href = getStyleHref(nextStyle);
document.documentElement.dataset.styleVariant = nextStyle;
if (!href) return;
document.documentElement.setAttribute("data-style-pending", "");
document.write(
`<link id="${styleLinkId}" rel="stylesheet" href="${href}" onload="document.documentElement.removeAttribute('data-style-pending');document.getElementById('${styleGateId}')?.remove()" onerror="document.documentElement.removeAttribute('data-style-pending');document.getElementById('${styleGateId}')?.remove()">`,
);
})();
</script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title} | Basecoat</title>
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href={favicon.src} />
<meta property="og:type" content="website" />
<meta property="og:title" content={`${title} | Basecoat`} />
<meta property="og:description" content={description} />
<meta property="og:image" content={socialImage.src} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={`${title} | Basecoat`} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={socialImage.src} />
<script src={basecoatJsUrl} defer></script>
<script src={dropdownMenuJsUrl} defer></script>
</head>
<body class="min-h-dvh overflow-x-clip bg-background text-foreground antialiased">
<header class="bg-background sticky inset-x-0 top-0 isolate z-30 flex shrink-0 items-center gap-2">
<div class="flex h-14 w-full items-center justify-between gap-2 px-4">
<div class="flex min-w-0 items-center gap-1">
<a href="/" class="btn" data-variant="ghost" aria-label="Basecoat home">
<span class="[&_svg]:size-4" set:html={logoSvg} />
<span class="truncate font-semibold">Basecoat</span>
</a>
<nav class="hidden items-center gap-1 sm:flex" aria-label="Primary">
<a class="btn" data-variant="ghost" href="/introduction">Docs</a>
<a class="btn" data-variant="ghost" href="https://github.com/hunvreus/basecoat" target="_blank" rel="noopener">GitHub</a>
<a class="btn" data-variant="ghost" href="https://basecoatui.com/chat" target="_blank" rel="noopener">Discord</a>
</nav>
</div>
<div class="ml-auto flex min-w-0 flex-1 items-center justify-end gap-2">
<select id="style-variant-select" class="select hidden h-8 w-28 shrink-0 text-sm leading-none sm:block" aria-label="Style pack">
<option value="vega">Vega</option>
<option value="nova">Nova</option>
<option value="maia">Maia</option>
<option value="lyra">Lyra</option>
<option value="mira">Mira</option>
<option value="luma">Luma</option>
<option value="sera">Sera</option>
<option value="rhea">Rhea</option>
</select>
<button
type="button"
aria-label="Toggle dark mode"
data-tooltip="Toggle dark mode"
data-side="bottom"
data-align="end"
onclick="document.dispatchEvent(new CustomEvent('basecoat:theme'))"
class="btn hidden sm:inline-flex"
data-size="icon"
data-variant="ghost"
>
<span class="hidden dark:block"><Fragment set:html={Sun} /></span>
<span class="block dark:hidden"><Fragment set:html={Moon} /></span>
</button>
</div>
</div>
</header>
<main class="flex flex-1 flex-col">
<section class="md:**:[.container]:pb-8 lg:**:[.container]:pb-12">
<div class="container-wrapper">
<div class="container mx-auto flex flex-col items-center gap-2 px-6 py-8 text-center md:py-16 lg:py-20 xl:gap-4">
<h1 class="leading-tighter text-3xl font-semibold tracking-tight text-balance text-primary lg:leading-[1.1] lg:font-semibold xl:text-5xl xl:tracking-tighter max-w-4xl">
{title}
</h1>
<p class="max-w-4xl text-base text-balance text-foreground sm:text-lg">
{description}
</p>
<div class="flex w-full items-center justify-center gap-2 pt-2">
<a class="btn" href="/installation">Get started</a>
<a class="btn" data-variant="outline" href="https://github.com/hunvreus/basecoat" target="_blank" rel="noopener">Source code</a>
</div>
</div>
</div>
</section>
<section class="flex-1 p-0">
<div class="w-full overflow-hidden">
<div class="theme-neutral relative flex h-[980px] max-h-[calc(100svh-14rem)] min-h-[680px] w-full max-w-none flex-col gap-(--gap) overflow-hidden bg-muted p-12 pb-0! [--gap:--spacing(8)] min-[1900px]:h-[1120px] min-[1900px]:p-12 min-[1900px]:[--gap:--spacing(10)]! lg:p-6 lg:[--gap:--spacing(6)] dark:bg-background">
<div class="relative z-10 mx-auto grid w-full grid-cols-1 gap-(--gap) [&_.card]:w-full md:max-w-3xl md:grid-cols-2 lg:max-w-none lg:grid-cols-3 xl:max-w-[1600px] min-[1400px]:grid-cols-4! 2xl:max-w-[1900px] min-[1900px]:grid-cols-5!">
<div class="flex min-w-0 flex-col gap-(--gap)">
<article class="card relative">
<section class="flex flex-col gap-6">
<div class="flex flex-wrap gap-2">
<button class="btn" type="button">Button <span data-icon="inline-end"><Fragment set:html={ArrowRight} /></span></button>
<button class="btn" data-variant="secondary" type="button">Secondary</button>
<button class="btn" data-variant="outline" type="button">Outline</button>
</div>
<div class="field">
<div class="input-group">
<input placeholder="Name" />
<span data-align="end"><Fragment set:html={Search} /></span>
</div>
</div>
<div class="field flex-1">
<textarea class="textarea min-h-20 resize-none" placeholder="Message"></textarea>
</div>
<div class="flex items-center gap-2">
<div class="flex gap-2">
<span class="badge">Badge</span>
<span class="badge" data-variant="secondary">Secondary</span>
<span class="badge hidden min-[1900px]:flex" data-variant="outline">Outline</span>
</div>
<div class="ml-auto flex w-fit gap-3" role="radiogroup" aria-label="Fruit preference">
<input class="input" type="radio" name="fruit" aria-label="Apple" checked />
<input class="input" type="radio" name="fruit" aria-label="Banana" />
</div>
<div class="flex gap-3">
<input class="input" type="checkbox" aria-label="Enable email alerts" checked />
<input class="input hidden min-[1900px]:flex" type="checkbox" aria-label="Enable push alerts" />
</div>
<input class="input flex min-[1900px]:hidden" type="checkbox" role="switch" aria-label="Enable compact notifications" checked />
</div>
<div class="flex flex-wrap items-center gap-3">
<button class="btn" data-variant="outline" type="button" onclick="document.getElementById('landing-alert-dialog').showModal()">Alert Dialog</button>
<div class="button-group ml-auto max-w-full">
<button class="btn min-w-0" data-variant="outline" type="button"><span class="truncate">Button Group</span></button>
<div id="landing-quick-actions" class="dropdown-menu">
<button id="landing-quick-actions-trigger" class="btn" data-size="icon" data-variant="outline" type="button" aria-label="Open quick actions" aria-haspopup="menu" aria-controls="landing-quick-actions-menu" aria-expanded="false"><Fragment set:html={ChevronUp} /></button>
<div id="landing-quick-actions-popover" data-popover data-side="top" data-align="end" aria-hidden="true" class="w-40">
<div role="menu" id="landing-quick-actions-menu" aria-labelledby="landing-quick-actions-trigger">
<div role="group" aria-labelledby="landing-quick-actions-heading">
<div role="heading" id="landing-quick-actions-heading">Quick Actions</div>
<div role="menuitem">Mute Conversation</div>
<div role="menuitem">Mark as Read</div>
<div role="menuitem">Block User</div>
</div>
<hr role="separator" />
<div role="group">
<div role="menuitem" data-variant="destructive">Delete Conversation</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<dialog id="landing-alert-dialog" class="dialog" aria-labelledby="landing-alert-dialog-title" aria-describedby="landing-alert-dialog-description">
<div>
<header>
<h2 id="landing-alert-dialog-title">Allow accessory to connect?</h2>
<p id="landing-alert-dialog-description">Do you want to allow the USB accessory to connect to this device and your data?</p>
</header>
<footer>
<button class="btn" data-variant="outline" type="button" onclick="document.getElementById('landing-alert-dialog').close()">Don't allow</button>
<button class="btn" type="button" onclick="document.getElementById('landing-alert-dialog').close()">Allow</button>
</footer>
</div>
</dialog>
</article>
<div class="grid w-full grid-cols-2 gap-4 xl:gap-6">
{sidebarSections.map((section) => (
<article data-size="sm" class:list={["card", section.className]}>
<section>
<h3 class="mb-2 text-xs font-medium text-muted-foreground">{section.label}</h3>
<ul class="grid gap-1">
{section.items.map(([item, icon]) => (
<li>
<a href="#" class:list={["btn w-full min-w-0 justify-start", section.active === item && "bg-muted font-medium"]} data-size="sm" data-variant="ghost">
<span class="shrink-0" set:html={icon} /> <span class="truncate">{item}</span>
</a>
</li>
))}
</ul>
</section>
</article>
))}
</div>
<article class="card relative">
<header>
<h2>Savings Targets</h2>
<p>Active milestones for 2024 across your portfolio. Monitor how close you are to each savings goal.</p>
</header>
<section class="grid gap-3">
{[
["Retirement", "$420,000", "65% achieved", "$273,000", "65%"],
["Real Estate", "$85,000", "32% achieved", "$27,200", "32%"],
].map(([label, amount, progress, value, width]) => (
<div class="landing-demo-surface bg-muted/50 p-4">
<p class="text-xs font-medium tracking-wider text-muted-foreground uppercase">{label}</p>
<p class="mt-3 text-3xl font-semibold tabular-nums">{amount}</p>
<div class="progress mt-3" role="progressbar" aria-label={`${label} savings progress`} aria-valuenow={parseInt(width, 10)} aria-valuemin="0" aria-valuemax="100"><span style={`width:${width}`}></span></div>
<div class="mt-3 flex justify-between text-sm"><span class="text-muted-foreground">{progress}</span><span class="font-medium tabular-nums">{value}</span></div>
</div>
))}
</section>
<footer><p class="mx-auto text-center text-sm text-muted-foreground">You have not met your targets for this year.</p></footer>
</article>
</div>
<div class="hidden min-w-0 flex-col gap-(--gap) lg:flex">
<article class="card relative">
<header><h2>Contribution History</h2><p>Last 6 months of activity</p></header>
<section>
<div class="flex h-[200px] w-full items-end gap-3" role="img" aria-label="Last 6 months of contribution activity">
{[
["Dec", 57],
["Jan", 79],
["Feb", 64],
["Mar", 93],
["Apr", 54],
["May", 100],
].map(([month, height]) => (
<div class="flex h-full flex-1 flex-col justify-end gap-2">
<div class="landing-demo-chart-bar min-h-2 bg-chart-2" style={`height:${height}%`}></div>
<span class="text-center text-xs text-muted-foreground">{month}</span>
</div>
))}
</div>
</section>
<section>
<div class="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
<div class="landing-demo-surface bg-muted/50 p-4"><p class="text-xs font-medium tracking-wider text-muted-foreground uppercase">Upcoming</p><p class="text-base font-semibold">May 2024</p><p class="text-sm text-muted-foreground">Scheduled</p></div>
<div class="landing-demo-surface hidden bg-muted/50 p-4 xl:block"><p class="text-xs font-medium tracking-wider text-muted-foreground uppercase">Savings Plan</p><p class="text-base font-semibold">Accelerated</p><p class="text-sm text-muted-foreground">Recurring</p></div>
</div>
</section>
<footer><button class="btn w-full" type="button">View Full Report</button></footer>
</article>
<article class="card relative">
<header>
<p class="text-sm text-muted-foreground">Claimable Balance</p>
<h2 class="text-4xl tabular-nums">$1,211.29</h2>
<span class="badge" data-variant="outline"><span class="size-2 rounded-full bg-yellow-500"></span> Pending Setup</span>
</header>
<section class="flex flex-1 flex-col justify-end">
<article class="item landing-demo-surface" data-variant="muted">
<section class="gap-3">
<div class="flex justify-between"><span class="text-muted-foreground">Net Royalties</span><span class="font-medium tabular-nums">$1,248.75</span></div>
<div class="mt-3 flex justify-between"><span class="text-muted-foreground">Processing Fee</span><span class="font-medium tabular-nums">-$37.46</span></div>
<hr class="my-3 border-border" />
<div class="flex justify-between"><span class="text-muted-foreground">Total Ready to Claim</span><span class="font-semibold tabular-nums">$1,211.29 USD</span></div>
</section>
</article>
</section>
<footer><p>Once your bank is connected, balances over $10.00 are automatically eligible for monthly distribution on the 15th of each month.</p></footer>
</article>
<article class="card relative hidden lg:flex">
<header>
<h2>Q2 Dividend Income</h2>
<p>Quarterly dividend payouts across your portfolio holdings.</p>
</header>
<section>
<div class="grid gap-2">
{holdings.map(([name, shares, _amount, data]) => (
<div class="landing-demo-surface flex items-center gap-3 bg-muted/50 p-3">
<div class="mr-auto"><p class="text-sm font-medium">{name}</p><p class="text-sm text-muted-foreground">{shares}</p></div>
<div class="hidden h-8 w-24 items-end gap-1 md:flex">{data.map((height) => <span class="landing-demo-chart-bar min-h-1 flex-1 bg-chart-2" style={`height:${height}%`}></span>)}</div>
</div>
))}
</div>
</section>
</article>
</div>
<div class="hidden min-w-0 flex-col gap-(--gap) min-[1900px]:flex">
<article class="card relative">
<header><h2>Set a new milestone</h2><p>Define your financial target and we'll help you pace your savings.</p></header>
<section class="grid gap-4">
<div class="field"><label for="goal-name">Goal Name</label><input id="goal-name" class="input" placeholder="e.g. New Car, Home Downpayment" /></div>
<div class="grid grid-cols-2 gap-3">
<div class="field"><label for="target-amount">Target Amount</label><input id="target-amount" class="input" value="$15,000" /></div>
<div class="field"><label for="target-date">Target Date</label><input id="target-date" class="input" value="Dec 2025" /></div>
</div>
</section>
<footer class="flex-col gap-2"><button class="btn w-full" type="button">Create Goal</button><button class="btn w-full" data-variant="outline" type="button">Cancel</button></footer>
</article>
<article class="card relative">
<header><h2>Payout Threshold</h2><p>Set the minimum balance required before a payout is triggered.</p></header>
<section class="grid gap-4">
<div class="field"><label for="preferred-currency">Preferred Currency</label><select id="preferred-currency" class="select"><option>USD — United States Dollar</option></select></div>
<div class="field"><div class="flex items-baseline justify-between"><label id="min-payout-label">Minimum Payout Amount</label><span class="text-2xl font-semibold tabular-nums">$2500.00</span></div><div class="progress" role="progressbar" aria-labelledby="min-payout-label" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100" aria-valuetext="$2,500 of $10,000"><span style="width:25%"></span></div><div class="flex justify-between text-sm text-muted-foreground"><span>$50 (MIN)</span><span>$10,000 (MAX)</span></div></div>
<div class="field"><label for="payout-notes">Notes</label><textarea id="payout-notes" class="textarea min-h-[100px]" placeholder="Add any notes for this payout configuration..."></textarea></div>
</section>
<footer><button class="btn w-full" type="button">Save Threshold</button></footer>
</article>
<article class="card relative">
<header><h2>Account Access</h2><p>Update your credentials or re-authenticate.</p></header>
<section class="grid gap-4">
<div class="field"><label for="email-address">Email Address</label><input id="email-address" class="input" type="email" placeholder="artist@studio.inc" /></div>
<div class="field"><div class="flex items-center justify-between"><label for="current-password">Current Password</label><a href="#" class="text-xs font-medium tracking-wider text-muted-foreground uppercase hover:text-foreground">Forgot?</a></div><input id="current-password" class="input" type="password" placeholder="••••••••••••••••••••••••" /></div>
</section>
<footer class="flex-col gap-4"><button class="btn w-full" type="button"><Fragment set:html={Lock} /> Update Security</button><a href="#" class="item landing-demo-surface" data-variant="muted"><figure class="text-destructive"><Fragment set:html={Info} /></figure><section><h3>Danger Zone</h3><p class="line-clamp-1">Archive account and remove catalog</p></section><aside class="text-muted-foreground [&_svg]:size-4"><Fragment set:html={ChevronRight} /></aside></a></footer>
</article>
</div>
<div class="hidden min-w-0 flex-col gap-(--gap) md:flex">
<article class="card relative">
<section class="flex justify-center pt-6">
<div class="landing-demo-surface border border-border bg-white p-4">
<svg viewBox="0 0 21 21" class="size-40 text-black" role="img" aria-label="Connect device QR code" shape-rendering="crispEdges">
<rect width="21" height="21" fill="white" />
{qrRows.map((row, y) => [...row].map((cell, x) => cell === "1" && <rect x={x} y={y} width="1" height="1" />))}
</svg>
</div>
</section>
<header class="text-center"><h2>Scan to connect your mobile device</h2><p class="text-balance">Open the Ledger mobile app and scan this code to link your device.</p></header>
</article>
<article class="card relative">
<header><h2>Transfer Funds</h2><p>Move money between your connected accounts.</p></header>
<section class="grid gap-4">
<div class="field w-full"><label for="transfer-amount">Amount to Transfer</label><div class="input-group w-full"><span data-align="start">$</span><input id="transfer-amount" class="w-full" value="1,200.00" /></div></div>
<div class="field w-full"><label for="from-account">From Account</label><select id="from-account" class="select w-full"><option>Main Checking (··8402) — $12,450.00</option></select></div>
<div class="field w-full"><label for="to-account">To Account</label><select id="to-account" class="select w-full"><option>High Yield Savings (··1192) — $42,100.00</option></select></div>
<article class="item landing-demo-surface" data-variant="muted">
<section class="gap-3">
<div class="flex justify-between"><span class="text-muted-foreground">Estimated arrival</span><span class="font-medium">Today, Apr 14</span></div><hr class="my-3 border-border" />
<div class="flex justify-between"><span class="text-muted-foreground">Transaction fee</span><span class="font-medium tabular-nums">$0.00</span></div><hr class="my-3 border-border" />
<div class="flex justify-between"><span class="font-medium">Total amount</span><span class="font-semibold tabular-nums">$1,200.00</span></div>
</section>
</article>
</section>
<footer><button class="btn w-full" type="button">Confirm Transfer</button></footer>
</article>
<article class="card">
<header class="flex flex-col gap-3">
<nav aria-label="Breadcrumb">
<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 aria-hidden="true" class="text-muted-foreground">
<span class="[&_svg]:size-3.5" set:html={ChevronRight} />
</li>
<li class="inline-flex items-center gap-1.5">
<div id="landing-payments-breadcrumb" class="dropdown-menu">
<button type="button" id="landing-payments-breadcrumb-trigger" aria-haspopup="menu" aria-controls="landing-payments-breadcrumb-menu" aria-expanded="false" class="flex size-9 h-4 w-4 cursor-pointer items-center justify-center hover:text-foreground [&_svg]:size-4" aria-label="Account options"><Fragment set:html={Ellipsis} /></button>
<div id="landing-payments-breadcrumb-popover" data-popover aria-hidden="true">
<div role="menu" id="landing-payments-breadcrumb-menu" aria-labelledby="landing-payments-breadcrumb-trigger">
<nav role="menu">
<button type="button" role="menuitem">Profile</button>
<button type="button" role="menuitem">Statements</button>
<button type="button" role="menuitem">Documents</button>
</nav>
</div>
</div>
</div>
</li>
<li aria-hidden="true" class="text-muted-foreground">
<span class="[&_svg]:size-3.5" set:html={ChevronRight} />
</li>
<li class="inline-flex items-center gap-1.5">
<span class="font-normal text-foreground">Payments</span>
</li>
</ol>
</nav>
</header>
<section>
<div class="grid gap-2">
{[
["Change transfer limit", "Adjust how much you can send from your balance."],
["Scheduled transfers", "Set up a transfer to send at a later date."],
["Recurring card payments", "Manage your repeated card transactions."],
].map(([name, desc], index) => (
<a href="#" class="item landing-demo-surface" data-variant="muted">
<figure><Fragment set:html={index === 0 ? Settings : index === 1 ? Calendar : Refresh} /></figure>
<section><h3>{name}</h3><p>{desc}</p></section>
<aside class="text-muted-foreground [&_svg]:size-4"><Fragment set:html={ChevronRight} /></aside>
</a>
))}
</div>
</section>
</article>
</div>
<div class="hidden min-w-0 flex-col gap-(--gap) min-[1400px]:flex">
<article class="card hidden min-[1400px]:flex">
<section>
<section class="empty p-4">
<header>
<figure><Fragment set:html={Plus} /></figure>
<h2>Distribute Track</h2>
<p>Upload your first master to start reaching listeners on Spotify, Apple Music, and more.</p>
</header>
<footer><button class="btn" type="button">Create Release</button></footer>
</section>
</section>
</article>
<article class="card relative w-full pb-0">
<header>
<h2>Analytics</h2>
<p>418.2K Visitors <span class="badge">+10%</span></p>
<a class="btn absolute end-4 top-4" data-size="sm" data-variant="outline" href="/components/chart">View Analytics</a>
</header>
<svg viewBox="0 0 100 86" preserveAspectRatio="none" class="aspect-[1/0.35] w-full text-chart-1" role="img" aria-label="Visitor trend"><path d="M0 52L18 40L36 46L54 70L72 50L100 49V86H0Z" fill="currentColor" opacity="0.28" /><path d="M0 52L18 40L36 46L54 70L72 50L100 49" fill="none" stroke="currentColor" stroke-width="1.5" vector-effect="non-scaling-stroke" /></svg>
</article>
<article class="card hidden min-[1400px]:flex">
<header><h2>Notifications</h2><p>Choose which email and push alerts you want to receive.</p></header>
<section class="grid gap-4">
{[
["transactions", "Transaction alerts", "Deposits, withdrawals, and transfers.", true],
["security", "Security alerts", "Login attempts and account changes.", true],
["goals", "Goal milestones", "Updates at 25%, 50%, 75%, and 100%.", false],
["market", "Market updates", "Daily portfolio summary and price alerts.", false],
].map(([id, label, desc, checked]) => (
<label class="field" data-orientation="horizontal">
<input class="input" type="checkbox" checked={checked} />
<section><span class="font-medium">{label}</span><p>{desc}</p></section>
</label>
))}
</section>
<footer><button class="btn w-full" type="button">Save Preferences</button></footer>
</article>
<article class="card hidden min-[1400px]:flex">
<header><h2>Power Usage</h2><p>Whole Home</p></header>
<section class="flex flex-col gap-4">
<div class="flex h-[140px] w-full items-end gap-2" role="img" aria-label="Power usage by hour">
{[32, 74, 82, 63, 89, 76, 100, 84].map((height, index) => (
<div class="flex h-full flex-1 flex-col justify-end gap-1.5"><div class="landing-demo-chart-bar min-h-2 bg-chart-2" style={`height:${height}%`}></div><span class="text-center text-xs text-muted-foreground">{["6a", "8a", "10a", "12p", "2p", "4p", "6p", "8p"][index]}</span></div>
))}
</div>
<hr class="border-border" />
<div class="grid grid-cols-2 gap-4">
<div><span class="text-sm text-muted-foreground">Currently Using</span><p class="text-lg font-semibold tabular-nums">3.4 kW</p></div>
<div><span class="text-sm text-muted-foreground">Solar Gen</span><p class="text-lg font-semibold tabular-nums">+1.2 kW</p></div>
</div>
</section>
</article>
</div>
</div>
<div class="absolute inset-x-0 top-0 z-1 h-120 bg-linear-to-b from-background via-muted to-transparent dark:hidden"></div>
<div class="absolute inset-x-0 bottom-0 z-20 h-48 bg-linear-to-t from-background via-muted/80 to-transparent lg:h-80 xl:h-64 dark:via-background/80"></div>
</div>
</div>
</section>
</main>
<script is:inline define:vars={{ styleUrls }}>
(() => {
const styleSelect = document.getElementById("style-variant-select");
const styleLinkId = "docs-style-variant";
const pendingStyleLinkId = "docs-style-variant-pending";
const styleGateId = "docs-style-gate";
const getStyleLink = () => document.getElementById(styleLinkId);
const clearStylePending = () => {
document.documentElement.removeAttribute("data-style-pending");
document.getElementById(styleGateId)?.remove();
};
const applyStyle = (style) => {
const nextStyle = Object.prototype.hasOwnProperty.call(styleUrls, style) ? style : "vega";
const href = styleUrls[nextStyle]?.startsWith("/docs/src/assets/styles/")
? `${styleUrls[nextStyle]}?direct`
: styleUrls[nextStyle];
const link = getStyleLink();
if (!href) {
link?.remove();
document.getElementById(pendingStyleLinkId)?.remove();
clearStylePending();
return;
}
const sameHref = link?.getAttribute("href") === href;
if (sameHref || link?.sheet) {
clearStylePending();
}
if (sameHref) return;
document.getElementById(pendingStyleLinkId)?.remove();
const pendingLink = document.createElement("link");
pendingLink.id = pendingStyleLinkId;
pendingLink.rel = "stylesheet";
pendingLink.addEventListener(
"load",
() => {
link?.remove();
pendingLink.id = styleLinkId;
document.documentElement.dataset.styleVariant = nextStyle;
clearStylePending();
document.dispatchEvent(new CustomEvent("basecoat:stylechange", { detail: { style: nextStyle } }));
},
{ once: true },
);
pendingLink.addEventListener(
"error",
() => {
pendingLink.remove();
clearStylePending();
},
{ once: true },
);
pendingLink.href = href;
document.head.appendChild(pendingLink);
};
const getStoredStyle = () => {
try {
return localStorage.getItem("styleVariant") || "vega";
} catch (_) {
return "vega";
}
};
const initialStyle = getStoredStyle();
if (styleSelect) styleSelect.value = initialStyle;
applyStyle(initialStyle);
styleSelect?.addEventListener("change", () => {
const nextStyle = styleSelect.value || "vega";
applyStyle(nextStyle);
try {
localStorage.setItem("styleVariant", nextStyle);
} catch (_) {}
});
})();
</script>
</body>
</html>