Adding empty, field and item, working on input group

This commit is contained in:
Ronan Berder
2025-11-02 11:56:40 +08:00
parent 15473865b4
commit ab44d6d48a
29 changed files with 2154 additions and 299 deletions
+18
View File
@@ -38,6 +38,24 @@ Basecoat is a vanilla JavaScript/HTML/CSS port of shadcn/ui components. It reuse
- Filenames for assets: kebab-case (e.g., `dropdown-menu.js`, `basecoat.cdn.css`).
- JS naming: `camelCase` for vars/functions; `PascalCase` for DOM classes and exported component names when applicable.
## Design Principles
- **Minimal code**: Prefer native HTML elements and browser behavior over JavaScript abstractions
- **Accessibility-first**: Semantic HTML, proper ARIA attributes, keyboard navigation
- **CSS patterns**:
- Use `border` for lines/separators (not `background` on `<hr>`)
- Use `:is()` and `:has()` for compact selectors
- Use `isolation: isolate` to contain z-index stacking contexts
- Use `data-*` attributes for styling states, `aria-*` for accessibility semantics
- **Component patterns**:
- Dialog variants use wrapper classes (e.g., `.command-dialog` wraps `.command`)
- JavaScript components register with `window.basecoat.register()`
- Emit custom events for user actions (e.g., `command:select`)
- Filter disabled options (`aria-disabled="true"`) from all interactions
- **When NOT to create a component**:
- If it's just composition of existing elements (users can do it with standard HTML/Tailwind)
- If behavior is fully native (e.g., fieldset, legend)
## Common tasks
- Update a component's CSS/JS: Edit `src/css/basecoat.css` and/or `src/js/*.js`.
+1 -1
View File
@@ -27,7 +27,7 @@
@apply font-medium underline underline-offset-4;
}
code:not(pre > code) {
@apply relative rounded-md bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm;
@apply relative rounded-md bg-muted/50 px-[0.3rem] py-[0.2rem] font-mono text-sm;
}
blockquote {
@apply border-l-4 border-border pl-4;
+137 -1
View File
@@ -5,4 +5,140 @@
@import "./themes/claude.css";
@import "./themes/doom-64.css";
@import "./themes/supabase.css";
@import "./themes/supabase.css";
/* @theme inline {
--breakpoint-3xl: 1600px;
--breakpoint-4xl: 2000px;
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--color-surface: var(--surface);
--color-surface-foreground: var(--surface-foreground);
--color-code: var(--code);
--color-code-foreground: var(--code-foreground);
--color-code-highlight: var(--code-highlight);
--color-code-number: var(--code-number);
--color-selection: var(--selection);
--color-selection-foreground: var(--selection-foreground);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--surface: oklch(0.98 0 0);
--surface-foreground: var(--foreground);
--code: var(--surface);
--code-foreground: var(--surface-foreground);
--code-highlight: oklch(0.96 0 0);
--code-number: oklch(0.56 0 0);
--selection: oklch(0.145 0 0);
--selection-foreground: oklch(1 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.269 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.371 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.439 0 0);
--surface: oklch(0.2 0 0);
--surface-foreground: oklch(0.708 0 0);
--code: var(--surface);
--code-foreground: var(--surface-foreground);
--code-highlight: oklch(0.27 0 0);
--code-number: oklch(0.72 0 0);
--selection: oklch(0.922 0 0);
--selection-foreground: oklch(0.205 0 0);
} */
-1
View File
@@ -99,7 +99,6 @@
<script>
document.addEventListener('htmx:historyRestore', () => {
if (window.basecoat) window.basecoat.initAll();
if (window.Alpine) Alpine.initTree(document.body);
});
</script>
{% if head %}{{ head | safe }}{% endif %}
+1 -1
View File
@@ -1,6 +1,6 @@
{% macro code_block(code, language="html", class="my-4") %}
<div class="relative {{ class }}">
<pre class="grid text-sm max-h-[650px] overflow-y-auto rounded-xl scrollbar"><code class="language-{{ language }} !bg-muted !p-3.5">{{- code | escape -}}</code></pre>
<pre class="grid text-sm max-h-[650px] overflow-y-auto rounded-xl scrollbar"><code class="language-{{ language }} !bg-muted/40 !p-3.5">{{- code | escape -}}</code></pre>
<button
onclick="
+17 -5
View File
@@ -61,6 +61,14 @@
"hx-swap": "outerHTML"
} %}
{% set new_link_attrs = {
"hx-boost": "true",
"hx-select": "#content",
"hx-target": "#content",
"hx-swap": "outerHTML",
"data-new-link": "true"
} %}
{% set icon_home %}{% lucide "house" %}{% endset %}
{% set icon_info %}{% lucide "info" %}{% endset %}
{% set icon_square_terminal %}{% lucide "square-terminal" %}{% endset %}
@@ -86,16 +94,20 @@
{ label: "Badge", url: "/components/badge", attrs: link_attrs },
{ label: "Breadcrumb", url: "/components/breadcrumb", attrs: link_attrs },
{ label: "Button", url: "/components/button", attrs: link_attrs },
{ label: "Button Group", url: "/components/button-group", attrs: link_attrs },
{ label: "Button Group", url: "/components/button-group", attrs: new_link_attrs },
{ label: "Card", url: "/components/card", attrs: link_attrs },
{ label: "Checkbox", url: "/components/checkbox", attrs: link_attrs },
{ label: "Kbd", url: "/components/kbd", attrs: link_attrs },
{ label: "Command", url: "/components/command", attrs: link_attrs },
{ label: "Command", url: "/components/command", attrs: new_link_attrs },
{ label: "Combobox", url: "/components/combobox", attrs: link_attrs },
{ label: "Dialog", url: "/components/dialog", attrs: link_attrs },
{ label: "Dropdown Menu", url: "/components/dropdown-menu", attrs: link_attrs },
{ label: "Empty", url: "/components/empty", attrs: new_link_attrs },
{ label: "Field", url: "/components/field", attrs: new_link_attrs },
{ label: "Form", url: "/components/form", attrs: link_attrs },
{ label: "Input", url: "/components/input", attrs: link_attrs },
{ label: "Input Group", url: "/components/input-group", attrs: new_link_attrs },
{ label: "Item", url: "/components/item", attrs: new_link_attrs },
{ label: "Kbd", url: "/components/kbd", attrs: new_link_attrs },
{ label: "Label", url: "/components/label", attrs: link_attrs },
{ label: "Pagination", url: "/components/pagination", attrs: link_attrs },
{ label: "Popover", url: "/components/popover", attrs: link_attrs },
@@ -120,6 +132,6 @@
label="Sidebar navigation",
header=header,
footer=footer,
content_attrs={ "class": "scrollbar" },
content_attrs={ "class": "scrollbar [&_[data-new-link]::after]:content-['New'] [&_[data-new-link]::after]:ml-auto [&_[data-new-link]::after]:text-xs [&_[data-new-link]::after]:font-medium [&_[data-new-link]::after]:bg-sidebar-primary [&_[data-new-link]::after]:text-sidebar-primary-foreground [&_[data-new-link]::after]:px-2 [&_[data-new-link]::after]:py-0.5 [&_[data-new-link]::after]:rounded-md" },
menu=menu
) }}
) }}
+443 -28
View File
@@ -12,15 +12,21 @@
--color-amber-900: oklch(41.4% 0.112 45.904);
--color-amber-950: oklch(27.9% 0.077 45.635);
--color-green-50: oklch(98.2% 0.018 155.826);
--color-green-500: oklch(72.3% 0.219 149.579);
--color-green-600: oklch(62.7% 0.194 149.214);
--color-green-900: oklch(39.3% 0.095 152.535);
--color-green-950: oklch(26.6% 0.065 152.934);
--color-blue-50: oklch(97% 0.014 254.604);
--color-blue-300: oklch(80.9% 0.105 251.813);
--color-blue-500: oklch(62.3% 0.214 259.815);
--color-blue-600: oklch(54.6% 0.245 262.881);
--color-blue-700: oklch(48.8% 0.243 264.376);
--color-blue-800: oklch(42.4% 0.199 265.638);
--color-blue-900: oklch(37.9% 0.146 265.522);
--color-blue-950: oklch(28.2% 0.091 267.935);
--color-purple-500: oklch(62.7% 0.265 303.9);
--color-neutral-300: oklch(87% 0 0);
--color-neutral-800: oklch(26.9% 0 0);
--color-black: #000;
--color-white: #fff;
--spacing: 0.25rem;
@@ -251,6 +257,9 @@
.invisible {
visibility: hidden;
}
.visible {
visibility: visible;
}
.sr-only {
position: absolute;
width: 1px;
@@ -292,15 +301,33 @@
.top-22 {
top: calc(var(--spacing) * 22);
}
.right-1 {
right: calc(var(--spacing) * 1);
}
.right-2 {
right: calc(var(--spacing) * 2);
}
.right-2\.5 {
right: calc(var(--spacing) * 2.5);
}
.right-3 {
right: calc(var(--spacing) * 3);
}
.bottom-2 {
bottom: calc(var(--spacing) * 2);
}
.bottom-3 {
bottom: calc(var(--spacing) * 3);
}
.left-1\/2 {
left: calc(1/2 * 100%);
}
.left-2 {
left: calc(var(--spacing) * 2);
}
.left-3 {
left: calc(var(--spacing) * 3);
}
.isolate {
isolation: isolate;
}
@@ -349,6 +376,12 @@
.mr-auto {
margin-right: auto;
}
.mb-1 {
margin-bottom: calc(var(--spacing) * 1);
}
.mb-2 {
margin-bottom: calc(var(--spacing) * 2);
}
.mb-4 {
margin-bottom: calc(var(--spacing) * 4);
}
@@ -367,6 +400,18 @@
.ml-auto {
margin-left: auto;
}
.line-clamp-1 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.block {
display: block;
}
@@ -413,6 +458,14 @@
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
.size-5 {
width: calc(var(--spacing) * 5);
height: calc(var(--spacing) * 5);
}
.size-6 {
width: calc(var(--spacing) * 6);
height: calc(var(--spacing) * 6);
}
.size-7 {
width: calc(var(--spacing) * 7);
height: calc(var(--spacing) * 7);
@@ -433,6 +486,10 @@
width: calc(var(--spacing) * 12);
height: calc(var(--spacing) * 12);
}
.size-16 {
width: calc(var(--spacing) * 16);
height: calc(var(--spacing) * 16);
}
.h-4 {
height: calc(var(--spacing) * 4);
}
@@ -451,6 +508,9 @@
.h-14 {
height: calc(var(--spacing) * 14);
}
.h-px {
height: 1px;
}
.max-h-64 {
max-height: calc(var(--spacing) * 64);
}
@@ -466,6 +526,15 @@
.max-h-\[650px\] {
max-height: 650px;
}
.min-h-24 {
min-height: calc(var(--spacing) * 24);
}
.min-h-32 {
min-height: calc(var(--spacing) * 32);
}
.min-h-64 {
min-height: calc(var(--spacing) * 64);
}
.min-h-\[350px\] {
min-height: 350px;
}
@@ -475,6 +544,9 @@
.w-2\/3 {
width: calc(2/3 * 100%);
}
.w-3\/4 {
width: calc(3/4 * 100%);
}
.w-4 {
width: calc(var(--spacing) * 4);
}
@@ -544,6 +616,9 @@
.max-w-xl {
max-width: var(--container-xl);
}
.min-w-0 {
min-width: calc(var(--spacing) * 0);
}
.min-w-5 {
min-width: calc(var(--spacing) * 5);
}
@@ -559,6 +634,9 @@
.flex-1 {
flex: 1;
}
.flex-none {
flex: none;
}
.flex-shrink {
flex-shrink: 1;
}
@@ -649,6 +727,9 @@
.gap-2 {
gap: calc(var(--spacing) * 2);
}
.gap-2\.5 {
gap: calc(var(--spacing) * 2.5);
}
.gap-3 {
gap: calc(var(--spacing) * 3);
}
@@ -664,6 +745,9 @@
.gap-10 {
gap: calc(var(--spacing) * 10);
}
.gap-12 {
gap: calc(var(--spacing) * 12);
}
.space-y-1 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
@@ -727,11 +811,17 @@
.gap-y-10 {
row-gap: calc(var(--spacing) * 10);
}
.self-start {
align-self: flex-start;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.overflow-hidden {
overflow: hidden;
}
.overflow-x-auto {
overflow-x: auto;
}
@@ -775,9 +865,16 @@
border-bottom-style: var(--tw-border-style);
border-bottom-width: 1px;
}
.border-dashed {
--tw-border-style: dashed;
border-style: dashed;
}
.border-amber-50 {
border-color: var(--color-amber-50);
}
.border-border {
border-color: var(--color-border);
}
.border-border\/50 {
border-color: var(--color-border);
@supports (color: color-mix(in lab, red, red)) {
@@ -787,8 +884,14 @@
.border-muted {
border-color: var(--color-muted);
}
.\!bg-muted {
.border-transparent {
border-color: transparent;
}
.\!bg-muted\/40 {
background-color: var(--color-muted) !important;
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-muted) 40%, transparent) !important;
}
}
.bg-accent {
background-color: var(--color-accent);
@@ -799,18 +902,33 @@
.bg-background {
background-color: var(--color-background);
}
.bg-blue-500 {
background-color: var(--color-blue-500);
}
.bg-card {
background-color: var(--color-card);
}
.bg-foreground {
background-color: var(--color-foreground);
}
.bg-green-500 {
background-color: var(--color-green-500);
}
.bg-muted {
background-color: var(--color-muted);
}
.bg-muted\/50 {
background-color: var(--color-muted);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-muted) 50%, transparent);
}
}
.bg-primary {
background-color: var(--color-primary);
}
.bg-purple-500 {
background-color: var(--color-purple-500);
}
.bg-sidebar-primary {
background-color: var(--color-sidebar-primary);
}
@@ -838,6 +956,9 @@
.p-4 {
padding: calc(var(--spacing) * 4);
}
.p-6 {
padding: calc(var(--spacing) * 6);
}
.p-10 {
padding: calc(var(--spacing) * 10);
}
@@ -883,9 +1004,24 @@
.pr-2 {
padding-right: calc(var(--spacing) * 2);
}
.pr-10 {
padding-right: calc(var(--spacing) * 10);
}
.pr-16 {
padding-right: calc(var(--spacing) * 16);
}
.pr-20 {
padding-right: calc(var(--spacing) * 20);
}
.pr-32 {
padding-right: calc(var(--spacing) * 32);
}
.pb-4 {
padding-bottom: calc(var(--spacing) * 4);
}
.pb-10 {
padding-bottom: calc(var(--spacing) * 10);
}
.pb-12 {
padding-bottom: calc(var(--spacing) * 12);
}
@@ -895,6 +1031,18 @@
.pl-8 {
padding-left: calc(var(--spacing) * 8);
}
.pl-8\.5 {
padding-left: calc(var(--spacing) * 8.5);
}
.pl-10 {
padding-left: calc(var(--spacing) * 10);
}
.pl-12 {
padding-left: calc(var(--spacing) * 12);
}
.pl-20 {
padding-left: calc(var(--spacing) * 20);
}
.text-center {
text-align: center;
}
@@ -927,6 +1075,10 @@
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
}
.text-sm\/relaxed {
font-size: var(--text-sm);
line-height: var(--leading-relaxed);
}
.text-xs {
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
@@ -974,9 +1126,15 @@
--tw-tracking: var(--tracking-widest);
letter-spacing: var(--tracking-widest);
}
.text-balance {
text-wrap: balance;
}
.break-words {
overflow-wrap: break-word;
}
.whitespace-nowrap {
white-space: nowrap;
}
.whitespace-pre-wrap {
white-space: pre-wrap;
}
@@ -989,15 +1147,24 @@
.text-foreground {
color: var(--color-foreground);
}
.text-green-500 {
color: var(--color-green-500);
}
.text-muted-foreground {
color: var(--color-muted-foreground);
}
.text-neutral-800 {
color: var(--color-neutral-800);
}
.text-primary-foreground {
color: var(--color-primary-foreground);
}
.text-sidebar-primary-foreground {
color: var(--color-sidebar-primary-foreground);
}
.text-white {
color: var(--color-white);
}
.uppercase {
text-transform: uppercase;
}
@@ -1039,6 +1206,10 @@
--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-offset-background {
--tw-ring-offset-color: var(--color-background);
}
@@ -1046,6 +1217,10 @@
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
.grayscale {
--tw-grayscale: grayscale(100%);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
@@ -1069,6 +1244,10 @@
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.duration-100 {
--tw-duration: 100ms;
transition-duration: 100ms;
}
.duration-200 {
--tw-duration: 200ms;
transition-duration: 200ms;
@@ -1090,6 +1269,17 @@
rotate: 180deg;
}
}
.group-has-\[\[data-slot\=item-description\]\]\/item\:translate-y-0\.5 {
&:is(:where(.group\/item):has(*:is([data-slot=item-description])) *) {
--tw-translate-y: calc(var(--spacing) * 0.5);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
}
.group-has-\[\[data-slot\=item-description\]\]\/item\:self-start {
&:is(:where(.group\/item):has(*:is([data-slot=item-description])) *) {
align-self: flex-start;
}
}
.group-aria-checked\:visible {
&:is(:where(.group)[aria-checked="true"] *) {
visibility: visible;
@@ -1168,6 +1358,13 @@
}
}
}
.hover\:bg-accent {
&:hover {
@media (hover: hover) {
background-color: var(--color-accent);
}
}
}
.hover\:bg-accent\/50 {
&:hover {
@media (hover: hover) {
@@ -1264,6 +1461,16 @@
outline-style: none;
}
}
.disabled\:pointer-events-none {
&:disabled {
pointer-events: none;
}
}
.disabled\:opacity-50 {
&:disabled {
opacity: 50%;
}
}
.has-\[input\[type\=\'checkbox\'\]\:checked\]\:border-blue-600 {
&:has(*:is(input[type='checkbox']:checked)) {
border-color: var(--color-blue-600);
@@ -1284,6 +1491,24 @@
background-color: var(--color-green-50);
}
}
.has-\[\>svg\]\:px-2\.5 {
&:has(>svg) {
padding-inline: calc(var(--spacing) * 2.5);
}
}
.aria-invalid\:border-destructive {
&[aria-invalid="true"] {
border-color: var(--color-destructive);
}
}
.aria-invalid\:ring-destructive\/20 {
&[aria-invalid="true"] {
--tw-ring-color: var(--color-destructive);
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklab, var(--color-destructive) 20%, transparent);
}
}
}
.max-sm\:flex-col {
@media (width < 40rem) {
flex-direction: column;
@@ -1348,6 +1573,11 @@
padding: calc(var(--spacing) * 6);
}
}
.md\:p-12 {
@media (width >= 48rem) {
padding: calc(var(--spacing) * 12);
}
}
.md\:pl-8 {
@media (width >= 48rem) {
padding-left: calc(var(--spacing) * 8);
@@ -1415,6 +1645,11 @@
color: var(--color-amber-100);
}
}
.dark\:text-neutral-300 {
&:is(.dark *) {
color: var(--color-neutral-300);
}
}
.dark\:checked\:border-blue-700 {
&:is(.dark *) {
&:checked {
@@ -1506,6 +1741,64 @@
}
}
}
.dark\:aria-invalid\:ring-destructive\/40 {
&:is(.dark *) {
&[aria-invalid="true"] {
--tw-ring-color: var(--color-destructive);
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklab, var(--color-destructive) 40%, transparent);
}
}
}
}
.\[\&_\[data-new-link\]\:\:after\]\:ml-auto {
& [data-new-link]::after {
margin-left: auto;
}
}
.\[\&_\[data-new-link\]\:\:after\]\:rounded-md {
& [data-new-link]::after {
border-radius: var(--radius-md);
}
}
.\[\&_\[data-new-link\]\:\:after\]\:bg-sidebar-primary {
& [data-new-link]::after {
background-color: var(--color-sidebar-primary);
}
}
.\[\&_\[data-new-link\]\:\:after\]\:px-2 {
& [data-new-link]::after {
padding-inline: calc(var(--spacing) * 2);
}
}
.\[\&_\[data-new-link\]\:\:after\]\:py-0\.5 {
& [data-new-link]::after {
padding-block: calc(var(--spacing) * 0.5);
}
}
.\[\&_\[data-new-link\]\:\:after\]\:text-xs {
& [data-new-link]::after {
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
}
}
.\[\&_\[data-new-link\]\:\:after\]\:font-medium {
& [data-new-link]::after {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
}
.\[\&_\[data-new-link\]\:\:after\]\:text-sidebar-primary-foreground {
& [data-new-link]::after {
color: var(--color-sidebar-primary-foreground);
}
}
.\[\&_\[data-new-link\]\:\:after\]\:content-\[\'New\'\] {
& [data-new-link]::after {
--tw-content: 'New';
content: var(--tw-content);
}
}
.\[\&_a\]\:inline-block {
& a {
display: inline-block;
@@ -1615,11 +1908,45 @@
padding-top: calc(var(--spacing) * 2);
}
}
.\[\&_svg\]\:pointer-events-none {
& svg {
pointer-events: none;
}
}
.\[\&_svg\]\:size-4 {
& svg {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
}
.\[\&_svg\]\:size-5 {
& svg {
width: calc(var(--spacing) * 5);
height: calc(var(--spacing) * 5);
}
}
.\[\&_svg\]\:shrink-0 {
& svg {
flex-shrink: 0;
}
}
.\[\&_svg\]\:\!text-destructive {
& svg {
color: var(--color-destructive) !important;
}
}
.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 {
& svg:not([class*='size-']) {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
}
.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-6 {
& svg:not([class*='size-']) {
width: calc(var(--spacing) * 6);
height: calc(var(--spacing) * 6);
}
}
.\[\&_ul\]\:m-0 {
& ul {
margin: calc(var(--spacing) * 0);
@@ -1635,6 +1962,11 @@
padding-left: calc(var(--spacing) * 4);
}
}
.\[\&\+\[data-slot\=item-content\]\]\:flex-none {
&+[data-slot=item-content] {
flex: none;
}
}
.md\:\*\:\[\.card\]\:basis-1\/4 {
@media (width >= 48rem) {
:is(& > *) {
@@ -1644,6 +1976,40 @@
}
}
}
.\[a\]\:transition-colors {
&:is(a) {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
}
.\[a\]\:hover\:bg-accent\/50 {
&:is(a) {
&:hover {
@media (hover: hover) {
background-color: var(--color-accent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-accent) 50%, transparent);
}
}
}
}
}
.\[\&\>a\]\:underline {
&>a {
text-decoration-line: underline;
}
}
.\[\&\>a\]\:underline-offset-4 {
&>a {
text-underline-offset: 4px;
}
}
.\[\&\>a\:hover\]\:text-primary {
&>a:hover {
color: var(--color-primary);
}
}
.\[\&\>svg\]\:mb-3 {
&>svg {
margin-bottom: calc(var(--spacing) * 3);
@@ -1769,9 +2135,6 @@
border-radius: var(--scrollbar-radius);
}
}
[x-cloak] {
display: none !important;
}
}
@layer components {
.alert, .alert-destructive {
@@ -2220,7 +2583,9 @@
.button-group {
isolation: isolate;
display: inline-flex;
> *:hover, > *:focus-visible, > :is(.dropdown-menu, .popover) > button:hover, > :is(.dropdown-menu, .popover) > button:focus-visible {
width: fit-content;
align-items: stretch;
> *:focus-visible, > :is(.dropdown-menu, .popover) > button:focus-visible {
position: relative;
z-index: 10;
}
@@ -2236,9 +2601,10 @@
}
&:not([data-orientation='vertical']) {
> *:not(:first-child), > :is(.dropdown-menu, .popover):not(:first-child) > button {
margin-left: -1px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left-style: var(--tw-border-style);
border-left-width: 0px;
}
> *:not(:last-child), > :is(.dropdown-menu, .popover):not(:last-child) > button {
border-top-right-radius: 0;
@@ -2252,10 +2618,10 @@
width: auto;
}
> *:not(:first-child) {
margin-top: -1px;
margin-left: calc(var(--spacing) * 0);
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top-style: var(--tw-border-style);
border-top-width: 0px;
}
> *:not(:last-child) {
border-bottom-right-radius: 0;
@@ -2317,7 +2683,7 @@
}
}
@layer components {
.form input[type='checkbox']:not([role='switch']), .input[type='checkbox']:not([role='switch']) {
:is(.form, .field) input[type='checkbox']:not([role='switch']), .input[type='checkbox']:not([role='switch']) {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
flex-shrink: 0;
@@ -2439,7 +2805,7 @@
&::backdrop {
opacity: 100%;
}
> article {
> * {
--tw-scale-x: 100%;
--tw-scale-y: 100%;
--tw-scale-z: 100%;
@@ -2450,7 +2816,7 @@
&::backdrop {
opacity: 0%;
}
> article {
> * {
--tw-scale-x: 95%;
--tw-scale-y: 95%;
--tw-scale-z: 95%;
@@ -2469,7 +2835,7 @@
transition-duration: var(--tw-duration, var(--default-transition-duration));
transition-behavior: allow-discrete;
}
> article {
> * {
position: fixed;
top: 50%;
left: 50%;
@@ -2561,9 +2927,9 @@
> header input {
height: calc(var(--spacing) * 12);
}
[role='listbox'] {
[role='menu'] {
padding-inline: calc(var(--spacing) * 2);
[role='option'] {
[role='menuitem'] {
padding-inline: calc(var(--spacing) * 2);
padding-block: calc(var(--spacing) * 3);
& svg {
@@ -2621,12 +2987,12 @@
}
}
}
[role='listbox'] {
[role='menu'] {
max-height: 300px;
overflow-x: hidden;
overflow-y: auto;
padding: calc(var(--spacing) * 1);
[role='option'] {
[role='menuitem'] {
position: relative;
display: flex;
width: 100%;
@@ -2692,7 +3058,7 @@
font-weight: var(--font-weight-medium);
color: var(--color-muted-foreground);
}
[role='group']:not(:has([role='option']:not([aria-hidden='true']))) {
[role='group']:not(:has([role='menuitem']:not([aria-hidden='true']))) {
display: none;
}
[role='separator'] {
@@ -2722,7 +3088,7 @@
content: var(--tw-content);
}
}
&:not([data-command-initialized]) [role='option'] {
&:not([data-command-initialized]) [role='menuitem'] {
&:hover {
@media (hover: hover) {
background-color: var(--color-accent);
@@ -2752,7 +3118,7 @@
&::backdrop {
opacity: 100%;
}
> article {
> * {
--tw-scale-x: 100%;
--tw-scale-y: 100%;
--tw-scale-z: 100%;
@@ -2763,7 +3129,7 @@
&::backdrop {
opacity: 0%;
}
> article {
> * {
--tw-scale-x: 95%;
--tw-scale-y: 95%;
--tw-scale-z: 95%;
@@ -2782,7 +3148,7 @@
transition-duration: var(--tw-duration, var(--default-transition-duration));
transition-behavior: allow-discrete;
}
> article {
> * {
position: fixed;
top: 50%;
left: 50%;
@@ -3011,7 +3377,53 @@
}
}
@layer components {
.form input[type='text'], .form input[type='email'], .form input[type='password'], .form input[type='number'], .form input[type='file'], .form input[type='tel'], .form input[type='url'], .form input[type='search'], .form input[type='date'], .form input[type='datetime-local'], .form input[type='month'], .form input[type='week'], .form input[type='time'], .input[type='text'], .input[type='email'], .input[type='password'], .input[type='number'], .input[type='file'], .input[type='tel'], .input[type='url'], .input[type='search'], .input[type='date'], .input[type='datetime-local'], .input[type='month'], .input[type='week'], .input[type='time'] {
.fieldset {
display: flex;
flex-direction: column;
gap: calc(var(--spacing) * 6);
> legend {
margin-bottom: calc(var(--spacing) * 3);
font-size: var(--text-base);
line-height: var(--tw-leading, var(--text-base--line-height));
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
> .description {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
color: var(--color-muted-foreground);
}
}
.field {
display: flex;
width: 100%;
flex-direction: column;
gap: calc(var(--spacing) * 3);
&[data-invalid="true"] {
color: var(--color-destructive);
}
&>* {
width: 100%;
}
&>.sr-only {
width: auto;
}
.description {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
color: var(--color-muted-foreground);
}
.error {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
color: var(--color-destructive);
}
}
}
@layer components {
:is(.form, .field) input[type='text'], :is(.form, .field) input[type='email'], :is(.form, .field) input[type='password'], :is(.form, .field) input[type='number'], :is(.form, .field) input[type='file'], :is(.form, .field) input[type='tel'], :is(.form, .field) input[type='url'], :is(.form, .field) input[type='search'], :is(.form, .field) input[type='date'], :is(.form, .field) input[type='datetime-local'], :is(.form, .field) input[type='month'], :is(.form, .field) input[type='week'], :is(.form, .field) input[type='time'], .input[type='text'], .input[type='email'], .input[type='password'], .input[type='number'], .input[type='file'], .input[type='tel'], .input[type='url'], .input[type='search'], .input[type='date'], .input[type='datetime-local'], .input[type='month'], .input[type='week'], .input[type='time'] {
display: flex;
height: calc(var(--spacing) * 9);
width: 100%;
@@ -3151,7 +3563,7 @@
}
}
@layer components {
.form label, .label {
:is(.form, .field) label, .label {
display: flex;
align-items: center;
gap: calc(var(--spacing) * 2);
@@ -3275,7 +3687,7 @@
}
}
@layer components {
.form input[type='radio'], .input[type='radio'] {
:is(.form, .field) input[type='radio'], .input[type='radio'] {
position: relative;
aspect-ratio: 1 / 1;
width: calc(var(--spacing) * 4);
@@ -3354,7 +3766,7 @@
}
}
@layer components {
.form input[type='range'], .input[type='range'] {
:is(.form, .field) input[type='range'], .input[type='range'] {
display: flex;
appearance: none;
align-items: center;
@@ -3464,7 +3876,7 @@
}
}
@layer components {
.form select, select.select {
:is(.form, .field) select, select.select {
display: flex;
height: calc(var(--spacing) * 9);
width: fit-content;
@@ -4069,7 +4481,7 @@
}
}
@layer components {
.form input[type='checkbox'][role='switch'], .input[type='checkbox'][role='switch'] {
:is(.form, .field) input[type='checkbox'][role='switch'], .input[type='checkbox'][role='switch'] {
display: inline-flex;
height: 1.15rem;
width: calc(var(--spacing) * 8);
@@ -4369,7 +4781,7 @@
}
}
@layer components {
.form textarea, .textarea {
:is(.form, .field) textarea, .textarea {
display: flex;
field-sizing: content;
min-height: calc(var(--spacing) * 16);
@@ -4799,6 +5211,9 @@
position: relative;
border-radius: var(--radius-md);
background-color: var(--color-muted);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-muted) 50%, transparent);
}
padding-inline: 0.3rem;
padding-block: 0.2rem;
font-family: var(--font-mono);
+2 -2
View File
@@ -11,7 +11,7 @@ toc:
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated accordion component in Basecoat.</h2>
<h2>There is no dedicated Accordion component in Basecoat.</h2>
</div>
{% set code %}<section class="accordion">
@@ -72,4 +72,4 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<p class="prose">Basecoat animates <code>&lt;details&gt;</code> elements by default. The example above uses Alpine.js to handle the open/close state along with some Tailwind CSS classes.</p>
<p class="prose">Basecoat already animates <code>&lt;details&gt;</code> elements by default. The example add some Tailwind CSS utility classes for style and a bit of vanilla JavaScript to handle the open/close state.</p>
+11 -6
View File
@@ -37,12 +37,17 @@ toc:
<section class="prose">
<p>The component has the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;svg&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The icon.</dd>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dd>The title.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The description.</dd>
<dt><code class="highlight language-html">&lt;div class="alert"&gt;</code></dt>
<dd>Main container. Use <code>alert</code> for default styling or <code>alert-destructive</code> for error states.
<dl>
<dt><code class="highlight language-html">&lt;svg&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The icon.</dd>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dd>The title.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The description.</dd>
</dl>
</dd>
</dl>
</section>
+18 -3
View File
@@ -12,12 +12,27 @@ toc:
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated avatar component in Basecoat.</h2>
<h2>There is no dedicated Avatar component in Basecoat.</h2>
</div>
{% set code %}<img class="size-8 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">{% endset %}
{% set code %}
<div class="flex flex-row flex-wrap items-center gap-12">
<img class="size-8 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">
<img class="size-8 shrink-0 object-cover rounded-lg" alt="@shadcn" src="https://github.com/shadcn.png">
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-8 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
</div>
{% endset %}
{{ code_preview("avatar", code, "max-w-md") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<p class="prose">Avatars are fairly simple to implement with Tailwind. The example above shows how to implement one with Tailwind and an <code>&lt;img&gt;</code> element. More examples can be found in the <a href="/kitchen-sink#avatar">kitchen sink</a>.</p>
<p class="prose">To make an avatar, simply apply Tailwind utility classes directly to an <code>&lt;img&gt;</code> element:</p>
{% set code_simple %}
<img class="size-8 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">
{% endset %}
{{ code_block(code_simple | prettyHtml, "html") }}
+14 -3
View File
@@ -13,7 +13,7 @@ toc:
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated breadcrumb component in Basecoat.</h2>
<h2>There is no dedicated Breadcrumb component in Basecoat.</h2>
</div>
{% set code_html %}
@@ -54,7 +54,18 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>This example uses some simple Tailwind CSS styles along with the <a href="/components/dropdown-menu">Dropdown Menu</a> component (which may require JavaScript).</p>
<p>You can style an ordered list of links combined with chevron icons as show below. The example above demonstrates a more complex use case using the <a href="/components/dropdown-menu">Dropdown Menu</a> component.</p>
</section>
{{ code_block(code_html | prettyHtml, class="") }}
{% set code_simple %}
<ol class="text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5">
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Home</a>
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Components</a>
</li>
</ol>
{% endset %}
{{ code_block(code_simple | prettyHtml, "html") }}
+10 -2
View File
@@ -78,7 +78,15 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
{{ code_block(code_default | prettyHtml, "html") }}
<p class="prose">Simply wrap buttons you want to group together in a <code>&lt;div role="group" class="button-group"&gt;</code> element. You can also use the <code>data-orientation="vertical"</code> attribute to stack buttons vertically. You can also group buttons with <a href="#example-dropdown-menu">dropdown menus</a> or <a href="#example-popover">popovers</a> (provided the trigger is a button). Finally, you can use a <a href="#example-separator">separator</a> to visually divide buttons within a group (for secondary buttons for example).</p>
{% set code_simple %}
<div role="group" class="button-group">
<button class="btn-outline">Archive</button>
<button class="btn-outline">Report</button>
</div>
{% endset %}
{{ code_block(code_simple | prettyHtml, "html") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
@@ -216,7 +224,7 @@ toc:
{{ code_preview("button-group-dropdown-menu", code_dropdown_menu | prettyHtml) }}
<h3 id="example-nested"><a href="#example-nested">Popover</a></h3>
<h3 id="example-popover"><a href="#example-popover">Popover</a></h3>
{% set code_popover %}
<div role="group" class="button-group">
+161 -21
View File
@@ -5,6 +5,20 @@ description: Fast, composable command menu for quick navigation and actions.
toc:
- label: Usage
id: usage
children:
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1: Include the JavaScript files"
id: usage-html-js-1
- label: "Step 2: Add your command HTML"
id: usage-html-js-2
- label: HTML structure
id: usage-html-js-3
- label: JavaScript events
id: usage-html-js-4
- label: Jinja and Nunjucks
id: usage-macro
- label: Examples
id: examples
children:
@@ -19,15 +33,15 @@ toc:
{% macro command_items(suggestions_id="suggestions", settings_id="settings") %}
<div role="group" aria-labelledby="{{ suggestions_id }}">
<span role="heading" id="{{ suggestions_id }}">Suggestions</span>
<div role="option" data-value="calendar" onclick="this.closest('dialog').close()">
<div role="menuitem" data-value="calendar" onclick="console.log('Open calendar')">
{% lucide "calendar" %}
<span>Calendar</span>
</div>
<div role="option" data-value="search-emoji" onclick="this.closest('dialog').close()">
<div role="menuitem" data-value="search-emoji" onclick="console.log('Search emoji')">
{% lucide "smile" %}
<span>Search Emoji</span>
</div>
<div role="option" data-value="calculator" aria-disabled="true">
<div role="menuitem" data-value="calculator" aria-disabled="true">
{% lucide "calculator" %}
<span>Calculator</span>
</div>
@@ -35,17 +49,17 @@ toc:
<hr role="separator" />
<div role="group" aria-labelledby="{{ settings_id }}">
<span role="heading" id="{{ settings_id }}">Settings</span>
<div role="option" data-value="profile" onclick="this.closest('dialog').close()">
<div role="menuitem" data-value="profile" onclick="console.log('Open profile')">
{% lucide "user" %}
<span>Profile</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘P</kbd>
</div>
<div role="option" data-value="billing" onclick="this.closest('dialog').close()">
<div role="menuitem" data-value="billing" onclick="console.log('Open billing')">
{% lucide "credit-card" %}
<span>Billing</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘B</kbd>
</div>
<div role="option" data-value="settings" onclick="this.closest('dialog').close()">
<div role="menuitem" data-value="settings" onclick="console.log('Open settings')">
{% lucide "settings" %}
<span>Settings</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘S</kbd>
@@ -66,14 +80,152 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript files</a></h4>
<section class="prose">
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/command.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/command.js" target="_blank">
command.js
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your command HTML</a></h4>
<section class="prose">
<p>You can use the command menu standalone:</p>
</section>
{{ code_block(code_standalone | prettyHtml, "html") }}
<section class="prose">
<p>Wrap the command menu in a <code>&lt;dialog class="command-dialog"&gt;</code> to display in a dialog:</p>
</section>
{% set code_dialog %}
<dialog class="command-dialog" aria-label="Command menu">
<div class="command">
<!-- Regular command content: header, menu, etc. -->
</div>
</dialog>
{% endset %}
{{ code_block(code_dialog | prettyHtml, "html") }}
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;dialog class="command-dialog"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>For dialog variant, wraps a <code>&lt;div class="command"&gt;</code> to display as a modal. Uses native <code>&lt;dialog&gt;</code> behavior and requires <code>aria-label</code> or <code>aria-labelledby</code> for accessibility.
<dl>
<dt><code class="highlight language-html">&lt;div class="command"&gt;</code></dt>
<dd>Wraps around the entire command component. Can be used standalone or inside a dialog.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Contains the search input to filter command options.
<dl>
<dt><code class="highlight language-html">&lt;input type="text"&gt;</code></dt>
<dd>The search input. Should have:
<ul>
<li><code>role="combobox"</code>: indicates the input controls a menu with filtering</li>
<li><code>aria-expanded="true"</code>: the menu is always expanded</li>
<li><code>aria-controls="{ MENU_ID }"</code>: points to the menu's id</li>
</ul>
</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;div role="menu"&gt;</code></dt>
<dd>The menu containing the command items. Should have <code>id="{ MENU_ID }"</code> refered to by the input's <code>aria-controls</code> attribute.
<dl>
<dt><code class="highlight language-html">&lt;div role="menuitem"&gt;</code></dt>
<dd>Command item that triggers an action when clicked. In a command-dialog, clicking a menuitem automatically closes the dialog unless the <code>data-keep-command-open</code> attribute is present. Optional attributes:
<ul>
<li><code>data-label</code>: custom label for filtering (if different from text content)</li>
<li><code>data-keywords</code>: additional keywords for enhanced filtering</li>
<li><code>aria-disabled="true"</code>: marks item as disabled (excluded from keyboard navigation and filtering)</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Separator between groups/items. Hidden when filtering is active.</dd>
<dt><code class="highlight language-html">&lt;div role="group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Group of items, can have <code>aria-labelledby</code> attribute to link to a heading.</dd>
<dt><code class="highlight language-html">&lt;span role="heading"&gt;</code></dt>
<dd>Group heading, must have an <code>id</code> attribute if you use the <code>aria-labelledby</code> attribute on the group.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</section>
<h4 id="usage-html-js-4"><a href="#usage-html-js-4">JavaScript events</a></h4>
<section class="prose">
<dl>
<dt><code>basecoat:initialized</code></dt>
<dd>Once the component is fully initialized, it dispatches a custom (non-bubbling) <code>basecoat:initialized</code> event on itself.</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">command()</code> or <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">command_dialog()</code> Nunjucks or Jinja macros for this component.</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/command.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/command.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% call command() %}
<div role="menuitem" data-value="calendar">
<svg>...</svg>
<span>Calendar</span>
</div>
<div role="menuitem" data-value="settings">
<svg>...</svg>
<span>Settings</span>
</div>
{% endcall %}{% endraw %}{% endset %}
{{ code_block(raw_code, "jinja") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-dialog"><a href="#example-dialog">Dialog</a></h3>
<section class="prose">
<p>Press <kbd>⌘</kbd><kbd>J</kbd></p>
<p>You can use some simple JavaScript to open the command menu with a keyboard shortcut (in this case <kbd>⌘J</kbd>).</p>
</section>
{% set code_dialog %}
@@ -83,7 +235,7 @@ toc:
onclick="document.getElementById('demo-command-dialog').showModal()"
>
Open command menu
<kbd>⌘ J</kbd>
<kbd>⌘J</kbd>
</button>
{% call command_dialog(id="demo-command-dialog") %}
@@ -103,19 +255,7 @@ toc:
}
}
});
const cmdDialog = document.getElementById('demo-command-dialog');
cmdDialog?.addEventListener('command:select', (e) => {
console.log('Selected command:', e.detail.value);
cmdDialog.close();
});
</script>
{% endset %}
{{ code_preview("command-dialog", code_dialog | prettyHtml) }}
<section class="prose">
<p>To show the command menu in a dialog, use the <code>command_dialog</code> macro.</p>
</section>
{{ code_block(code_dialog | prettyHtml, "html") }}
{{ code_preview("command-dialog", code_dialog | prettyHtml) }}
+2 -2
View File
@@ -69,8 +69,8 @@ toc:
<li><code>onclick</code>: closes the dialog when the backdrop is clicked.</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;article&gt;</code></dt>
<dd>Wraps the content of the dialog. It includes an <code>onclick</code> attribute to prevent from closing the dialog if we have an <code>onclick</code> event on <code>&lt;dialog&gt;</code>.
<dt><code class="highlight language-html">&lt;div&gt;</code></dt>
<dd>Wraps the content of the dialog.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Contains the header for the dialog:
+209
View File
@@ -0,0 +1,209 @@
---
layout: layouts/page.njk
title: Empty
description: Display empty states with icons, titles, descriptions, and actions.
toc:
- label: Usage
id: usage
children:
- label: HTML structure
id: html-structure
- label: Examples
id: examples
children:
- label: Outline
id: example-outline
- label: Avatar
id: example-avatar
- label: Avatar Group
id: example-avatar-group
- label: Input Group
id: example-input-group
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated Empty component in Basecoat.</h2>
</div>
{% set code_default %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300">
<header class="flex max-w-sm flex-col items-center gap-2 text-center">
<div class="mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6">
{% lucide "folder-code" %}
</div>
<h3 class="text-lg font-medium tracking-tight">No Projects Yet</h3>
<p class="text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4">
You haven't created any projects yet. Get started by creating your first project.
</p>
</header>
<section class="flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance">
<div class="flex gap-2">
<button class="btn">Create Project</button>
<button class="btn-outline">Import Project</button>
</div>
</section>
<a href="#" class="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive underline-offset-4 hover:underline h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5 text-muted-foreground">
Learn More
{% lucide "arrow-up-right" %}
</a>
</div>
{% endset %}
{{ code_preview("empty-default", code_default) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>The example simply uses Tailwind utility classes to create an empty state layout.</p>
</section>
{{ code_block(code_default) }}
<h3 id="html-structure"><a href="#html-structure">HTML structure</a></h3>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;div&gt;</code></dt>
<dd>Main container with centered layout utilities:
<ul>
<li><code>flex flex-col items-center justify-center</code>: centers content</li>
<li><code>gap-6</code>: spacing between sections</li>
<li><code>p-6 md:p-12</code>: responsive padding</li>
<li><code>text-center text-balance</code>: centered text with better wrapping</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Header section containing the icon/avatar, title, and description:
<ul>
<li><code>max-w-sm</code>: constrains width for readability</li>
<li><code>gap-2</code> or <code>gap-3</code>: spacing between elements</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;div&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Media container for icon or avatar:
<ul>
<li>For icons: <code>bg-muted text-foreground size-10 rounded-lg</code></li>
<li>For avatars: <code>size-12 rounded-full grayscale</code></li>
<li>For avatar groups: <code>flex -space-x-2</code> with ring utilities</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;h3&gt;</code></dt>
<dd>Title with <code>text-lg font-medium tracking-tight</code></dd>
<dt><code class="highlight language-html">&lt;p&gt;</code></dt>
<dd>Description with <code>text-muted-foreground text-sm/relaxed</code></dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Content area for actions (buttons, inputs, etc.):
<ul>
<li><code>max-w-sm</code> or <code>max-w-md</code>: constrains width</li>
<li><code>flex flex-col items-center gap-3</code>: vertical layout with spacing</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;a&gt;</code> or other elements <span class="badge-secondary">Optional</span></dt>
<dd>Additional links or actions outside the main content section</dd>
</dl>
</dd>
</dl>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-outline"><a href="#example-outline">Outline</a></h3>
{% set code_outline %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300 border">
<header class="flex max-w-sm flex-col items-center gap-3 text-center">
<div class="mb-2 bg-muted text-foreground flex size-12 shrink-0 items-center justify-center rounded-lg [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6">
{% lucide "cloud" %}
</div>
<h3 class="text-lg font-semibold tracking-tight">Cloud Storage Empty</h3>
<p class="text-muted-foreground text-sm/relaxed">
Upload files to your cloud storage to access them anywhere.
</p>
</header>
<section class="flex w-full max-w-sm min-w-0 flex-col items-center gap-3">
<button class="btn-sm-outline">Upload Files</button>
</section>
</div>
{% endset %}
{{ code_preview("empty-outline", code_outline) }}
<h3 id="example-avatar"><a href="#example-avatar">Avatar</a></h3>
{% set code_avatar %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300">
<header class="flex max-w-sm flex-col items-center gap-3 text-center">
<div class="mb-2 flex items-center justify-center">
<img alt="@shadcn" src="https://github.com/shadcn.png" class="grayscale size-12 shrink-0 object-cover rounded-full">
</div>
<h3 class="text-lg font-semibold tracking-tight">User Offline</h3>
<p class="text-muted-foreground text-sm/relaxed">
This user is currently offline. You can leave a message to notify them or try again later.
</p>
</header>
<section class="flex w-full max-w-sm min-w-0 flex-col items-center gap-3">
<button class="btn-sm">Leave Message</button>
</section>
</div>
{% endset %}
{{ code_preview("empty-avatar", code_avatar) }}
<h3 id="example-avatar-group"><a href="#example-avatar-group">Avatar Group</a></h3>
{% set code_avatar_group %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300">
<header class="flex max-w-sm flex-col items-center gap-3 text-center">
<div class="mb-2 flex items-center justify-center">
<div class="flex -space-x-2 [&amp;_img]:ring-background [&amp;_img]:ring-2 [&amp;_img]:grayscale [&amp;_img]:size-12 [&amp;_img]:shrink-0 [&amp;_img]:object-cover [&amp;_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
</div>
<h3 class="text-lg font-semibold tracking-tight">No Team Members</h3>
<p class="text-muted-foreground text-sm/relaxed">
Invite your team to collaborate on this project.
</p>
</header>
<section class="flex w-full max-w-sm min-w-0 flex-col items-center gap-3">
<button class="btn-sm inline-flex items-center gap-2">
{% lucide "plus" %}
Invite Members
</button>
</section>
</div>
{% endset %}
{{ code_preview("empty-avatar-group", code_avatar_group) }}
<h3 id="example-input-group"><a href="#example-input-group">Input Group</a></h3>
{% set code_input_group %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300">
<header class="flex max-w-sm flex-col items-center gap-3 text-center">
<h3 class="text-lg font-semibold tracking-tight">404 - Not Found</h3>
<p class="text-muted-foreground text-sm/relaxed">
The page you're looking for doesn't exist. Try searching for what you need below.
</p>
</header>
<section class="flex w-full max-w-md min-w-0 flex-col items-center gap-3">
<div class="relative w-3/4">
<input type="text" class="input pl-8.5 pr-10" placeholder="Try searching for pages...">
{% lucide "search" , { "class": "absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground pointer-events-none shrink-0 size-4" } %}
<kbd class="kbd absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">/</kbd>
</div>
<p class="text-muted-foreground text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary">
Need help? <a href="#">Contact support</a>
</p>
</section>
</div>
{% endset %}
{{ code_preview("empty-input-group", code_input_group) }}
+316
View File
@@ -0,0 +1,316 @@
---
layout: layouts/page.njk
title: Field
description: Combine labels, controls, and help text to compose accessible form fields.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Input
id: example-input
- label: Textarea
id: example-textarea
- label: Select
id: example-select
- label: Fieldset
id: example-fieldset
- label: Checkbox
id: example-checkbox
- label: Radio Group
id: example-radio
- label: Switch
id: example-switch
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code_demo %}
<form class="w-full max-w-md space-y-6">
<fieldset class="fieldset" aria-describedby="payment-desc">
<legend>Payment Method</legend>
<p id="payment-desc" class="description">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" class="description">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">
<option value="">MM</option>
<option value="01">01</option>
<option value="02">02</option>
<option value="03">03</option>
<option value="04">04</option>
<option value="05">05</option>
<option value="06">06</option>
<option value="07">07</option>
<option value="08">08</option>
<option value="09">09</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
</select>
</div>
<div role="group" class="field">
<label for="exp-year">Year</label>
<select id="exp-year" class="select">
<option value="">YYYY</option>
<option value="2024">2024</option>
<option value="2025">2025</option>
<option value="2026">2026</option>
<option value="2027">2027</option>
<option value="2028">2028</option>
<option value="2029">2029</option>
</select>
</div>
<div role="group" class="field">
<label for="cvv">CVV</label>
<input id="cvv" type="text" placeholder="123" required>
</div>
</div>
</fieldset>
<hr role="separator" class="my-6 border-border">
<fieldset class="fieldset" aria-describedby="billing-desc">
<legend>Billing Address</legend>
<p id="billing-desc" class="description">The billing address associated with your payment method</p>
<div class="flex items-center gap-3">
<input type="checkbox" id="same-as-shipping" checked>
<label for="same-as-shipping" class="font-normal">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-2">
<button type="submit" class="btn">Submit</button>
<button type="button" class="btn-outline">Cancel</button>
</div>
</form>
{% endset %}
{{ code_preview("field-demo", code_demo) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Field provides semantic HTML patterns for accessible forms:</p>
<ul>
<li><code>&lt;fieldset&gt;</code> and <code>&lt;legend&gt;</code> for grouping related fields</li>
<li><code>role="group"</code> with <code>.field</code> class for individual fields</li>
<li><code>.description</code> class for helper text</li>
<li><code>.error</code> class with <code>role="alert"</code> for error messages</li>
<li><code>aria-describedby</code> to associate descriptions and errors with inputs</li>
</ul>
</section>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-input"><a href="#example-input">Input</a></h3>
{% set code_input %}
<div class="grid gap-6 w-full max-w-md">
<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" class="description">Choose a unique username for your account</p>
</div>
<div role="group" class="field">
<label for="password">Password</label>
<input id="password" type="password" placeholder="••••••••" aria-invalid="true" aria-describedby="password-desc password-error">
<p id="password-desc" class="description">Must be at least 8 characters</p>
<p id="password-error" class="error" role="alert">Password is too short</p>
</div>
</div>
{% endset %}
{{ code_preview("field-input", code_input) }}
<h3 id="example-textarea"><a href="#example-textarea">Textarea</a></h3>
{% set code_textarea %}
<div role="group" class="field w-full max-w-md">
<label for="bio">Bio</label>
<textarea id="bio" rows="4" placeholder="Tell us about yourself" aria-describedby="bio-desc"></textarea>
<p id="bio-desc" class="description">Brief description for your profile (max 200 characters)</p>
</div>
{% endset %}
{{ code_preview("field-textarea", code_textarea) }}
<h3 id="example-select"><a href="#example-select">Select</a></h3>
{% set code_select %}
<div role="group" class="field w-full max-w-md">
<label for="country">Country</label>
<select id="country" class="select" aria-describedby="country-desc">
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
<option value="au">Australia</option>
</select>
<p id="country-desc" class="description">Choose your country of residence</p>
</div>
{% endset %}
{{ code_preview("field-select", code_select) }}
<h3 id="example-fieldset"><a href="#example-fieldset">Fieldset</a></h3>
<section class="prose">
<p>Group related fields with <code>&lt;fieldset class="fieldset"&gt;</code> and <code>&lt;legend&gt;</code>.</p>
</section>
{% set code_fieldset %}
<form class="w-full max-w-md">
<fieldset class="fieldset" aria-describedby="profile-desc">
<legend>Profile</legend>
<p id="profile-desc" class="description">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" class="description">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" class="error" role="alert">Choose another username</p>
</div>
<div class="flex items-center gap-3">
<input type="checkbox" id="newsletter">
<label for="newsletter" class="font-normal">Subscribe to the newsletter</label>
</div>
</fieldset>
<button type="submit" class="btn mt-4">Save changes</button>
</form>
{% endset %}
{{ code_preview("field-fieldset", code_fieldset) }}
<h3 id="example-checkbox"><a href="#example-checkbox">Checkbox</a></h3>
{% set code_checkbox %}
<fieldset class="fieldset w-full max-w-md" aria-describedby="tasks-desc">
<legend>Tasks</legend>
<p id="tasks-desc" class="description">Select the tasks you want to receive updates for</p>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3">
<input type="checkbox" id="push-notifications" checked>
<label for="push-notifications" class="font-normal">Push notifications</label>
</div>
<div class="flex items-center gap-3">
<input type="checkbox" id="email-notifications" checked>
<label for="email-notifications" class="font-normal">Email notifications</label>
</div>
<div class="flex items-center gap-3">
<input type="checkbox" id="sms-notifications">
<label for="sms-notifications" class="font-normal">SMS notifications</label>
</div>
</div>
</fieldset>
{% endset %}
{{ code_preview("field-checkbox", code_checkbox) }}
<h3 id="example-radio"><a href="#example-radio">Radio Group</a></h3>
{% set code_radio %}
<fieldset class="fieldset w-full max-w-md" aria-describedby="notify-desc">
<legend>Notify me about</legend>
<p id="notify-desc" class="description">Choose which notifications you want to receive</p>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3">
<input type="radio" id="notify-all" name="notifications" checked>
<label for="notify-all" class="font-normal">All new messages</label>
</div>
<div class="flex items-center gap-3">
<input type="radio" id="notify-direct" name="notifications">
<label for="notify-direct" class="font-normal">Direct messages and mentions</label>
</div>
<div class="flex items-center gap-3">
<input type="radio" id="notify-none" name="notifications">
<label for="notify-none" class="font-normal">Nothing</label>
</div>
</div>
</fieldset>
{% endset %}
{{ code_preview("field-radio", code_radio) }}
<h3 id="example-switch"><a href="#example-switch">Switch</a></h3>
<section class="prose">
<p>Use horizontal layout for switches by wrapping in a flex container.</p>
</section>
{% set code_switch %}
<fieldset class="fieldset w-full max-w-md">
<legend>Notifications</legend>
<p class="description">Configure how you receive notifications</p>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<div>
<label for="marketing" class="font-medium">Marketing emails</label>
<p class="description text-sm">Receive emails about new products and features</p>
</div>
<input type="checkbox" id="marketing" class="toggle" checked>
</div>
<div class="flex items-center justify-between">
<div>
<label for="security" class="font-medium">Security emails</label>
<p class="description text-sm">Receive emails about account security</p>
</div>
<input type="checkbox" id="security" class="toggle" checked>
</div>
</div>
</fieldset>
{% endset %}
{{ code_preview("field-switch", code_switch) }}
<h2 id="accessibility"><a href="#accessibility">Accessibility</a></h2>
<section class="prose">
<ul>
<li>Always use <code>&lt;label&gt;</code> elements with <code>for</code> attribute matching input <code>id</code></li>
<li>Associate helper text and errors using <code>aria-describedby</code> on the input</li>
<li>Use <code>aria-invalid="true"</code> on inputs with errors</li>
<li>Use <code>role="alert"</code> on error messages for screen reader announcements</li>
<li>Group related fields with <code>&lt;fieldset&gt;</code> and <code>&lt;legend&gt;</code></li>
<li>Use <code>role="group"</code> for individual field wrappers</li>
</ul>
</section>
+247
View File
@@ -0,0 +1,247 @@
---
layout: layouts/page.njk
title: Input Group
description: Display additional information or actions alongside inputs using simple HTML patterns.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Icon Prefix
id: example-icon-prefix
- label: Icon Suffix
id: example-icon-suffix
- label: Text Prefix/Suffix
id: example-text
- label: Button Suffix
id: example-button
- label: Multiple Elements
id: example-multiple
- label: Textarea
id: example-textarea
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code_default %}
<div class="relative">
<input type="text" class="input pl-10" placeholder="Search...">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground">
{% lucide "search" %}
</div>
</div>
{% endset %}
{{ code_preview("input-group-default", code_default) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Input groups are achieved through composition using relative positioning and Tailwind utilities. No special component needed - just wrap your input with a positioned container and add your icons, text, or buttons.</p>
<p><strong>Key patterns:</strong></p>
<ul>
<li>Use <code>relative</code> wrapper around input</li>
<li>Position content with <code>absolute</code> + <code>top-1/2 -translate-y-1/2</code></li>
<li>Add padding to input (<code>pl-*</code> or <code>pr-*</code>) to make room for content</li>
<li>Use <code>pointer-events-none</code> for decorative icons</li>
</ul>
</section>
{{ code_block(code_default) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-icon-prefix"><a href="#example-icon-prefix">Icon Prefix</a></h3>
<section class="prose">
<p>Add an icon at the start of the input field.</p>
</section>
{% set code_icon_prefix %}
<div class="flex flex-col gap-4 w-full max-w-sm">
<div class="relative">
<input type="text" class="input pl-10" placeholder="Search...">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground">
{% lucide "search" %}
</div>
</div>
<div class="relative">
<input type="email" class="input pl-10" placeholder="Email">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground">
{% lucide "mail" %}
</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-icon-prefix", code_icon_prefix) }}
<h3 id="example-icon-suffix"><a href="#example-icon-suffix">Icon Suffix</a></h3>
<section class="prose">
<p>Add an icon at the end of the input field.</p>
</section>
{% set code_icon_suffix %}
<div class="flex flex-col gap-4 w-full max-w-sm">
<div class="relative">
<input type="text" class="input pr-10" placeholder="Username">
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-green-500">
{% lucide "check" %}
</div>
</div>
<div class="relative">
<input type="text" class="input pr-16" placeholder="Card number">
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1 pointer-events-none text-muted-foreground">
{% lucide "credit-card" %}
{% lucide "info" %}
</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-icon-suffix", code_icon_suffix) }}
<h3 id="example-text"><a href="#example-text">Text Prefix/Suffix</a></h3>
<section class="prose">
<p>Display text before or after the input value.</p>
</section>
{% set code_text %}
<div class="flex flex-col gap-4 w-full max-w-sm">
<div class="relative">
<input type="text" class="input pl-12 pr-16" placeholder="0.00">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">
$
</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">
USD
</div>
</div>
<div class="relative">
<input type="text" class="input pl-20" placeholder="example.com">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">
https://
</div>
</div>
<div class="relative">
<input type="text" class="input pr-32" placeholder="username">
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">
@company.com
</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-text", code_text) }}
<h3 id="example-button"><a href="#example-button">Button Suffix</a></h3>
<section class="prose">
<p>Add interactive buttons inside the input.</p>
</section>
{% set code_button %}
<div class="flex flex-col gap-4 w-full max-w-sm">
<div class="relative">
<input type="text" class="input pr-10" placeholder="Search...">
<button type="button" class="absolute right-1 top-1/2 -translate-y-1/2 btn-icon-sm-ghost">
{% lucide "search" %}
</button>
</div>
<div class="relative">
<input type="password" class="input pr-10" placeholder="Password">
<button type="button" class="absolute right-1 top-1/2 -translate-y-1/2 btn-icon-sm-ghost">
{% lucide "eye" %}
</button>
</div>
<div class="relative">
<input type="text" class="input pr-10" value="https://example.com/page">
<button type="button" class="absolute right-1 top-1/2 -translate-y-1/2 btn-icon-sm-ghost">
{% lucide "copy" %}
</button>
</div>
</div>
{% endset %}
{{ code_preview("input-group-button", code_button) }}
<h3 id="example-multiple"><a href="#example-multiple">Multiple Elements</a></h3>
<section class="prose">
<p>Combine multiple icons, text, and buttons.</p>
</section>
{% set code_multiple %}
<div class="flex flex-col gap-4 w-full max-w-sm">
<div class="relative">
<input type="text" class="input pl-10 pr-20" placeholder="Search...">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground">
{% lucide "search" %}
</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1.5">
<span class="text-xs text-muted-foreground pointer-events-none">⌘K</span>
</div>
</div>
<div class="relative">
<input type="text" class="input pl-20 pr-10" placeholder="example.com">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">
https://
</div>
<button type="button" class="absolute right-1 top-1/2 -translate-y-1/2 btn-icon-sm-ghost">
{% lucide "info" %}
</button>
</div>
</div>
{% endset %}
{{ code_preview("input-group-multiple", code_multiple) }}
<h3 id="example-textarea"><a href="#example-textarea">Textarea</a></h3>
<section class="prose">
<p>Add buttons to textareas for actions like submit.</p>
</section>
{% set code_textarea %}
<div class="flex flex-col gap-4 w-full max-w-sm">
<div class="relative">
<textarea class="textarea pr-10 min-h-24" placeholder="Enter your message..."></textarea>
<button type="button" class="absolute right-2 bottom-2 btn-icon-sm">
{% lucide "send" %}
</button>
</div>
<div class="relative">
<textarea class="textarea pb-10 min-h-32" placeholder="Type something..."></textarea>
<div class="absolute bottom-3 left-3 right-3 flex items-center justify-between gap-2">
<span class="text-xs text-muted-foreground">120 characters left</span>
<button type="button" class="btn-sm">Submit</button>
</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-textarea", code_textarea) }}
<h2 id="accessibility"><a href="#accessibility">Accessibility</a></h2>
<section class="prose">
<ul>
<li>Use <code>pointer-events-none</code> for decorative icons that shouldn't be interactive</li>
<li>Ensure buttons have proper <code>aria-label</code> or visible text</li>
<li>Maintain sufficient padding so text doesn't overlap with icons/buttons</li>
<li>Use semantic button types (<code>type="button"</code> vs <code>type="submit"</code>)</li>
</ul>
</section>
+311
View File
@@ -0,0 +1,311 @@
---
layout: layouts/page.njk
title: Item
description: A versatile component that you can use to display any content.
toc:
- label: Examples
id: examples
children:
- label: Variants
id: example-variants
- label: Size
id: example-size
- label: Icon
id: example-icon
- label: Avatar
id: example-avatar
- label: Image
id: example-image
- label: Group
id: example-group
- label: Header
id: example-header
- label: Link
id: example-link
---
{% from "macros/code_block.njk" import code_block %}
{% from "macros/code_preview.njk" import code_preview %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated Item component in Basecoat.</h2>
</div>
{% set code_basic %}
<div class="flex flex-col gap-6">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Basic Item</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">A simple item with title and description.</p>
</div>
<button class="btn-sm-outline">Action</button>
</article>
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border py-3 px-4 gap-2.5">
<div class="flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5 bg-transparent [&_svg]:size-5">
{% lucide "badge-check" %}
</div>
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Your profile has been verified.</h3>
</div>
<div class="flex items-center gap-2 [&_svg]:size-4">
{% lucide "chevron-right" %}
</div>
</a>
</div>
{% endset %}
{{ code_preview("item-basic", code_basic | prettyHtml, class="w-full max-w-md") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-variants"><a href="#example-variants">Variants</a></h3>
{% set code_variants %}
<div class="flex flex-col gap-6">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Default Variant</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Standard styling with subtle background and borders.</p>
</div>
<button class="btn-sm-outline">Open</button>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Outline Variant</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Outlined style with clear borders and transparent background.</p>
</div>
<button class="btn-sm-outline">Open</button>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent bg-muted/50 p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Muted Variant</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Subdued appearance with muted colors for secondary content.</p>
</div>
<button class="btn-sm-outline">Open</button>
</article>
</div>
{% endset %}
{{ code_preview("item-variants", code_variants) }}
<h3 id="example-size"><a href="#example-size">Size</a></h3>
<section class="prose">
<p>Use different padding and gap sizes for compact or spacious layouts.</p>
</section>
{{ code_preview("item-size", code_basic) }}
<h3 id="example-icon"><a href="#example-icon">Icon</a></h3>
{% set code_icon %}
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex shrink-0 items-center justify-center gap-2 self-start [&_svg]:pointer-events-none size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4">
{% lucide "shield-alert" %}
</div>
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Security Alert</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">New login detected from unknown device.</p>
</div>
<button class="btn-sm-outline">Review</button>
</article>
{% endset %}
{{ code_preview("item-icon", code_icon, class="max-w-lg") }}
<h3 id="example-avatar"><a href="#example-avatar">Avatar</a></h3>
{% set code_avatar %}
<div class="flex flex-col gap-6">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img src="https://github.com/hunvreus.png" alt="hunvreus" class="size-10 rounded-full object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">hunvreus</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Last seen 5 months ago</p>
</div>
<button class="btn-icon-outline rounded-full">
{% lucide "plus" %}
</button>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-8 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full self-start">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">No Team Members</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Invite your team to collaborate on this project.</p>
</div>
<button class="btn-outline">
Invite
</button>
</article>
</div>
{% endset %}
{{ code_preview("item-avatar", code_avatar, class="w-full max-w-lg") }}
<h3 id="example-image"><a href="#example-image">Image</a></h3>
{% set code_image %}
<div class="flex flex-col gap-6 w-full max-w-md">
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img src="https://avatar.vercel.sh/example_1" alt="Midnight City Lights" class="grayscale size-10 rounded-sm object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="w-fit items-center gap-2 text-sm leading-snug font-medium line-clamp-1">Midnight City Lights - <span class="text-muted-foreground">Electric Nights</span></h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Neon Dreams</p>
</div>
<div class="flex flex-col gap-1 flex-none text-center">
<p data-slot="item-description" class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">3:45</p>
</div>
</a>
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img src="https://avatar.vercel.sh/example_2" alt="Midnight City Lights" class="grayscale size-10 rounded-sm object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="w-fit items-center gap-2 text-sm leading-snug font-medium line-clamp-1">Coffee Shop Conversations - <span class="text-muted-foreground">Urban Stories</span></h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">The Morning Brew</p>
</div>
<div class="flex flex-col gap-1 flex-none text-center">
<p data-slot="item-description" class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">4:05</p>
</div>
</a>
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img src="https://avatar.vercel.sh/example_3" alt="Midnight City Lights" class="grayscale size-10 rounded-sm object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="w-fit items-center gap-2 text-sm leading-snug font-medium line-clamp-1">Digital Rain - <span class="text-muted-foreground">Binary Beats</span></h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Cyber Symphony</p>
</div>
<div class="flex flex-col gap-1 flex-none text-center">
<p data-slot="item-description" class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">3:30</p>
</div>
</a>
</div>
{% endset %}
{{ code_preview("item-image", code_image) }}
<h3 id="example-group"><a href="#example-group">Group</a></h3>
{% set code_group %}
<div class="flex flex-col">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<img src="https://github.com/hunvreus.png" alt="Ronan Berder" class="grayscale size-10 rounded-full object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">hunvreus</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">hunvreus@gmail.com</p>
</div>
<button class="btn-icon-outline rounded-full">
{% lucide "plus" %}
</button>
</article>
<hr role="separator" class="border-border">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<img src="https://github.com/shadcn.png" alt="shadcn" class="grayscale size-10 rounded-full object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">shadcn</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">shadcn@vercel.com</p>
</div>
<button class="btn-icon-outline rounded-full">
{% lucide "plus" %}
</button>
</article>
<hr role="separator" class="border-border">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<img src="https://github.com/adamwathan.png" alt="Adam Wathan" class="grayscale size-10 rounded-full object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">adamwathan</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">adam.wathan@gmail.com</p>
</div>
<button class="btn-icon-outline rounded-full">
{% lucide "plus" %}
</button>
</article>
</div>
{% endset %}
{{ code_preview("item-group", code_group, class="w-full max-w-md") }}
<h3 id="example-header"><a href="#example-header">Header</a></h3>
{% set code_header %}
<div class="flex-col grid grid-cols-3 gap-4">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img alt="v0-1.5-sm" loading="lazy" class="grayscale aspect-square w-full rounded-sm object-cover flex-basis" src="https://avatar.vercel.sh/v0-1.5-sm">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">v0-1.5-sm</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Everyday tasks and UI generation.</p>
</div>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img alt="v0-1.5-lg" loading="lazy" class="grayscale aspect-square w-full rounded-sm object-cover flex-basis" src="https://avatar.vercel.sh/v0-1.5-lg">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">v0-1.5-lg</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Advanced thinking or reasoning.</p>
</div>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img alt="v0-2.0-mini" loading="lazy" class="grayscale aspect-square w-full rounded-sm object-cover flex-basis" src="https://avatar.vercel.sh/v0-2.0-mini">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">v0-2.0-mini</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Open Source model for everyone.</p>
</div>
</article>
</div>
{% endset %}
{{ code_preview("item-header", code_header) }}
<h3 id="example-link"><a href="#example-link">Link</a></h3>
<section class="prose">
<p>Items can be rendered as links by using an <code>&lt;a&gt;</code> element. Hover and focus states are applied automatically.</p>
</section>
{% set code_link %}
<div class="flex flex-col gap-6">
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Visit our documentation</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Learn how to get started with our components.</p>
</div>
<div class="flex items-center gap-2 [&_svg]:size-4">
{% lucide "chevron-right" %}
</div>
</a>
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">External resource</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Opens in a new tab with security attributes.</p>
</div>
<div class="flex items-center gap-2 [&_svg]:size-4">
{% lucide "external-link" %}
</div>
</a>
</div>
{% endset %}
{{ code_preview("item-link", code_link, class="w-full max-w-md") }}
+9 -1
View File
@@ -37,7 +37,15 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
{{ code_block(code_html | prettyHtml, "html") }}
<section class="prose">
<p>Simply use the <code>&lt;kbd&gt;</code> element:</p>
</section>
{% set code_simple %}
<kbd>⌘K</kbd>
{% endset %}
{{ code_block(code_simple | prettyHtml, "html") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
+4 -4
View File
@@ -1,12 +1,12 @@
{
"name": "basecoat",
"version": "0.2.0-beta.1",
"version": "0.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "basecoat",
"version": "0.2.0-beta.1",
"version": "0.3.2",
"license": "MIT",
"workspaces": [
"packages/*"
@@ -4337,7 +4337,7 @@
},
"packages/cli": {
"name": "basecoat-cli",
"version": "0.2.0-beta.1",
"version": "0.3.2",
"license": "MIT",
"dependencies": {
"commander": "^13.1.0",
@@ -4350,7 +4350,7 @@
},
"packages/css": {
"name": "basecoat-css",
"version": "0.2.0-beta.1",
"version": "0.3.2",
"license": "MIT"
}
}
+1 -2
View File
@@ -21,8 +21,7 @@
"css",
"html",
"jinja",
"nunjucks",
"alpinejs"
"nunjucks"
],
"license": "MIT",
"repository": {
+1 -2
View File
@@ -30,8 +30,7 @@
"css",
"html",
"jinja",
"nunjucks",
"alpinejs"
"nunjucks"
],
"repository": {
"type": "git",
+61 -44
View File
@@ -146,10 +146,6 @@
border-radius: var(--scrollbar-radius);
}
}
/* Alpine.js */
[x-cloak] {
display: none !important;
}
}
/* Alert */
@@ -385,11 +381,9 @@
/* Button Group */
.button-group {
@apply inline-flex isolate;
@apply inline-flex w-fit items-stretch isolate;
> *:hover,
> *:focus-visible,
> :is(.dropdown-menu, .popover) > button:hover,
> :is(.dropdown-menu, .popover) > button:focus-visible {
@apply relative z-10;
}
@@ -401,7 +395,7 @@
&:not([data-orientation='vertical']) {
> *:not(:first-child),
> :is(.dropdown-menu, .popover):not(:first-child) > button {
@apply -ml-px rounded-l-none;
@apply rounded-l-none border-l-0;
}
> *:not(:last-child),
> :is(.dropdown-menu, .popover):not(:last-child) > button {
@@ -416,7 +410,7 @@
}
> *:not(:first-child) {
@apply -mt-px ml-0 rounded-t-none;
@apply rounded-t-none border-t-0;
}
> *:not(:last-child) {
@apply rounded-b-none;
@@ -451,7 +445,7 @@
/* Checkbox */
@layer components {
.form input[type='checkbox']:not([role='switch']),
:is(.form, .field) input[type='checkbox']:not([role='switch']),
.input[type='checkbox']:not([role='switch']) {
@apply appearance-none border-input dark:bg-input/30 checked:bg-primary dark:checked:bg-primary checked:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50;
@@ -494,7 +488,7 @@
&::backdrop {
@apply opacity-100;
}
> article {
> * {
@apply scale-100;
}
@@ -504,7 +498,7 @@
&::backdrop {
@apply opacity-0;
}
> article {
> * {
@apply scale-95;
}
}
@@ -512,7 +506,7 @@
&::backdrop {
@apply bg-black/50 opacity-0 transition-all transition-discrete;
}
> article {
> * {
@apply bg-background fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%_-_2rem)] -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-lg border shadow-lg sm:max-w-lg max-h-[min(640px,calc(100%_-_2rem))];
@apply transition-all scale-95;
@@ -530,10 +524,10 @@
> header input {
@apply h-12;
}
[role='listbox'] {
[role='menu'] {
@apply px-2;
[role='option'] {
[role='menuitem'] {
@apply py-3 px-2 [&_svg]:size-5;
}
[role='heading'] {
@@ -554,11 +548,11 @@
@apply placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50;
}
}
[role='listbox'] {
[role='menu'] {
@apply max-h-[300px] overflow-y-auto overflow-x-hidden p-1;
[role='option'] {
@apply aria-hidden:hidden relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none aria-disabled:opacity-50 aria-disabled:pointer-events-none disabled:opacity-50 disabled:pointer-events-none w-full truncate [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4;
[role='menuitem'] {
@apply aria-hidden:hidden relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none aria-disabled:opacity-50 aria-disabled:pointer-events-none disabled:opacity-50 disabled:pointer-events-none w-full truncate [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4;
&.active {
@apply bg-accent text-accent-foreground;
@@ -567,7 +561,7 @@
[role='heading'] {
@apply text-muted-foreground flex px-2 py-1.5 text-xs font-medium;
}
[role='group']:not(:has([role='option']:not([aria-hidden='true']))) {
[role='group']:not(:has([role='menuitem']:not([aria-hidden='true']))) {
@apply hidden;
}
[role='separator'] {
@@ -583,7 +577,7 @@
@apply content-['No_results_found'];
}
}
&:not([data-command-initialized]) [role='option'] {
&:not([data-command-initialized]) [role='menuitem'] {
@apply hover:bg-accent hover:text-accent-foreground;
}
&:has(> header input:not(:placeholder-shown)) [role='separator'] {
@@ -603,7 +597,7 @@
&::backdrop {
@apply opacity-100;
}
> article {
> * {
@apply scale-100;
}
@@ -613,7 +607,7 @@
&::backdrop {
@apply opacity-0;
}
> article {
> * {
@apply scale-95;
}
}
@@ -621,7 +615,7 @@
&::backdrop {
@apply bg-black/50 opacity-0 transition-all transition-discrete;
}
> article {
> * {
@apply bg-background fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%_-_2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border p-6 shadow-lg sm:max-w-lg max-h-[calc(100%_-_2rem)];
@apply transition-all scale-95;
@@ -692,21 +686,46 @@
}
}
/* Field */
@layer components {
.fieldset {
@apply flex flex-col gap-6;
> legend {
@apply text-base font-medium mb-3;
}
> .description {
@apply text-sm text-muted-foreground;
}
}
.field {
@apply flex flex-col w-full gap-3 data-[invalid=true]:text-destructive [&>*]:w-full [&>.sr-only]:w-auto;
.description {
@apply text-sm text-muted-foreground;
}
.error {
@apply text-sm text-destructive font-medium;
}
}
}
/* Input */
@layer components {
.form input[type='text'],
.form input[type='email'],
.form input[type='password'],
.form input[type='number'],
.form input[type='file'],
.form input[type='tel'],
.form input[type='url'],
.form input[type='search'],
.form input[type='date'],
.form input[type='datetime-local'],
.form input[type='month'],
.form input[type='week'],
.form input[type='time'],
:is(.form, .field) input[type='text'],
:is(.form, .field) input[type='email'],
:is(.form, .field) input[type='password'],
:is(.form, .field) input[type='number'],
:is(.form, .field) input[type='file'],
:is(.form, .field) input[type='tel'],
:is(.form, .field) input[type='url'],
:is(.form, .field) input[type='search'],
:is(.form, .field) input[type='date'],
:is(.form, .field) input[type='datetime-local'],
:is(.form, .field) input[type='month'],
:is(.form, .field) input[type='week'],
:is(.form, .field) input[type='time'],
.input[type='text'],
.input[type='email'],
.input[type='password'],
@@ -735,7 +754,7 @@
/* Label */
@layer components {
.form label,
:is(.form, .field) label,
.label {
@apply flex items-center gap-2 text-sm leading-none font-medium select-none peer-disabled:pointer-events-none peer-disabled:opacity-50;
@@ -820,7 +839,7 @@
/* Radio */
@layer components {
.form input[type='radio'],
:is(.form, .field) input[type='radio'],
.input[type='radio'] {
@apply appearance-none border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 relative;
@@ -832,7 +851,7 @@
/* Range */
@layer components {
.form input[type='range'],
:is(.form, .field) input[type='range'],
.input[type='range'] {
@apply appearance-none flex items-center p-0 outline-none;
--slider-value: 20%;
@@ -881,7 +900,7 @@
/* Select */
@layer components {
.form select,
:is(.form, .field) select,
select.select {
@apply appearance-none border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent pl-3 pr-9 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 h-9;
@apply bg-[image:var(--chevron-down-icon-50)] bg-no-repeat bg-position-[center_right_0.75rem] bg-size-[1rem];
@@ -1068,7 +1087,7 @@
/* Switch */
@layer components {
.form input[type='checkbox'][role='switch'],
:is(.form, .field) input[type='checkbox'][role='switch'],
.input[type='checkbox'][role='switch'] {
@apply appearance-none focus-visible:border-ring focus-visible:ring-ring/50 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50;
@apply bg-input dark:bg-input/80 checked:bg-primary dark:checked:bg-primary;
@@ -1130,7 +1149,7 @@
/* Textarea */
@layer components {
.form textarea,
:is(.form, .field) textarea,
.textarea {
@apply border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm;
}
@@ -1196,8 +1215,6 @@
/* Tooltip */
@layer components {
[data-tooltip] {
@apply relative;
+48 -48
View File
@@ -7,7 +7,7 @@
@param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param input_attrs {object} [optional] - Additional HTML attributes for the search input.
@param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
#}
{% macro command(
id=None,
@@ -16,7 +16,7 @@
empty_text="No results found.",
main_attrs={},
input_attrs={},
listbox_attrs={}
menu_attrs={}
) %}
{% set id = id or ("command-" ~ range(100000, 999999) | random | string) %}
<div
@@ -39,19 +39,19 @@
aria-autocomplete="list"
role="combobox"
aria-expanded="true"
aria-controls="{{ id }}-listbox"
aria-controls="{{ id }}-menu"
{% for key, value in input_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
</header>
<div
role="listbox"
id="{{ id }}-listbox"
role="menu"
id="{{ id }}-menu"
aria-orientation="vertical"
data-empty="{{ empty_text }}"
class="scrollbar {{ listbox_attrs.class }}"
{% for key, value in listbox_attrs.items() %}
class="scrollbar {{ menu_attrs.class }}"
{% for key, value in menu_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
@@ -73,7 +73,7 @@
@param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog element.
@param input_attrs {object} [optional] - Additional HTML attributes for the search input.
@param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
@param open {boolean} [optional] [default=False] - Whether the command dialog should be open initially.
#}
{% macro command_dialog(
@@ -83,7 +83,7 @@
empty_text="No results found.",
dialog_attrs={},
input_attrs={},
listbox_attrs={},
menu_attrs={},
open=False
) %}
{% set id = id or ("command-dialog-" ~ range(100000, 999999) | random | string) %}
@@ -97,54 +97,56 @@
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<article>
<div class="command">
<header>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input
type="text"
id="{{ id }}-input"
placeholder="{{ placeholder }}"
autocomplete="off"
autocorrect="off"
spellcheck="false"
aria-autocomplete="list"
role="combobox"
aria-expanded="true"
aria-controls="{{ id }}-listbox"
{% for key, value in input_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
</header>
<div
role="listbox"
id="{{ id }}-listbox"
aria-orientation="vertical"
data-empty="{{ empty_text }}"
class="scrollbar {{ listbox_attrs.class }}"
{% for key, value in listbox_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
<div class="command">
<header>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input
type="text"
id="{{ id }}-input"
placeholder="{{ placeholder }}"
autocomplete="off"
autocorrect="off"
spellcheck="false"
aria-autocomplete="list"
role="combobox"
aria-expanded="true"
aria-controls="{{ id }}-menu"
{% for key, value in input_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if items and items|length > 0 %}
{{ render_command_items(items, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{% endif %}
</div>
</header>
<div
role="menu"
id="{{ id }}-menu"
aria-orientation="vertical"
data-empty="{{ empty_text }}"
class="scrollbar {{ menu_attrs.class }}"
{% for key, value in menu_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% if items and items|length > 0 %}
{{ render_command_items(items, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{% endif %}
</div>
<button type="button" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</article>
</div>
</dialog>
{% endmacro %}
{#
Renders a list of items for the command component.
@param items {array} - The array of items to render.
@param items {array} - Array of items to render. Each item can be:
- { type: "item", label: "Text", content: "HTML", keywords: "search terms", disabled: true, attrs: {} }
- { type: "group", label: "Group Title", items: [...] }
- { type: "separator" }
Note: Use attrs to add data-label for alternative search text if needed.
@param parent_id_prefix {string} [optional] - The prefix for the item id.
#}
{% macro render_command_items(items, parent_id_prefix="items") %}
@@ -169,10 +171,8 @@
{% elif item.type == "item" or not item.type %}
<div
id="{{ item_id }}"
role="option"
data-value="{{ item.value }}"
role="menuitem"
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
{% if item.label %}data-label="{{ item.label }}"{% endif %}
{% if item.disabled %}aria-disabled="true"{% endif %}
{% if item.attrs %}
{% for key, value in item.attrs.items() %}
+3 -3
View File
@@ -8,7 +8,7 @@
@param footer {string} [optional] - HTML content for the dialog footer.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog content article.
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog content container.
@param header_attrs {object} [optional] - Additional HTML attributes for the dialog header.
@param body_attrs {object} [optional] - Additional HTML attributes for the dialog body section.
@param footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer.
@@ -53,7 +53,7 @@
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<article>
<div>
{% if title or description %}
<header
{% for key, value in header_attrs %}
@@ -89,6 +89,6 @@
</button>
</form>
{% endif %}
</article>
</div>
</dialog>
{% endmacro %}
+53 -64
View File
@@ -1,32 +1,36 @@
(() => {
const initCommand = (container) => {
const input = container.querySelector('header input');
const listbox = container.querySelector('[role="listbox"]');
const menu = container.querySelector('[role="menu"]');
if (!input || !listbox) {
if (!input || !menu) {
const missing = [];
if (!input) missing.push('input');
if (!listbox) missing.push('listbox');
if (!menu) missing.push('menu');
console.error(`Command component initialization failed. Missing element(s): ${missing.join(', ')}`, container);
return;
}
const options = Array.from(listbox.querySelectorAll('[role="option"]'));
let visibleOptions = [...options];
const allMenuItems = Array.from(menu.querySelectorAll('[role="menuitem"]'));
const menuItems = allMenuItems.filter(item =>
!item.hasAttribute('disabled') &&
item.getAttribute('aria-disabled') !== 'true'
);
let visibleMenuItems = [...menuItems];
let activeIndex = -1;
const setActiveOption = (index) => {
if (activeIndex > -1 && options[activeIndex]) {
options[activeIndex].classList.remove('active');
const setActiveItem = (index) => {
if (activeIndex > -1 && menuItems[activeIndex]) {
menuItems[activeIndex].classList.remove('active');
}
activeIndex = index;
if (activeIndex > -1) {
const activeOption = options[activeIndex];
activeOption.classList.add('active');
if (activeOption.id) {
input.setAttribute('aria-activedescendant', activeOption.id);
const activeItem = menuItems[activeIndex];
activeItem.classList.add('active');
if (activeItem.id) {
input.setAttribute('aria-activedescendant', activeItem.id);
} else {
input.removeAttribute('aria-activedescendant');
}
@@ -35,39 +39,29 @@
}
};
const filterOptions = () => {
const filterMenuItems = () => {
const searchTerm = input.value.trim().toLowerCase();
setActiveOption(-1);
setActiveItem(-1);
visibleOptions = [];
options.forEach(option => {
const optionText = (option.dataset.label || option.textContent).trim().toLowerCase();
const keywords = (option.dataset.keywords || '').toLowerCase();
const matches = optionText.includes(searchTerm) || keywords.includes(searchTerm);
option.setAttribute('aria-hidden', String(!matches));
if (matches) {
visibleOptions.push(option);
visibleMenuItems = [];
allMenuItems.forEach(item => {
const itemText = (item.dataset.label || item.textContent).trim().toLowerCase();
const keywords = (item.dataset.keywords || '').toLowerCase();
const matches = itemText.includes(searchTerm) || keywords.includes(searchTerm);
item.setAttribute('aria-hidden', String(!matches));
if (matches && menuItems.includes(item)) {
visibleMenuItems.push(item);
}
});
if (visibleOptions.length > 0) {
setActiveOption(options.indexOf(visibleOptions[0]));
visibleOptions[0].scrollIntoView({ block: 'nearest' });
if (visibleMenuItems.length > 0) {
setActiveItem(menuItems.indexOf(visibleMenuItems[0]));
visibleMenuItems[0].scrollIntoView({ block: 'nearest' });
}
};
const selectOption = (option) => {
if (!option) return;
const event = new CustomEvent('command:select', {
detail: { value: option.dataset.value },
bubbles: true
});
container.dispatchEvent(event);
};
input.addEventListener('input', filterOptions);
input.addEventListener('input', filterMenuItems);
const handleKeyNavigation = (event) => {
if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End'].includes(event.key)) {
@@ -77,21 +71,21 @@
if (event.key === 'Enter') {
event.preventDefault();
if (activeIndex > -1) {
selectOption(options[activeIndex]);
menuItems[activeIndex]?.click();
}
return;
}
if (visibleOptions.length === 0) return;
if (visibleMenuItems.length === 0) return;
event.preventDefault();
const currentVisibleIndex = activeIndex > -1 ? visibleOptions.indexOf(options[activeIndex]) : -1;
const currentVisibleIndex = activeIndex > -1 ? visibleMenuItems.indexOf(menuItems[activeIndex]) : -1;
let nextVisibleIndex = currentVisibleIndex;
switch (event.key) {
case 'ArrowDown':
if (currentVisibleIndex < visibleOptions.length - 1) {
if (currentVisibleIndex < visibleMenuItems.length - 1) {
nextVisibleIndex = currentVisibleIndex + 1;
}
break;
@@ -106,47 +100,42 @@
nextVisibleIndex = 0;
break;
case 'End':
nextVisibleIndex = visibleOptions.length - 1;
nextVisibleIndex = visibleMenuItems.length - 1;
break;
}
if (nextVisibleIndex !== currentVisibleIndex) {
const newActiveOption = visibleOptions[nextVisibleIndex];
setActiveOption(options.indexOf(newActiveOption));
newActiveOption.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
const newActiveItem = visibleMenuItems[nextVisibleIndex];
setActiveItem(menuItems.indexOf(newActiveItem));
newActiveItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
};
listbox.addEventListener('mousemove', (event) => {
const option = event.target.closest('[role="option"]');
if (option && visibleOptions.includes(option)) {
const index = options.indexOf(option);
menu.addEventListener('mousemove', (event) => {
const menuItem = event.target.closest('[role="menuitem"]');
if (menuItem && visibleMenuItems.includes(menuItem)) {
const index = menuItems.indexOf(menuItem);
if (index !== activeIndex) {
setActiveOption(index);
setActiveItem(index);
}
}
});
listbox.addEventListener('mouseleave', () => {
if (visibleOptions.length > 0) {
setActiveOption(options.indexOf(visibleOptions[0]));
} else {
setActiveOption(-1);
}
});
listbox.addEventListener('click', (event) => {
const clickedOption = event.target.closest('[role="option"]');
if (clickedOption && visibleOptions.includes(clickedOption)) {
selectOption(clickedOption);
menu.addEventListener('click', (event) => {
const clickedItem = event.target.closest('[role="menuitem"]');
if (clickedItem && visibleMenuItems.includes(clickedItem)) {
const dialog = container.closest('dialog.command-dialog');
if (dialog && !clickedItem.hasAttribute('data-keep-command-open')) {
dialog.close();
}
}
});
input.addEventListener('keydown', handleKeyNavigation);
if (visibleOptions.length > 0) {
setActiveOption(options.indexOf(visibleOptions[0]));
visibleOptions[0].scrollIntoView({ block: 'nearest' });
if (visibleMenuItems.length > 0) {
setActiveItem(menuItems.indexOf(visibleMenuItems[0]));
visibleMenuItems[0].scrollIntoView({ block: 'nearest' });
}
container.dataset.commandInitialized = true;
+5 -4
View File
@@ -16,7 +16,8 @@
return;
}
const options = Array.from(listbox.querySelectorAll('[role="option"]'));
const allOptions = Array.from(listbox.querySelectorAll('[role="option"]'));
const options = allOptions.filter(opt => opt.getAttribute('aria-disabled') !== 'true');
let visibleOptions = [...options];
let activeIndex = -1;
@@ -69,7 +70,7 @@
const resetFilter = () => {
filter.value = '';
visibleOptions = [...options];
options.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
allOptions.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
};
if (hasTransition()) {
@@ -110,11 +111,11 @@
setActiveOption(-1);
visibleOptions = [];
options.forEach(option => {
allOptions.forEach(option => {
const optionText = (option.dataset.label || option.textContent).trim().toLowerCase();
const matches = optionText.includes(searchTerm);
option.setAttribute('aria-hidden', String(!matches));
if (matches) {
if (matches && options.includes(option)) {
visibleOptions.push(option);
}
});
+48 -48
View File
@@ -7,7 +7,7 @@
@param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param input_attrs {object} [optional] - Additional HTML attributes for the search input.
@param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
#}
{% macro command(
id=None,
@@ -16,7 +16,7 @@
empty_text="No results found.",
main_attrs={},
input_attrs={},
listbox_attrs={}
menu_attrs={}
) %}
{% set id = id or ("command-" + (range(100000, 999999) | random | string)) %}
<div
@@ -39,19 +39,19 @@
aria-autocomplete="list"
role="combobox"
aria-expanded="true"
aria-controls="{{ id }}-listbox"
aria-controls="{{ id }}-menu"
{% for key, value in input_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
</header>
<div
role="listbox"
id="{{ id }}-listbox"
role="menu"
id="{{ id }}-menu"
aria-orientation="vertical"
data-empty="{{ empty_text }}"
class="scrollbar {{ listbox_attrs.class }}"
{% for key, value in listbox_attrs %}
class="scrollbar {{ menu_attrs.class }}"
{% for key, value in menu_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
@@ -73,7 +73,7 @@
@param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog element.
@param input_attrs {object} [optional] - Additional HTML attributes for the search input.
@param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
@param open {boolean} [optional] [default=false] - Whether the command dialog should be open initially.
#}
{% macro command_dialog(
@@ -83,7 +83,7 @@
empty_text="No results found.",
dialog_attrs={},
input_attrs={},
listbox_attrs={},
menu_attrs={},
open=false
) %}
{% set id = id or ("command-dialog-" + (range(100000, 999999) | random | string)) %}
@@ -97,54 +97,56 @@
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<article>
<div class="command">
<header>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input
type="text"
id="{{ id }}-input"
placeholder="{{ placeholder }}"
autocomplete="off"
autocorrect="off"
spellcheck="false"
aria-autocomplete="list"
role="combobox"
aria-expanded="true"
aria-controls="{{ id }}-listbox"
{% for key, value in input_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
</header>
<div
role="listbox"
id="{{ id }}-listbox"
aria-orientation="vertical"
data-empty="{{ empty_text }}"
class="scrollbar {{ listbox_attrs.class }}"
{% for key, value in listbox_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
<div class="command">
<header>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input
type="text"
id="{{ id }}-input"
placeholder="{{ placeholder }}"
autocomplete="off"
autocorrect="off"
spellcheck="false"
aria-autocomplete="list"
role="combobox"
aria-expanded="true"
aria-controls="{{ id }}-menu"
{% for key, value in input_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if items and items.length > 0 %}
{{ render_command_items(items, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{% endif %}
</div>
</header>
<div
role="menu"
id="{{ id }}-menu"
aria-orientation="vertical"
data-empty="{{ empty_text }}"
class="scrollbar {{ menu_attrs.class }}"
{% for key, value in menu_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% if items and items.length > 0 %}
{{ render_command_items(items, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{% endif %}
</div>
<button type="button" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</article>
</div>
</dialog>
{% endmacro %}
{#
Renders a list of items for the command component.
@param items {array} - The array of items to render.
@param items {array} - Array of items to render. Each item can be:
- { type: "item", label: "Text", content: "HTML", keywords: "search terms", disabled: true, attrs: {} }
- { type: "group", label: "Group Title", items: [...] }
- { type: "separator" }
Note: Use attrs to add data-label for alternative search text if needed.
@param parent_id_prefix {string} [optional] - The prefix for the item id.
#}
{% macro render_command_items(items, parent_id_prefix="items") %}
@@ -169,10 +171,8 @@
{% elif item.type == "item" or not item.type %}
<div
id="{{ item_id }}"
role="option"
data-value="{{ item.value }}"
role="menuitem"
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
{% if item.label %}data-label="{{ item.label }}"{% endif %}
{% if item.disabled %}aria-disabled="true"{% endif %}
{% if item.attrs %}
{% for key, value in item.attrs %}
+3 -3
View File
@@ -8,7 +8,7 @@
@param footer {string} [optional] - HTML content for the dialog footer.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog content article.
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog content container.
@param header_attrs {object} [optional] - Additional HTML attributes for the dialog header.
@param body_attrs {object} [optional] - Additional HTML attributes for the dialog body section.
@param footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer.
@@ -53,7 +53,7 @@
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<article>
<div>
{% if title or description %}
<header
{% for key, value in header_attrs %}
@@ -87,6 +87,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
{% endif %}
</article>
</div>
</dialog>
{% endmacro %}