diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1247087 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,87 @@ +# Basecoat – Agent Guide + +## What is Basecoat? + +Basecoat is a vanilla JavaScript/HTML/CSS port of shadcn/ui components. It reuses shadcn/ui's design patterns and Tailwind CSS classes while maintaining semantic HTML, native browser elements, and full accessibility (ARIA attributes, proper hierarchies, keyboard navigation). The goal is to provide shadcn-quality components without React dependencies. + +## Quickstart + +- Install deps: `npm i` +- Dev docs server: `npm run docs:dev` +- Build CSS/JS: `npm run build` +- Build static docs: `npm run docs:build` + +## What to edit (and what not) + +- Edit sources only: + - `src/css/basecoat.css` – All component CSS classes (Tailwind + custom) + - `src/js/*.js` – Individual component JS files (kebab-case, ESM) + `basecoat.js` (component registry) + - `src/nunjucks/*.njk` and `src/jinja/*.html.jinja` – Component template macros + - `docs/src/components/*.njk` – Component documentation pages with examples + - `docs/src/_includes/` – Layout templates, partials, and navigation +- Do not edit build outputs: + - `packages/*/dist` – built CSS/JS bundles + - `_site/` – generated docs site + +## Project layout + +- Workspaces: root npm workspace with publishable packages in `packages/*`. +- Packages: + - `packages/css` – built CSS and JS bundles under `dist/`. + - `packages/cli` – CLI; distributed assets live under `dist/`. +- Docs site: `docs/src` (Eleventy); output goes to `_site/`. + +## Conventions + +- ESM everywhere (`import`/`export`). +- Prettier defaults (2 spaces; single quotes per file preference). +- Filenames for assets: kebab-case (e.g., `dropdown-menu.js`, `basecoat.cdn.css`). +- JS naming: `camelCase` for vars/functions; `PascalCase` for DOM classes and exported component names when applicable. + +## Design Principles + +- **Minimal code**: Prefer native HTML elements and browser behavior over JavaScript abstractions +- **Accessibility-first**: Semantic HTML, proper ARIA attributes, keyboard navigation +- **CSS patterns**: + - Use `border` for lines/separators (not `background` on `
`) + - 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`. +- Update a component template: Edit `src/nunjucks/*.njk` and/or `src/jinja/*.html.jinja`. +- Add a new component: Create JS, CSS, templates, and docs page under `docs/src/components/*.njk`. +- Update navigation: Edit files in `docs/src/_includes/partials/`. + +## Testing + +No automated testing configured. User validates manually via the docs site (`npm run docs:dev`). + +## Commits and PRs + +- Commits: concise, imperative (≤72 chars), optional scope. + - Example: `select: prevent change event on init`. +- PRs: include description, linked issues (e.g., `Closes #123`), and before/after screenshots or code snippets for visual/behavioral changes. +- Keep PRs focused; update docs examples when components or APIs change. + +## Environment and tooling + +- Node 18+ recommended. +- Eleventy config: `.eleventy.js`. +- Tailwind via `@tailwindcss/cli` v4. +- Build uses Tailwind CLI and `terser`; both are provided via devDependencies. + +## Package references + +- `packages/css` usage and details: see `packages/css/README.md`. +- `packages/cli` usage and details: see `packages/cli/README.md`. diff --git a/docs/css/custom.css b/docs/css/custom.css index 37f4d57..44236ba 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -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; diff --git a/docs/css/styles.css b/docs/css/styles.css index 029084f..c1ddbaf 100644 --- a/docs/css/styles.css +++ b/docs/css/styles.css @@ -5,4 +5,140 @@ @import "./themes/claude.css"; @import "./themes/doom-64.css"; -@import "./themes/supabase.css"; \ No newline at end of file +@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); + } */ \ No newline at end of file diff --git a/docs/src/_includes/layouts/base.njk b/docs/src/_includes/layouts/base.njk index 444b061..d93d3c5 100644 --- a/docs/src/_includes/layouts/base.njk +++ b/docs/src/_includes/layouts/base.njk @@ -70,6 +70,7 @@ {# JS #} + @@ -98,7 +99,6 @@ {% if head %}{{ head | safe }}{% endif %} diff --git a/docs/src/_includes/macros/code_block.njk b/docs/src/_includes/macros/code_block.njk index af87e0a..f60a19a 100644 --- a/docs/src/_includes/macros/code_block.njk +++ b/docs/src/_includes/macros/code_block.njk @@ -1,6 +1,6 @@ {% macro code_block(code, language="html", class="my-4") %}
-
{{- code | escape -}}
+
{{- code | escape -}}
diff --git a/docs/src/_includes/partials/sidebar.njk b/docs/src/_includes/partials/sidebar.njk index 8b857e8..98bc8d4 100644 --- a/docs/src/_includes/partials/sidebar.njk +++ b/docs/src/_includes/partials/sidebar.njk @@ -61,6 +61,14 @@ "hx-swap": "outerHTML" } %} +{% set new_link_attrs = { + "hx-boost": "true", + "hx-select": "#content", + "hx-target": "#content", + "hx-swap": "outerHTML", + "data-new-link": "true" +} %} + {% set icon_home %}{% lucide "house" %}{% endset %} {% set icon_info %}{% lucide "info" %}{% endset %} {% set icon_square_terminal %}{% lucide "square-terminal" %}{% endset %} @@ -86,21 +94,30 @@ { label: "Badge", url: "/components/badge", attrs: link_attrs }, { label: "Breadcrumb", url: "/components/breadcrumb", attrs: link_attrs }, { label: "Button", url: "/components/button", attrs: link_attrs }, + { label: "Button Group", url: "/components/button-group", attrs: new_link_attrs }, { label: "Card", url: "/components/card", attrs: link_attrs }, { label: "Checkbox", url: "/components/checkbox", attrs: link_attrs }, + { label: "Command", url: "/components/command", attrs: new_link_attrs }, { label: "Combobox", url: "/components/combobox", attrs: link_attrs }, { label: "Dialog", url: "/components/dialog", attrs: link_attrs }, { label: "Dropdown Menu", url: "/components/dropdown-menu", attrs: link_attrs }, + { label: "Empty", url: "/components/empty", attrs: new_link_attrs }, + { label: "Field", url: "/components/field", attrs: new_link_attrs }, { label: "Form", url: "/components/form", attrs: link_attrs }, { label: "Input", url: "/components/input", attrs: link_attrs }, + { label: "Input Group", url: "/components/input-group", attrs: new_link_attrs }, + { label: "Item", url: "/components/item", attrs: new_link_attrs }, + { label: "Kbd", url: "/components/kbd", attrs: new_link_attrs }, { label: "Label", url: "/components/label", attrs: link_attrs }, { label: "Pagination", url: "/components/pagination", attrs: link_attrs }, { label: "Popover", url: "/components/popover", attrs: link_attrs }, + { label: "Progress", url: "/components/progress", attrs: new_link_attrs }, { label: "Radio Group", url: "/components/radio-group", attrs: link_attrs }, { label: "Select", url: "/components/select", attrs: link_attrs }, - { label: "Skeleton", url: "/components/skeleton", attrs: link_attrs }, { label: "Sidebar", url: "/components/sidebar", attrs: link_attrs }, + { label: "Skeleton", url: "/components/skeleton", attrs: link_attrs }, { label: "Slider", url: "/components/slider", attrs: link_attrs }, + { label: "Spinner", url: "/components/spinner", attrs: new_link_attrs }, { label: "Switch", url: "/components/switch", attrs: link_attrs }, { label: "Table", url: "/components/table", attrs: link_attrs }, { label: "Tabs", url: "/components/tabs", attrs: link_attrs }, @@ -117,6 +134,6 @@ label="Sidebar navigation", header=header, footer=footer, - content_attrs={ "class": "scrollbar" }, + content_attrs={ "class": "scrollbar [&_[data-new-link]::after]:content-['New'] [&_[data-new-link]::after]:ml-auto [&_[data-new-link]::after]:text-xs [&_[data-new-link]::after]:font-medium [&_[data-new-link]::after]:bg-sidebar-primary [&_[data-new-link]::after]:text-sidebar-primary-foreground [&_[data-new-link]::after]:px-2 [&_[data-new-link]::after]:py-0.5 [&_[data-new-link]::after]:rounded-md" }, menu=menu -) }} +) }} \ No newline at end of file diff --git a/docs/src/assets/styles.css b/docs/src/assets/styles.css index 17d35b0..7b4895a 100644 --- a/docs/src/assets/styles.css +++ b/docs/src/assets/styles.css @@ -5,23 +5,30 @@ :root, :host { --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --color-red-500: oklch(63.7% 0.237 25.331); --color-amber-50: oklch(98.7% 0.022 95.277); --color-amber-100: oklch(96.2% 0.059 95.617); --color-amber-900: oklch(41.4% 0.112 45.904); --color-amber-950: oklch(27.9% 0.077 45.635); + --color-yellow-500: oklch(79.5% 0.184 86.047); --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; @@ -32,6 +39,7 @@ --container-md: 28rem; --container-lg: 32rem; --container-xl: 36rem; + --container-3xl: 48rem; --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); --text-sm: 0.875rem; @@ -64,13 +72,6 @@ --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); - --shadow-2xs: 0 1px rgb(0 0 0 / 0.05); - --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); - --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); --animate-spin: spin 1s linear infinite; --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; @@ -253,9 +254,6 @@ } } @layer utilities { - .\@container { - container-type: inline-size; - } .pointer-events-none { pointer-events: none; } @@ -279,6 +277,9 @@ .relative { position: relative; } + .static { + position: static; + } .sticky { position: sticky; } @@ -291,9 +292,6 @@ .top-0 { top: calc(var(--spacing) * 0); } - .top-1 { - top: calc(var(--spacing) * 1); - } .top-1\/2 { top: calc(1/2 * 100%); } @@ -303,14 +301,20 @@ .top-22 { top: calc(var(--spacing) * 22); } - .right-2 { - right: calc(var(--spacing) * 2); + .right-1\.5 { + right: calc(var(--spacing) * 1.5); } .right-2\.5 { right: calc(var(--spacing) * 2.5); } - .left-1 { - left: calc(var(--spacing) * 1); + .right-3 { + right: calc(var(--spacing) * 3); + } + .bottom-0 { + bottom: calc(var(--spacing) * 0); + } + .left-1\.5 { + left: calc(var(--spacing) * 1.5); } .left-1\/2 { left: calc(1/2 * 100%); @@ -318,6 +322,9 @@ .left-2 { left: calc(var(--spacing) * 2); } + .left-3 { + left: calc(var(--spacing) * 3); + } .isolate { isolation: isolate; } @@ -345,6 +352,9 @@ max-width: 96rem; } } + .m-0 { + margin: calc(var(--spacing) * 0); + } .mx-auto { margin-inline: auto; } @@ -366,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); } @@ -378,15 +394,24 @@ .mb-12 { margin-bottom: calc(var(--spacing) * 12); } - .-ml-1 { - margin-left: calc(var(--spacing) * -1); - } .-ml-1\.5 { margin-left: calc(var(--spacing) * -1.5); } .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; } @@ -402,6 +427,9 @@ .hidden { display: none; } + .inline { + display: inline; + } .inline-block { display: inline-block; } @@ -437,6 +465,10 @@ width: calc(var(--spacing) * 4); height: calc(var(--spacing) * 4); } + .size-6 { + width: calc(var(--spacing) * 6); + height: calc(var(--spacing) * 6); + } .size-7 { width: calc(var(--spacing) * 7); height: calc(var(--spacing) * 7); @@ -457,9 +489,15 @@ width: calc(var(--spacing) * 12); height: calc(var(--spacing) * 12); } + .h-2 { + height: calc(var(--spacing) * 2); + } .h-4 { height: calc(var(--spacing) * 4); } + .h-5 { + height: calc(var(--spacing) * 5); + } .h-6 { height: calc(var(--spacing) * 6); } @@ -475,6 +513,9 @@ .h-14 { height: calc(var(--spacing) * 14); } + .h-full { + height: 100%; + } .max-h-64 { max-height: calc(var(--spacing) * 64); } @@ -490,24 +531,36 @@ .max-h-\[650px\] { max-height: 650px; } + .min-h-25 { + min-height: calc(var(--spacing) * 25); + } + .min-h-27 { + min-height: calc(var(--spacing) * 27); + } + .min-h-77 { + min-height: calc(var(--spacing) * 77); + } .min-h-\[350px\] { min-height: 350px; } - .w-1 { - width: calc(var(--spacing) * 1); + .w-0 { + width: calc(var(--spacing) * 0); } .w-1\/2 { width: calc(1/2 * 100%); } - .w-2 { - width: calc(var(--spacing) * 2); - } .w-2\/3 { width: calc(2/3 * 100%); } + .w-3\/4 { + width: calc(3/4 * 100%); + } .w-4 { width: calc(var(--spacing) * 4); } + .w-7\.5 { + width: calc(var(--spacing) * 7.5); + } .w-10 { width: calc(var(--spacing) * 10); } @@ -535,12 +588,21 @@ .w-\[271px\] { width: 271px; } + .w-fit { + width: fit-content; + } .w-full { width: 100%; } .w-max { width: max-content; } + .max-w-3xl { + max-width: var(--container-3xl); + } + .max-w-72 { + max-width: calc(var(--spacing) * 72); + } .max-w-\[75\%\] { max-width: 75%; } @@ -550,6 +612,9 @@ .max-w-\[300px\] { max-width: 300px; } + .max-w-\[450px\] { + max-width: 450px; + } .max-w-lg { max-width: var(--container-lg); } @@ -568,6 +633,12 @@ .max-w-xl { max-width: var(--container-xl); } + .max-w-xs { + max-width: var(--container-xs); + } + .min-w-0 { + min-width: calc(var(--spacing) * 0); + } .min-w-5 { min-width: calc(var(--spacing) * 5); } @@ -583,35 +654,26 @@ .flex-1 { flex: 1; } + .flex-none { + flex: none; + } .flex-shrink { flex-shrink: 1; } .shrink-0 { flex-shrink: 0; } - .border-collapse { - border-collapse: collapse; - } - .-translate-x-1 { - --tw-translate-x: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); + .basis-full { + flex-basis: 100%; } .-translate-x-1\/2 { --tw-translate-x: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); } - .-translate-y-1 { - --tw-translate-y: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } .-translate-y-1\/2 { --tw-translate-y: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); } - .translate-y-0 { - --tw-translate-y: calc(var(--spacing) * 0); - translate: var(--tw-translate-x) var(--tw-translate-y); - } .translate-y-0\.5 { --tw-translate-y: calc(var(--spacing) * 0.5); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -628,9 +690,6 @@ .cursor-pointer { cursor: pointer; } - .resize { - resize: both; - } .scroll-m-20 { scroll-margin: calc(var(--spacing) * 20); } @@ -640,9 +699,15 @@ .scroll-mt-16 { scroll-margin-top: calc(var(--spacing) * 16); } + .list-inside { + list-style-position: inside; + } .list-decimal { list-style-type: decimal; } + .list-disc { + list-style-type: disc; + } .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -667,6 +732,9 @@ .items-start { align-items: flex-start; } + .items-stretch { + align-items: stretch; + } .justify-between { justify-content: space-between; } @@ -676,9 +744,6 @@ .justify-start { justify-content: flex-start; } - .gap-0 { - gap: calc(var(--spacing) * 0); - } .gap-0\.5 { gap: calc(var(--spacing) * 0.5); } @@ -691,6 +756,9 @@ .gap-2 { gap: calc(var(--spacing) * 2); } + .gap-2\.5 { + gap: calc(var(--spacing) * 2.5); + } .gap-3 { gap: calc(var(--spacing) * 3); } @@ -700,12 +768,18 @@ .gap-6 { gap: calc(var(--spacing) * 6); } + .gap-7 { + gap: calc(var(--spacing) * 7); + } .gap-8 { gap: calc(var(--spacing) * 8); } .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; @@ -734,15 +808,15 @@ margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); } } - .gap-x-1 { - column-gap: calc(var(--spacing) * 1); - } .gap-x-1\.5 { column-gap: calc(var(--spacing) * 1.5); } .gap-x-2 { column-gap: calc(var(--spacing) * 2); } + .gap-x-4 { + column-gap: calc(var(--spacing) * 4); + } .gap-x-10 { column-gap: calc(var(--spacing) * 10); } @@ -769,11 +843,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; } @@ -795,12 +875,23 @@ .rounded-md { border-radius: var(--radius-md); } + .rounded-none { + border-radius: 0; + } .rounded-sm { border-radius: var(--radius-sm); } .rounded-xl { border-radius: var(--radius-xl); } + .rounded-l-md { + border-top-left-radius: var(--radius-md); + border-bottom-left-radius: var(--radius-md); + } + .rounded-r-md { + border-top-right-radius: var(--radius-md); + border-bottom-right-radius: var(--radius-md); + } .border { border-style: var(--tw-border-style); border-width: 1px; @@ -813,10 +904,26 @@ border-top-style: var(--tw-border-style); border-top-width: 1px; } + .border-r { + border-right-style: var(--tw-border-style); + border-right-width: 1px; + } + .border-r-0 { + border-right-style: var(--tw-border-style); + border-right-width: 0px; + } .border-b { border-bottom-style: var(--tw-border-style); border-bottom-width: 1px; } + .border-l-0 { + border-left-style: var(--tw-border-style); + border-left-width: 0px; + } + .border-dashed { + --tw-border-style: dashed; + border-style: dashed; + } .border-amber-50 { border-color: var(--color-amber-50); } @@ -832,8 +939,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); @@ -844,6 +957,9 @@ .bg-background { background-color: var(--color-background); } + .bg-blue-500 { + background-color: var(--color-blue-500); + } .bg-card { background-color: var(--color-card); } @@ -853,14 +969,29 @@ .bg-muted { background-color: var(--color-muted); } + .bg-muted-foreground { + background-color: var(--color-muted-foreground); + } + .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-primary\/20 { + background-color: var(--color-primary); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-primary) 20%, transparent); + } + } .bg-sidebar-primary { background-color: var(--color-sidebar-primary); } - .mask-repeat { - mask-repeat: repeat; + .bg-transparent { + background-color: transparent; } .object-cover { object-fit: cover; @@ -868,6 +999,9 @@ .\!p-3\.5 { padding: calc(var(--spacing) * 3.5) !important; } + .p-0 { + padding: calc(var(--spacing) * 0); + } .p-1 { padding: calc(var(--spacing) * 1); } @@ -880,6 +1014,9 @@ .p-4 { padding: calc(var(--spacing) * 4); } + .p-6 { + padding: calc(var(--spacing) * 6); + } .p-10 { padding: calc(var(--spacing) * 10); } @@ -901,12 +1038,12 @@ .px-4 { padding-inline: calc(var(--spacing) * 4); } + .px-9 { + padding-inline: calc(var(--spacing) * 9); + } .px-\[0\.3rem\] { padding-inline: 0.3rem; } - .py-1 { - padding-block: calc(var(--spacing) * 1); - } .py-1\.5 { padding-block: calc(var(--spacing) * 1.5); } @@ -922,21 +1059,75 @@ .py-\[0\.2rem\] { padding-block: 0.2rem; } + .pt-1\.5 { + padding-top: calc(var(--spacing) * 1.5); + } .pt-2 { padding-top: calc(var(--spacing) * 2); } + .pt-15 { + padding-top: calc(var(--spacing) * 15); + } + .pr-2 { + padding-right: calc(var(--spacing) * 2); + } + .pr-9 { + padding-right: calc(var(--spacing) * 9); + } + .pr-10 { + padding-right: calc(var(--spacing) * 10); + } + .pr-12 { + padding-right: calc(var(--spacing) * 12); + } + .pr-16 { + padding-right: calc(var(--spacing) * 16); + } + .pr-20 { + padding-right: calc(var(--spacing) * 20); + } + .pr-24 { + padding-right: calc(var(--spacing) * 24); + } + .pr-26 { + padding-right: calc(var(--spacing) * 26); + } + .pr-30 { + padding-right: calc(var(--spacing) * 30); + } + .pb-3 { + padding-bottom: calc(var(--spacing) * 3); + } .pb-4 { padding-bottom: calc(var(--spacing) * 4); } .pb-12 { padding-bottom: calc(var(--spacing) * 12); } + .pb-17 { + padding-bottom: calc(var(--spacing) * 17); + } .pl-6 { padding-left: calc(var(--spacing) * 6); } + .pl-7 { + padding-left: calc(var(--spacing) * 7); + } .pl-8 { padding-left: calc(var(--spacing) * 8); } + .pl-8\.5 { + padding-left: calc(var(--spacing) * 8.5); + } + .pl-9 { + padding-left: calc(var(--spacing) * 9); + } + .pl-15 { + padding-left: calc(var(--spacing) * 15); + } + .pl-21 { + padding-left: calc(var(--spacing) * 21); + } .text-center { text-align: center; } @@ -969,6 +1160,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)); @@ -1016,30 +1211,57 @@ --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; } .text-amber-900 { color: var(--color-amber-900); } + .text-blue-500 { + color: var(--color-blue-500); + } .text-destructive { color: var(--color-destructive); } .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-purple-500 { + color: var(--color-purple-500); + } + .text-red-500 { + color: var(--color-red-500); + } .text-sidebar-primary-foreground { color: var(--color-sidebar-primary-foreground); } + .text-white { + color: var(--color-white); + } + .text-yellow-500 { + color: var(--color-yellow-500); + } .uppercase { text-transform: uppercase; } @@ -1065,6 +1287,10 @@ .opacity-100 { opacity: 100%; } + .shadow-md { + --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } .shadow-none { --tw-shadow: 0 0 #0000; box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); @@ -1084,6 +1310,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,); } @@ -1107,6 +1337,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; @@ -1133,6 +1367,16 @@ visibility: visible; } } + .group-data-\[copied\=true\]\:block { + &:is(:where(.group)[data-copied="true"] *) { + display: block; + } + } + .group-data-\[copied\=true\]\:hidden { + &:is(:where(.group)[data-copied="true"] *) { + display: none; + } + } .group-\[\.copied\]\:block { &:is(:where(.group):is(.copied) *) { display: block; @@ -1226,6 +1470,13 @@ } } } + .hover\:bg-foreground { + &:hover { + @media (hover: hover) { + background-color: var(--color-foreground); + } + } + } .hover\:bg-muted { &:hover { @media (hover: hover) { @@ -1233,6 +1484,13 @@ } } } + .hover\:text-accent-foreground { + &:hover { + @media (hover: hover) { + color: var(--color-accent-foreground); + } + } + } .hover\:text-foreground { &:hover { @media (hover: hover) { @@ -1302,6 +1560,29 @@ outline-style: none; } } + .disabled\:pointer-events-none { + &:disabled { + pointer-events: none; + } + } + .disabled\:opacity-50 { + &:disabled { + opacity: 50%; + } + } + .has-\[\:checked\]\:border-primary { + &:has(*:is(:checked)) { + border-color: var(--color-primary); + } + } + .has-\[\:checked\]\:bg-primary\/5 { + &:has(*:is(:checked)) { + background-color: var(--color-primary); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-primary) 5%, transparent); + } + } + } .has-\[input\[type\=\'checkbox\'\]\:checked\]\:border-blue-600 { &:has(*:is(input[type='checkbox']:checked)) { border-color: var(--color-blue-600); @@ -1322,6 +1603,43 @@ 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); + } + } + } + .aria-pressed\:bg-transparent { + &[aria-pressed="true"] { + background-color: transparent; + } + } + .aria-pressed\:text-blue-600 { + &[aria-pressed="true"] { + color: var(--color-blue-600); + } + } + .aria-pressed\:hover\:bg-accent { + &[aria-pressed="true"] { + &:hover { + @media (hover: hover) { + background-color: var(--color-accent); + } + } + } + } .max-sm\:flex-col { @media (width < 40rem) { flex-direction: column; @@ -1365,16 +1683,41 @@ width: 239px; } } + .md\:w-auto { + @media (width >= 48rem) { + width: auto; + } + } .md\:max-w-\[200px\] { @media (width >= 48rem) { max-width: 200px; } } + .md\:min-w-80 { + @media (width >= 48rem) { + min-width: calc(var(--spacing) * 80); + } + } + .md\:flex-auto { + @media (width >= 48rem) { + flex: auto; + } + } .md\:flex-row { @media (width >= 48rem) { flex-direction: row; } } + .md\:items-center { + @media (width >= 48rem) { + align-items: center; + } + } + .md\:items-start { + @media (width >= 48rem) { + align-items: flex-start; + } + } .md\:border-l { @media (width >= 48rem) { border-left-style: var(--tw-border-style); @@ -1386,6 +1729,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); @@ -1448,11 +1796,21 @@ background-color: var(--color-amber-950); } } + .dark\:bg-blue-600 { + &:is(.dark *) { + background-color: var(--color-blue-600); + } + } .dark\:text-amber-100 { &:is(.dark *) { color: var(--color-amber-100); } } + .dark\:text-neutral-300 { + &:is(.dark *) { + color: var(--color-neutral-300); + } + } .dark\:checked\:border-blue-700 { &:is(.dark *) { &:checked { @@ -1516,6 +1874,16 @@ } } } + .dark\:has-\[\:checked\]\:bg-primary\/10 { + &:is(.dark *) { + &:has(*:is(:checked)) { + background-color: var(--color-primary); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-primary) 10%, transparent); + } + } + } + } .dark\:has-\[input\[type\=\'checkbox\'\]\:checked\]\:border-blue-900 { &:is(.dark *) { &:has(*:is(input[type='checkbox']:checked)) { @@ -1544,6 +1912,83 @@ } } } + .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); + } + } + } + } + .dark\:aria-pressed\:hover\:bg-accent\/50 { + &:is(.dark *) { + &[aria-pressed="true"] { + &: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); + } + } + } + } + } + } + .\[\&_\[data-name\]\]\:hidden { + & [data-name] { + display: none; + } + } + .\[\&_\[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; @@ -1653,11 +2098,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); @@ -1682,17 +2161,70 @@ } } } + .\[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); } } + .\[\&\>svg\]\:size-3 { + &>svg { + width: calc(var(--spacing) * 3); + height: calc(var(--spacing) * 3); + } + } + .\[\&\>svg\]\:size-4 { + &>svg { + width: calc(var(--spacing) * 4); + height: calc(var(--spacing) * 4); + } + } .\[\&\>svg\]\:size-6 { &>svg { width: calc(var(--spacing) * 6); height: calc(var(--spacing) * 6); } } + .aria-pressed\:\[\&\>svg\]\:fill-blue-600 { + &[aria-pressed="true"] { + &>svg { + fill: var(--color-blue-600); + } + } + } } :root { --radius: 0.625rem; @@ -1807,9 +2339,6 @@ border-radius: var(--scrollbar-radius); } } - [x-cloak] { - display: none !important; - } } @layer components { .alert, .alert-destructive { @@ -1843,7 +2372,7 @@ &>svg { color: currentcolor; } - h2, h3 { + h2 { grid-column-start: 2; overflow: hidden; display: -webkit-box; @@ -1890,6 +2419,149 @@ } } } +@layer components { + .badge, .badge-primary, .badge-secondary, .badge-destructive, .badge-outline { + display: inline-flex; + width: fit-content; + flex-shrink: 0; + align-items: center; + justify-content: center; + gap: calc(var(--spacing) * 1); + overflow: hidden; + border-radius: calc(infinity * 1px); + border-style: var(--tw-border-style); + border-width: 1px; + padding-inline: calc(var(--spacing) * 2); + padding-block: calc(var(--spacing) * 0.5); + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + white-space: nowrap; + transition-property: color,box-shadow; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + &:focus-visible { + border-color: var(--color-ring); + } + &:focus-visible { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(3px + 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); + } + &:focus-visible { + --tw-ring-color: var(--color-ring); + @supports (color: color-mix(in lab, red, red)) { + --tw-ring-color: color-mix(in oklab, var(--color-ring) 50%, transparent); + } + } + &[aria-invalid="true"] { + border-color: var(--color-destructive); + } + &[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); + } + } + &: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); + } + } + } + &>svg { + pointer-events: none; + } + &>svg { + width: calc(var(--spacing) * 3); + height: calc(var(--spacing) * 3); + } + } + .badge, .badge-primary { + border-color: transparent; + background-color: var(--color-primary); + color: var(--color-primary-foreground); + a& { + &:hover { + @media (hover: hover) { + background-color: var(--color-primary); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-primary) 90%, transparent); + } + } + } + } + } + .badge-secondary { + border-color: transparent; + background-color: var(--color-secondary); + color: var(--color-secondary-foreground); + a& { + &:hover { + @media (hover: hover) { + background-color: var(--color-secondary); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-secondary) 90%, transparent); + } + } + } + } + } + .badge-destructive { + border-color: transparent; + background-color: var(--color-destructive); + color: var(--color-white); + &:focus-visible { + --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); + } + } + &:is(.dark *) { + background-color: var(--color-destructive); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-destructive) 60%, transparent); + } + } + &:is(.dark *) { + &:focus-visible { + --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); + } + } + } + a& { + &:hover { + @media (hover: hover) { + background-color: var(--color-destructive); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-destructive) 90%, transparent); + } + } + } + } + } + .badge-outline { + color: var(--color-foreground); + a& { + &:hover { + @media (hover: hover) { + background-color: var(--color-accent); + } + } + } + a& { + &:hover { + @media (hover: hover) { + color: var(--color-accent-foreground); + } + } + } + } +} @layer components { .btn, .btn-primary, .btn-secondary, .btn-outline, .btn-ghost, .btn-link, .btn-destructive, .btn-sm, .btn-sm-primary, .btn-sm-secondary, .btn-sm-outline, .btn-sm-ghost, .btn-sm-link, .btn-sm-destructive, .btn-lg, .btn-lg-primary, .btn-lg-secondary, .btn-lg-outline, .btn-lg-ghost, .btn-lg-link, .btn-lg-destructive, .btn-icon, .btn-icon-primary, .btn-icon-secondary, .btn-icon-outline, .btn-icon-ghost, .btn-icon-link, .btn-icon-destructive, .btn-sm-icon, .btn-sm-icon-primary, .btn-sm-icon-secondary, .btn-sm-icon-outline, .btn-sm-icon-ghost, .btn-sm-icon-link, .btn-sm-icon-destructive, .btn-lg-icon, .btn-lg-icon-primary, .btn-lg-icon-secondary, .btn-lg-icon-outline, .btn-lg-icon-ghost, .btn-lg-icon-link, .btn-lg-icon-destructive { display: inline-flex; @@ -2112,146 +2784,52 @@ } } } -@layer components { - .badge, .badge-primary, .badge-secondary, .badge-destructive, .badge-outline { - display: inline-flex; - width: fit-content; +.button-group { + isolation: isolate; + display: inline-flex; + width: fit-content; + align-items: stretch; + > *:focus-visible, > :is(.dropdown-menu, .popover, .select) > button:focus-visible { + position: relative; + z-index: 10; + } + > hr[role='separator'] { + margin: calc(var(--spacing) * 0); + height: auto; + width: calc(var(--spacing) * 0); flex-shrink: 0; - align-items: center; - justify-content: center; - gap: calc(var(--spacing) * 1); - overflow: hidden; - border-radius: var(--radius-md); + align-self: stretch; border-style: var(--tw-border-style); border-width: 1px; - padding-inline: calc(var(--spacing) * 2); - padding-block: calc(var(--spacing) * 0.5); - font-size: var(--text-xs); - line-height: var(--tw-leading, var(--text-xs--line-height)); - --tw-font-weight: var(--font-weight-medium); - font-weight: var(--font-weight-medium); - white-space: nowrap; - transition-property: color,box-shadow; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - &:focus-visible { - border-color: var(--color-ring); + border-color: var(--color-input); + } + &:not([data-orientation='vertical']) { + > *:not(:first-child), > :is(.dropdown-menu, .popover, .select):not(:first-child) > button { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left-style: var(--tw-border-style); + border-left-width: 0px; } - &:focus-visible { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(3px + 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); - } - &:focus-visible { - --tw-ring-color: var(--color-ring); - @supports (color: color-mix(in lab, red, red)) { - --tw-ring-color: color-mix(in oklab, var(--color-ring) 50%, transparent); - } - } - &[aria-invalid="true"] { - border-color: var(--color-destructive); - } - &[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); - } - } - &: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); - } - } - } - &>svg { - pointer-events: none; - } - &>svg { - width: calc(var(--spacing) * 3); - height: calc(var(--spacing) * 3); + > *:not(:last-child), > :is(.dropdown-menu, .popover, .select):not(:last-child) > button { + border-top-right-radius: 0; + border-bottom-right-radius: 0; } } - .badge, .badge-primary { - border-color: transparent; - background-color: var(--color-primary); - color: var(--color-primary-foreground); - a& { - &:hover { - @media (hover: hover) { - background-color: var(--color-primary); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-primary) 90%, transparent); - } - } - } + &[data-orientation='vertical'] { + flex-direction: column; + > hr[role='separator'] { + height: 1px; + width: auto; } - } - .badge-secondary { - border-color: transparent; - background-color: var(--color-secondary); - color: var(--color-secondary-foreground); - a& { - &:hover { - @media (hover: hover) { - background-color: var(--color-secondary); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-secondary) 90%, transparent); - } - } - } + > *:not(:first-child), > :is(.dropdown-menu, .popover, .select):not(:first-child) > button { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-top-style: var(--tw-border-style); + border-top-width: 0px; } - } - .badge-destructive { - border-color: transparent; - background-color: var(--color-destructive); - color: var(--color-white); - &:focus-visible { - --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); - } - } - &:is(.dark *) { - background-color: var(--color-destructive); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-destructive) 60%, transparent); - } - } - &:is(.dark *) { - &:focus-visible { - --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); - } - } - } - a& { - &:hover { - @media (hover: hover) { - background-color: var(--color-destructive); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-destructive) 90%, transparent); - } - } - } - } - } - .badge-outline { - color: var(--color-foreground); - a& { - &:hover { - @media (hover: hover) { - background-color: var(--color-accent); - } - } - } - a& { - &:hover { - @media (hover: hover) { - color: var(--color-accent-foreground); - } - } + > *:not(:last-child), > :is(.dropdown-menu, .popover, .select):not(:last-child) > button { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } } } @@ -2309,7 +2887,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; @@ -2419,7 +2997,7 @@ } } @layer components { - .dialog { + .command-dialog { inset-block: calc(var(--spacing) * 0); opacity: 0%; transition-property: all; @@ -2431,7 +3009,7 @@ &::backdrop { opacity: 100%; } - > article { + > * { --tw-scale-x: 100%; --tw-scale-y: 100%; --tw-scale-z: 100%; @@ -2442,7 +3020,7 @@ &::backdrop { opacity: 0%; } - > article { + > * { --tw-scale-x: 95%; --tw-scale-y: 95%; --tw-scale-z: 95%; @@ -2461,7 +3039,320 @@ transition-duration: var(--tw-duration, var(--default-transition-duration)); transition-behavior: allow-discrete; } - > article { + > * { + position: fixed; + top: 50%; + left: 50%; + z-index: 50; + display: flex; + max-height: min(640px, calc(100% - 2rem)); + width: 100%; + max-width: calc(100% - 2rem); + --tw-translate-x: calc(calc(1/2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + --tw-translate-y: calc(calc(1/2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + flex-direction: column; + overflow: hidden; + border-radius: var(--radius-lg); + border-style: var(--tw-border-style); + border-width: 1px; + background-color: var(--color-background); + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + @media (width >= 40rem) { + max-width: var(--container-lg); + } + --tw-scale-x: 95%; + --tw-scale-y: 95%; + --tw-scale-z: 95%; + scale: var(--tw-scale-x) var(--tw-scale-y); + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + > button, > form[method='dialog'] { + position: absolute; + top: calc(var(--spacing) * 4); + right: calc(var(--spacing) * 4); + } + > button, > form[method='dialog'] > button { + border-radius: var(--radius-xs); + opacity: 70%; + --tw-ring-offset-color: var(--color-background); + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + &:hover { + @media (hover: hover) { + opacity: 100%; + } + } + &:focus { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + 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); + } + &:focus { + --tw-ring-color: var(--color-ring); + } + &:focus { + --tw-ring-offset-width: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + } + &:focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + &:disabled { + pointer-events: none; + } + &[data-state="open"] { + background-color: var(--color-accent); + } + &[data-state="open"] { + color: var(--color-muted-foreground); + } + & svg { + pointer-events: none; + } + & svg { + flex-shrink: 0; + } + & svg:not([class*='size-']) { + width: calc(var(--spacing) * 4); + height: calc(var(--spacing) * 4); + } + } + } + .command { + > header input { + height: calc(var(--spacing) * 12); + } + [role='menu'] { + padding-inline: calc(var(--spacing) * 2); + [role='menuitem'] { + padding-inline: calc(var(--spacing) * 2); + padding-block: calc(var(--spacing) * 3); + & svg { + width: calc(var(--spacing) * 5); + height: calc(var(--spacing) * 5); + } + } + [role='heading'] { + padding-inline: calc(var(--spacing) * 2); + padding-block: calc(var(--spacing) * 1.5); + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + color: var(--color-muted-foreground); + } + } + } + } + .command { + > header { + display: flex; + align-items: center; + gap: calc(var(--spacing) * 2); + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + padding-inline: calc(var(--spacing) * 3); + svg { + width: calc(var(--spacing) * 4); + height: calc(var(--spacing) * 4); + flex-shrink: 0; + opacity: 50%; + } + input { + display: flex; + height: calc(var(--spacing) * 10); + width: 100%; + border-radius: var(--radius-md); + background-color: transparent; + padding-block: calc(var(--spacing) * 3); + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + &::placeholder { + color: var(--color-muted-foreground); + } + &:disabled { + cursor: not-allowed; + } + &:disabled { + opacity: 50%; + } + } + } + [role='menu'] { + max-height: 300px; + overflow-x: hidden; + overflow-y: auto; + padding: calc(var(--spacing) * 1); + [role='menuitem'] { + position: relative; + display: flex; + width: 100%; + cursor: default; + align-items: center; + gap: calc(var(--spacing) * 2); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border-radius: var(--radius-sm); + padding-inline: calc(var(--spacing) * 2); + padding-block: calc(var(--spacing) * 1.5); + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + -webkit-user-select: none; + user-select: none; + &:disabled { + pointer-events: none; + } + &:disabled { + opacity: 50%; + } + &[aria-disabled="true"] { + pointer-events: none; + } + &[aria-disabled="true"] { + opacity: 50%; + } + &[aria-hidden="true"] { + display: none; + } + & svg { + pointer-events: none; + } + & svg { + flex-shrink: 0; + } + & svg:not([class*='size-']) { + width: calc(var(--spacing) * 4); + height: calc(var(--spacing) * 4); + } + & svg:not([class*='text-']) { + color: var(--color-muted-foreground); + } + &.active { + background-color: var(--color-accent); + color: var(--color-accent-foreground); + } + } + [role='heading'] { + display: flex; + padding-inline: calc(var(--spacing) * 2); + padding-block: calc(var(--spacing) * 1.5); + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + color: var(--color-muted-foreground); + } + [role='group']:not(:has([role='menuitem']:not([aria-hidden='true']))) { + display: none; + } + [role='separator'] { + margin-inline: calc(var(--spacing) * -1); + margin-block: calc(var(--spacing) * 1); + border-color: var(--color-border); + } + &:not(:has([data-value]:not([aria-hidden='true'])))::before { + margin: calc(var(--spacing) * -1); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-inline: calc(var(--spacing) * 3); + padding-block: calc(var(--spacing) * 6); + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + &[data-empty]:not(:has([data-value]:not([aria-hidden='true'])))::before { + --tw-content: attr(data-empty); + content: var(--tw-content); + } + &:not([data-empty]):not(:has([data-value]:not([aria-hidden='true'])))::before { + --tw-content: 'No results found'; + content: var(--tw-content); + } + } + &:not([data-command-initialized]) [role='menuitem'] { + &:hover { + @media (hover: hover) { + background-color: var(--color-accent); + } + } + &:hover { + @media (hover: hover) { + color: var(--color-accent-foreground); + } + } + } + &:has(> header input:not(:placeholder-shown)) [role='separator'] { + display: none; + } + } +} +@layer components { + .dialog { + inset-block: calc(var(--spacing) * 0); + opacity: 0%; + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + transition-behavior: allow-discrete; + &:is([open],:popover-open) { + opacity: 100%; + &::backdrop { + opacity: 100%; + } + > * { + --tw-scale-x: 100%; + --tw-scale-y: 100%; + --tw-scale-z: 100%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + @starting-style { + opacity: 0%; + &::backdrop { + opacity: 0%; + } + > * { + --tw-scale-x: 95%; + --tw-scale-y: 95%; + --tw-scale-z: 95%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + } + } + &::backdrop { + background-color: color-mix(in srgb, #000 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 50%, transparent); + } + opacity: 0%; + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + transition-behavior: allow-discrete; + } + > * { position: fixed; top: 50%; left: 50%; @@ -2690,7 +3581,117 @@ } } @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); + } + } + .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; + } + h2, h3 { + display: flex; + width: fit-content; + align-items: center; + gap: calc(var(--spacing) * 2); + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + --tw-leading: var(--leading-snug); + line-height: var(--leading-snug); + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + [role="alert"] { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + --tw-font-weight: var(--font-weight-normal); + font-weight: var(--font-weight-normal); + color: var(--color-destructive); + ul { + margin-left: calc(var(--spacing) * 4); + display: flex; + list-style-type: disc; + flex-direction: column; + gap: calc(var(--spacing) * 1); + } + } + section { + display: flex; + flex: 1; + flex-direction: column; + gap: calc(var(--spacing) * 1.5); + --tw-leading: var(--leading-snug); + line-height: var(--leading-snug); + } + &[data-orientation='horizontal'] { + flex-direction: row; + align-items: center; + &:has(>section) { + align-items: flex-start; + } + & p { + text-wrap: balance; + } + &:has(>section) { + &>[type=checkbox],[type=radio] { + margin-top: 1px; + } + } + &>label { + flex: auto; + } + } + } + .fieldset legend + p, .field > p, .field section > p { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + --tw-leading: var(--leading-normal); + line-height: var(--leading-normal); + --tw-font-weight: var(--font-weight-normal); + font-weight: var(--font-weight-normal); + color: var(--color-muted-foreground); + &>a { + text-decoration-line: underline; + } + &>a { + text-underline-offset: 4px; + } + &>a:hover { + color: var(--color-primary); + } + } + .fieldset legend + p { + margin-top: calc(var(--spacing) * -1.5); + } + .field > p { + &:last-child { + margin-top: calc(var(--spacing) * 0); + } + &:nth-last-child(2) { + margin-top: calc(var(--spacing) * -1); + } + } +} +@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%; @@ -2803,7 +3804,34 @@ } } @layer components { - .form label, .label { + .kbd { + pointer-events: none; + display: inline-flex; + height: calc(var(--spacing) * 5); + width: fit-content; + min-width: calc(var(--spacing) * 5); + align-items: center; + justify-content: center; + gap: calc(var(--spacing) * 1); + border-radius: var(--radius-sm); + background-color: var(--color-muted); + padding-inline: calc(var(--spacing) * 1); + font-family: var(--font-sans); + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + color: var(--color-muted-foreground); + -webkit-user-select: none; + user-select: none; + & svg:not([class*='size-']) { + width: calc(var(--spacing) * 3); + height: calc(var(--spacing) * 3); + } + } +} +@layer components { + :is(.form, .field) label, .label { display: flex; align-items: center; gap: calc(var(--spacing) * 2); @@ -2927,7 +3955,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); @@ -3006,7 +4034,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; @@ -3116,7 +4144,7 @@ } } @layer components { - .form select, select.select { + :is(.form, .field) select, select.select { display: flex; height: calc(var(--spacing) * 9); width: fit-content; @@ -3349,6 +4377,9 @@ content: var(--tw-content); } } + [data-popover]:has(> header input:not(:placeholder-shown)) [role='separator'] { + display: none; + } &:not([data-select-initialized]) [data-popover] [role='option'] { &:hover { @media (hover: hover) { @@ -3718,7 +4749,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); @@ -4018,7 +5049,7 @@ } } @layer components { - .form textarea, .textarea { + :is(.form, .field) textarea, .textarea { display: flex; field-sizing: content; min-height: calc(var(--spacing) * 16); @@ -4448,6 +5479,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); diff --git a/docs/src/components/accordion.njk b/docs/src/components/accordion.njk index 31bf87f..f9de3ee 100644 --- a/docs/src/components/accordion.njk +++ b/docs/src/components/accordion.njk @@ -11,7 +11,7 @@ toc:
{% lucide "circle-alert" %} -

There is no dedicated accordion component in Basecoat.

+

There is no dedicated Accordion component in Basecoat.

{% set code %}
@@ -72,4 +72,4 @@ toc:

Usage

-

Basecoat animates <details> elements by default. The example above uses Alpine.js to handle the open/close state along with some Tailwind CSS classes.

\ No newline at end of file +

Basecoat already animates <details> 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.

\ No newline at end of file diff --git a/docs/src/components/alert-dialog.njk b/docs/src/components/alert-dialog.njk index 2b0d83c..dde193d 100644 --- a/docs/src/components/alert-dialog.njk +++ b/docs/src/components/alert-dialog.njk @@ -43,7 +43,7 @@ toc:

The Alert Dialog component is identical to Dialog except for two small differences: we do not display a close button and we do not let the user close the dialog when clicking on the backdrop (no onclick attribute on the <dialog> or the <article> elements).

-{{ code_block(code_html | prettyHtml, class="") }} +{{ code_block(code_html | prettyHtml) }}

The component has the following HTML structure:

diff --git a/docs/src/components/alert.njk b/docs/src/components/alert.njk index ea37877..4d6064d 100644 --- a/docs/src/components/alert.njk +++ b/docs/src/components/alert.njk @@ -19,30 +19,63 @@ toc: {% from "macros/code_preview.njk" import code_preview %} {% from "macros/code_block.njk" import code_block %} -{% set code %}
- {% lucide "circle-check" %} -

Success! Your changes have been saved

-
This is an alert with icon, title and description.
-
{% endset %} -{{ code_preview("alert", code) }} +{% set code %} +
+
+ {% lucide "circle-check" %} +

Success! Your changes have been saved

+
This is an alert with icon, title and description.
+
+
+ {% lucide "popcorn" %} +

This Alert has a title and an icon. No description.

+
This is an alert with icon, title and description.
+
+
+ {% lucide "circle-check" %} +

Unable to process your payment.

+
+

Please verify your billing information and try again.

+
    +
  • Check your card details
  • +
  • Ensure sufficient funds
  • +
  • Verify billing address
  • +
+
+
+
+{% endset %} +{{ code_preview("alert", code | prettyHtml) }}

Usage

-

You can use one of the two classes: alert or alert-dialog.

+

Use the alert or alert-destructive.

-{{ code_block(code) }} +{% set code_simple %} +
+ {% lucide "circle-check" %} +

Success! Your changes have been saved

+
This is an alert with icon, title and description.
+
+{% endset %} +{{ code_block(code_simple | prettyHtml) }}

The component has the following HTML structure:

-
<svg> Optional
-
The icon.
-
<h2>
-
The title.
-
<section> Optional
-
The description.
+
<div class="alert">
+
Main container. Use alert for default styling or alert-destructive for error states. +
+
<svg> Optional
+
The icon.
+
<h2>
+
The title.
+
<section> Optional
+
The description.
+
+
diff --git a/docs/src/components/avatar.njk b/docs/src/components/avatar.njk index 771d863..c9c09c8 100644 --- a/docs/src/components/avatar.njk +++ b/docs/src/components/avatar.njk @@ -12,12 +12,29 @@ toc:
{% lucide "circle-alert" %} -

There is no dedicated avatar component in Basecoat.

+

There is no dedicated Avatar component in Basecoat.

-{% set code %}@hunvreus{% endset %} -{{ code_preview("avatar", code, "max-w-md") }} +{% set code %} +
+ @hunvreus + @shadcn +
+ @hunvreus + @shadcn + @adamwathan +
+
+{% endset %} +{{ code_preview("avatar", code | prettyHtml, "w-full max-w-md") }}

Usage

-

Avatars are fairly simple to implement with Tailwind. The example above shows how to implement one with Tailwind and an <img> element. More examples can be found in the kitchen sink.

\ No newline at end of file +
+

Avatars are just <img> elements styled with Tailwind utility classes.

+
+ +{% set code_simple %} + @hunvreus +{% endset %} +{{ code_block(code_simple | prettyHtml, "html") }} \ No newline at end of file diff --git a/docs/src/components/badge.njk b/docs/src/components/badge.njk index ef1f67a..5eff349 100644 --- a/docs/src/components/badge.njk +++ b/docs/src/components/badge.njk @@ -25,7 +25,25 @@ toc: {% from "macros/code_preview.njk" import code_preview %} {% from "macros/code_block.njk" import code_block %} -{% set code %}Badge{% endset %} +{% set code %} +
+
+ Badge + Secondary + Destructive + Outline +
+
+ + {% lucide "badge-check" %} + Verified + + 8 + 99 + 20+ +
+
+{% endset %} {{ code_preview("badge", code) }}

Usage

diff --git a/docs/src/components/breadcrumb.njk b/docs/src/components/breadcrumb.njk index 2b64ba0..48fe503 100644 --- a/docs/src/components/breadcrumb.njk +++ b/docs/src/components/breadcrumb.njk @@ -13,7 +13,7 @@ toc:
{% lucide "circle-alert" %} -

There is no dedicated breadcrumb component in Basecoat.

+

There is no dedicated Breadcrumb component in Basecoat.

{% set code_html %} @@ -54,7 +54,18 @@ toc:

Usage

-

This example uses some simple Tailwind CSS styles along with the Dropdown Menu component (which may require JavaScript).

+

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 Dropdown Menu component.

-{{ code_block(code_html | prettyHtml, class="") }} \ No newline at end of file +{% set code_simple %} +
    +
  1. + Home +
  2. +
  3. {% lucide "chevron-right", { "class": "size-3.5" } %}
  4. +
  5. + Components +
  6. +
+{% endset %} +{{ code_block(code_simple | prettyHtml, "html") }} \ No newline at end of file diff --git a/docs/src/components/button-group.njk b/docs/src/components/button-group.njk new file mode 100644 index 0000000..289babb --- /dev/null +++ b/docs/src/components/button-group.njk @@ -0,0 +1,290 @@ +--- +layout: layouts/page.njk +title: Button Group +description: A container that groups related buttons together with consistent styling. +toc: + - label: Usage + id: usage + - label: Examples + id: examples + children: + - label: Orientation + id: example-orientation + - label: Nested + id: example-nested + - label: Separator + id: example-separator + - label: Input + id: example-input + - label: Dropdown Menu + id: example-dropdown-menu + - label: Select + id: example-select + - label: Popover + id: example-popover +--- + +{% from "dropdown-menu.njk" import dropdown_menu %} +{% from "popover.njk" import popover %} +{% from "select.njk" import select %} +{% from "macros/code_preview.njk" import code_preview %} +{% from "macros/code_block.njk" import code_block %} + +{% set code_default %} +
+ + +
+ + +
+ +
+ + {% set trigger %} + {% lucide "ellipsis" %} + {% endset %} + {% call dropdown_menu( + trigger=trigger, + trigger_attrs={"class": "btn-icon-outline"}, + popover_attrs={"data-align": "end"} + ) %} +
+ {% lucide "mail-check" %} + Mark as Read +
+
+ {% lucide "archive" %} + Archive +
+
+
+ {% lucide "clock" %} + Snooze +
+
+ {% lucide "calendar" %} + Add to Calendar +
+
+ {% lucide "list-filter-plus" %} + Add to List +
+
+ + {% endcall %} +
+{% endset %} + +{{ code_preview("button-group-default", code_default | prettyHtml) }} + +

Usage

+ +

Simply wrap buttons you want to group together in a <div role="group" class="button-group"> element. You can also use the data-orientation="vertical" attribute to stack buttons vertically. You can also group buttons with dropdown menus or popovers (provided the trigger is a button). Finally, you can use a separator to visually divide buttons within a group (for secondary buttons for example).

+ +{% set code_simple %} +
+ + +
+{% endset %} +{{ code_block(code_simple | prettyHtml, "html") }} + +

Examples

+ +

Orientation

+ +
+

Use data-orientation="vertical" to stack buttons vertically.

+
+ +{% set code_orientation %} +
+ + +
+{% endset %} + +{{ code_preview("button-group-orientation", code_orientation | prettyHtml) }} + +

Size

+ +{% set code_size %} +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+{% endset %} + +{{ code_preview("button-group-size", code_size | prettyHtml) }} + +

Nested

+ +{% set code_nested %} +
+
+ + + + + +
+
+ + +
+
+{% endset %} + +{{ code_preview("button-group-nested", code_nested | prettyHtml) }} + +

Separator

+ +
+

Use <hr role="separator"> to visually divide buttons within a group.

+
+ +{% set code_separator %} +
+ +
+ +
+{% endset %} + +{{ code_preview("button-group-separator", code_separator | prettyHtml) }} + + +

Input

+ +{% set code_input %} +
+ + +
+{% endset %} + +{{ code_preview("button-group-input", code_input | prettyHtml) }} + + +

Dropdown Menu

+ +{% set code_dropdown_menu %} +
+ + {% set trigger %} + {% lucide "chevron-down" %} + {% endset %} + {% call dropdown_menu( + trigger=trigger, + trigger_attrs={"class": "btn-icon-outline"}, + popover_attrs={"data-align": "end"} + ) %} +
+ {% lucide "volume-off" %} + Mute Conversation +
+
+ {% lucide "check" %} + Mark as Read +
+
+ {% lucide "triangle-alert" %} + Report Conversation +
+
+ {% lucide "user-round-x" %} + Block User +
+
+ {% lucide "share" %} + Share Conversation +
+
+ {% lucide "copy" %} + Copy Conversation +
+
+ + {% endcall %} +
+{% endset %} + +{{ code_preview("button-group-dropdown-menu", code_dropdown_menu | prettyHtml) }} + +

Select

+ +{% set code_select %} +
+
+ {% call select(trigger_attrs={"class": "[&_[data-name]]:hidden"}) %} +
$ US Dollar
+
€ Euro
+
£ British Pound
+ {% endcall %} + +
+ + +
+{% endset %} + +{{ code_preview("button-group-select", code_select | prettyHtml) }} + +

Popover

+ +{% set code_popover %} +
+ + {% call popover( + trigger=trigger, + trigger_attrs={"class": "btn-icon-outline"}, + popover_attrs={"class": "w-72 p-0", "data-align": "end"} + ) %} +
Agent Tasks
+
+ +

Start a new task with Copilot

+

+ Describe your task in natural language. Copilot will work in the + background and open a pull request for your review. +

+
+ {% endcall %} +
+{% endset %} + +{{ code_preview("button-group-popover", code_popover | prettyHtml) }} \ No newline at end of file diff --git a/docs/src/components/command.njk b/docs/src/components/command.njk new file mode 100644 index 0000000..3f468a3 --- /dev/null +++ b/docs/src/components/command.njk @@ -0,0 +1,261 @@ +--- +layout: layouts/page.njk +title: Command +description: Fast, composable command menu for quick navigation and actions. +toc: + - label: Usage + id: usage + children: + - label: HTML + JavaScript + id: usage-html-js + children: + - label: "Step 1: Include the JavaScript files" + id: usage-html-js-1 + - label: "Step 2: Add your command HTML" + id: usage-html-js-2 + - label: HTML structure + id: usage-html-js-3 + - label: JavaScript events + id: usage-html-js-4 + - label: Jinja and Nunjucks + id: usage-macro + - label: Examples + id: examples + children: + - label: Dialog + id: example-dialog +--- + +{% from "macros/code_preview.njk" import code_preview %} +{% from "macros/code_block.njk" import code_block %} +{% from "command.njk" import command, command_dialog %} + +{% macro command_items(suggestions_id="suggestions", settings_id="settings") %} +
+ Suggestions +
+ {% lucide "calendar" %} + Calendar +
+
+ {% lucide "smile" %} + Search Emoji +
+
+ {% lucide "calculator" %} + Calculator +
+
+
+
+ Settings +
+ {% lucide "user" %} + Profile + ⌘P +
+
+ {% lucide "credit-card" %} + Billing + ⌘B +
+
+ {% lucide "settings" %} + Settings + ⌘S +
+
+{% endmacro %} + +{% set code_standalone %} +{% call command( + id="demo-command-standalone", + main_attrs={ "class": "rounded-lg border shadow-md" } +) %} +{{ command_items("suggestions", "settings") }} +{% endcall %} +{% endset %} + +{{ code_preview("command-standalone", code_standalone | prettyHtml, class=" w-full max-w-[450px]") }} + +

Usage

+ +

HTML + JavaScript

+ +

Step 1: Include the JavaScript files

+ +
+

You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:

+
+ +{% set code_script %} +{% endset %} +{{ code_block(code_script | prettyHtml, "html") }} + + + +

Step 2: Add your command HTML

+ +
+

You can use the command menu standalone:

+
+ +{{ code_block(code_standalone | prettyHtml, "html") }} + +
+

Wrap the command menu in a <dialog class="command-dialog"> to display in a dialog:

+
+ +{% set code_dialog %} + +
+ +
+
+{% endset %} + +{{ code_block(code_dialog | prettyHtml, "html") }} + +

HTML structure

+ +
+
+
<dialog class="command-dialog"> Optional
+
For dialog variant, wraps a <div class="command"> to display as a modal. Uses native <dialog> behavior and requires aria-label or aria-labelledby for accessibility. +
+
<div class="command">
+
Wraps around the entire command component. Can be used standalone or inside a dialog. +
+
<header>
+
Contains the search input to filter command options. +
+
<input type="text">
+
The search input. Should have: +
    +
  • role="combobox": indicates the input controls a menu with filtering
  • +
  • aria-expanded="true": the menu is always expanded
  • +
  • aria-controls="{ MENU_ID }": points to the menu's id
  • +
+
+
+
+
<div role="menu">
+
The menu containing the command items. Should have id="{ MENU_ID }" refered to by the input's aria-controls attribute. +
+
<div role="menuitem">
+
Command item that triggers an action when clicked. In a command-dialog, clicking a menuitem automatically closes the dialog unless the data-keep-command-open attribute is present. Optional attributes: +
    +
  • data-label: custom label for filtering (if different from text content)
  • +
  • data-keywords: additional keywords for enhanced filtering
  • +
  • aria-disabled="true": marks item as disabled (excluded from keyboard navigation and filtering)
  • +
+
+
<hr role="separator"> Optional
+
Separator between groups/items. Hidden when filtering is active.
+
<div role="group"> Optional
+
Group of items, can have aria-labelledby attribute to link to a heading.
+
<span role="heading">
+
Group heading, must have an id attribute if you use the aria-labelledby attribute on the group.
+
+
+
+
+
+
+
+
+ +

JavaScript events

+ +
+
+
basecoat:initialized
+
Once the component is fully initialized, it dispatches a custom (non-bubbling) basecoat:initialized event on itself.
+
+
+ +

Jinja and Nunjucks

+ +
+

You can use the command() or command_dialog() Nunjucks or Jinja macros for this component.

+
+ + + +{% set raw_code %}{% raw %}{% call command() %} +
+ ... + Calendar +
+
+ ... + Settings +
+{% endcall %}{% endraw %}{% endset %} +{{ code_block(raw_code, "jinja") }} + +

Examples

+ +

Dialog

+ +
+

You can use some simple JavaScript to open the command menu with a keyboard shortcut (in this case ⌘J).

+
+ +{% set code_dialog %} + + +{% call command_dialog(id="demo-command-dialog") %} +{{ command_items("cmd-suggestions", "cmd-settings") }} +{% endcall %} + + +{% endset %} + +{{ code_preview("command-dialog", code_dialog | prettyHtml) }} \ No newline at end of file diff --git a/docs/src/components/dialog.njk b/docs/src/components/dialog.njk index a734cec..dafa34c 100644 --- a/docs/src/components/dialog.njk +++ b/docs/src/components/dialog.njk @@ -69,8 +69,8 @@ toc:
  • onclick: closes the dialog when the backdrop is clicked.
  • -
    <article>
    -
    Wraps the content of the dialog. It includes an onclick attribute to prevent from closing the dialog if we have an onclick event on <dialog>. +
    <div>
    +
    Wraps the content of the dialog.
    <header>
    Contains the header for the dialog: diff --git a/docs/src/components/empty.njk b/docs/src/components/empty.njk new file mode 100644 index 0000000..c094e45 --- /dev/null +++ b/docs/src/components/empty.njk @@ -0,0 +1,157 @@ +--- +layout: layouts/page.njk +title: Empty +description: Display empty states with icons, titles, descriptions, and actions. +toc: + - label: Usage + id: usage + - label: Examples + id: examples + children: + - label: Outline + id: example-outline + - label: Avatar + id: example-avatar + - label: Avatar Group + id: example-avatar-group + - label: Input Group + id: example-input-group +--- + +{% from "macros/code_preview.njk" import code_preview %} +{% from "macros/code_block.njk" import code_block %} + +
    + {% lucide "circle-alert" %} +

    There is no dedicated Empty component in Basecoat.

    +
    + +{% set code_default %} +
    +
    +
    + {% lucide "folder-code" %} +
    +

    No Projects Yet

    +

    + You haven't created any projects yet. Get started by creating your first project. +

    +
    +
    +
    + + +
    +
    + + Learn More + {% lucide "arrow-up-right" %} + +
    +{% endset %} + +{{ code_preview("empty-default", code_default) }} + +

    Usage

    + +
    +

    Empty states are pure HTML composition using Tailwind utility classes. Use flex layout utilities to center content, and compose with semantic elements like <header>, <h3>, <p>, and <button>.

    +

    Browse the examples below and copy the pattern that fits your needs.

    +
    + +

    Examples

    + +

    Outline

    + +{% set code_outline %} +
    +
    +
    + {% lucide "cloud" %} +
    +

    Cloud Storage Empty

    +

    + Upload files to your cloud storage to access them anywhere. +

    +
    +
    + +
    +
    +{% endset %} + +{{ code_preview("empty-outline", code_outline) }} + +

    Avatar

    + +{% set code_avatar %} +
    +
    +
    + @shadcn +
    +

    User Offline

    +

    + This user is currently offline. You can leave a message to notify them or try again later. +

    +
    +
    + +
    +
    +{% endset %} + +{{ code_preview("empty-avatar", code_avatar) }} + +

    Avatar Group

    + +{% set code_avatar_group %} +
    +
    +
    +
    + @hunvreus + @shadcn + @adamwathan +
    +
    +

    No Team Members

    +

    + Invite your team to collaborate on this project. +

    +
    +
    + +
    +
    +{% endset %} + +{{ code_preview("empty-avatar-group", code_avatar_group) }} + +

    Input Group

    + +{% set code_input_group %} +
    +
    +

    404 - Not Found

    +

    + The page you're looking for doesn't exist. Try searching for what you need below. +

    +
    +
    +
    + + {% 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" } %} + / +
    +

    + Need help? Contact support +

    +
    +
    +{% endset %} + +{{ code_preview("empty-input-group", code_input_group) }} \ No newline at end of file diff --git a/docs/src/components/field.njk b/docs/src/components/field.njk new file mode 100644 index 0000000..c281856 --- /dev/null +++ b/docs/src/components/field.njk @@ -0,0 +1,459 @@ +--- +layout: layouts/page.njk +title: Field +description: Combine labels, controls, and help text to compose accessible form fields. +toc: + - label: Usage + id: usage + - label: Examples + id: examples + children: + - label: Input + id: example-input + - label: Textarea + id: example-textarea + - label: Select + id: example-select + - label: Fieldset + id: example-fieldset + - label: Checkbox + id: example-checkbox + - label: Radio Group + id: example-radio + - label: Switch + id: example-switch + - label: Responsive Layout + id: example-responsive +--- + +{% from "macros/code_preview.njk" import code_preview %} +{% from "macros/code_block.njk" import code_block %} + +{% set code_demo %} +
    +
    + Payment Method +

    All transactions are secure and encrypted

    + +
    + + +
    + +
    + + +

    Enter your 16-digit card number

    +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + + +
    + Billing Address +

    The billing address associated with your payment method

    + +
    + +
    +
    + +
    +
    + + +
    +
    + +
    + + +
    +
    +{% endset %} + +{{ code_preview("field-demo", code_demo) }} + +

    Usage

    + +
    +

    Field uses semantic HTML with minimal classes. Two main patterns can be used independently or composed together:

    +
      +
    • <div role="group" class="field"> for individual field containers (label, input, helper text, errors)
    • +
    • <fieldset class="fieldset"> for grouping multiple related fields together
    • +
    +

    Elements inside these containers are styled automatically based on their purpose. Use aria-describedby to connect helper text and errors to inputs for accessibility.

    +

    Fields have the following HTML structure:

    +
    +
    <div role="group" class="field">
    +
    Field container. Add data-orientation="horizontal" to align controls horizontally. For responsive layouts, use standard Tailwind responsive classes. +
    +
    <label>
    +
    Label for the input.
    +
    <input>, <select> or <textarea>
    +
    Form input.
    +
    <p> Optional
    +
    Helper text. Use aria-describedby on the input to reference this element's id.
    +
    <p role="alert"> Optional
    +
    Error message. Use aria-describedby on the input and aria-invalid="true" to reference this element's id.
    +
    <section> Optional
    +
    Wraps label and description when the label sits beside the input (e.g. data-orientation="horizontal"). Not required if you have no description.
    +
    +
    +
    +

    Fieldsets have the following HTML structure:

    +
    +
    <fieldset class="fieldset">
    +
    Container for grouping related fields. +
    +
    <legend>
    +
    Heading for the fieldset.
    +
    <p> Optional
    +
    Description text. When placed directly after <legend>, automatically styled as muted text.
    +
    <div role="group" class="field"> Optional
    +
    Individual field containers (see structure above). You can include multiple .field elements, or place form controls directly in the fieldset.
    +
    +
    +
    +
    + +

    Examples

    + +

    Input

    + +{% set code_input %} +
    +
    + + +

    Choose a unique username for your account.

    +
    + +
    + +

    Must be at least 8 characters long.

    + +
    +
    +{% endset %} + +{{ code_preview("field-input", code_input, class="w-full max-w-md") }} + +

    Textarea

    + +{% set code_textarea %} +
    + + +

    Share your thoughts about our service.

    +
    +{% endset %} + +{{ code_preview("field-textarea", code_textarea, class="w-full max-w-md") }} + +

    Select

    + +{% set code_select %} +
    + + +

    Select your department or area of work.

    +
    +{% endset %} + +{{ code_preview("field-select", code_select, class="w-full max-w-md") }} + +

    Slider

    + +{% set code %} +
    + +

    Set your budget: $150

    + +
    + +{% endset %} +{{ code_preview("slider", code, "w-full max-w-sm") }} + +

    Fieldset

    + +
    +

    Group related fields with <fieldset class="fieldset"> and <legend>.

    +
    + +{% set code_fieldset %} +
    +
    + Profile +

    This information will be displayed on your profile

    + +
    + + +

    Your first and last name

    +
    + +
    + + + +
    + +
    + + +
    +
    + + +
    +{% endset %} + +{{ code_preview("field-fieldset", code_fieldset) }} + +

    Checkbox

    + +{% set code_checkbox %} +
    + Show these items on the desktop +

    Select the items you want to show on the desktop.

    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    + {#
    #} + +
    + +

    Your Desktop & Documents folders are being synced with iCloud Drive. You can access them from other devices.

    +
    + {#
    #} +
    +
    +{% endset %} + +{{ code_preview("field-checkbox", code_checkbox, class="w-full max-w-md") }} + +

    Radio Group

    + +{% set code_radio %} +
    + Subscription plan +

    Yearly and lifetime plans offer significant savings.

    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +{% endset %} + +{{ code_preview("field-radio", code_radio, class="w-full max-w-md") }} + +

    Switch

    + +
    +

    Set data-orientation="horizontal" on .field to align labels and controls side-by-side.

    +
    + +{% set code_switch %} +
    +
    + +

    Enable multi-factor authentication. If you do not have a two-factor device, you can use a one-time code sent to your email.

    +
    + +
    +{% endset %} + +{{ code_preview("field-switch", code_switch, class="w-full max-w-md") }} + +

    Choice Card

    + +{% set code_choice_card %} +
    + Compute Environment +

    Select the compute environment for your cluster.

    + +
    + +
    +
    + +
    +
    +{% endset %} + +{{ code_preview("field-choice-card", code_choice_card, class="w-full max-w-md") }} + +

    Responsive Layout

    + +
    +

    For responsive layouts, use standard Tailwind responsive classes directly on .field. This example stacks vertically on mobile and switches to horizontal at the md breakpoint.

    +
    + +{% set code_responsive %} +
    + Profile +

    Keep your profile details up to date.

    + +
    + +
    +
    +
    + +

    Provide your full name for identification.

    +
    + +
    + +
    + +
    +
    + +

    You can write your message here. Keep it short, preferably under 100 characters.

    +
    + +
    + +
    + +
    + + +
    +
    +
    +{% endset %} + +{{ code_preview("field-responsive", code_responsive, class="w-full max-w-3xl") }} \ No newline at end of file diff --git a/docs/src/components/input-group.njk b/docs/src/components/input-group.njk new file mode 100644 index 0000000..590bd9d --- /dev/null +++ b/docs/src/components/input-group.njk @@ -0,0 +1,372 @@ +--- +layout: layouts/page.njk +title: Input Group +description: Display additional information or actions to an input or textarea. +toc: + - label: Usage + id: usage + - label: Examples + id: examples + children: + - label: Icon + id: example-icon + - label: Text + id: example-text + - label: Button + id: example-button + - label: Tooltip + id: example-tooltip + - label: Textarea + id: example-textarea + - label: Spinner + id: example-spinner + - label: Dropdown + id: example-dropdown + - label: Group + id: example-group +--- + +{% from "macros/code_preview.njk" import code_preview %} +{% from "macros/code_block.njk" import code_block %} +{% from "dropdown-menu.njk" import dropdown_menu %} +{% from "popover.njk" import popover %} + +
    + {% lucide "circle-alert" %} +

    There is no dedicated Input Group component in Basecoat.

    +
    + +{% set code_default %} +
    +
    + +
    + {% lucide "search" %} +
    +
    12 results
    +
    + +
    + +
    https://
    +
    {% lucide "info" %}
    +
    + +
    + +
    + + {{ dropdown_menu( + id="demo-dropdown-menu", + trigger="Auto", + trigger_attrs={"class": "btn-sm-ghost text-muted-foreground hover:text-accent-foreground h-6 p-2"}, + popover_attrs={"data-side": "top", "class": "min-w-32"}, + items=[ + { type: "item", label: "Auto" }, + { type: "item", label: "Agent" }, + { type: "item", label: "Manual" } + ] + ) }} +
    52% used
    +
    + +
    +
    + +
    + +
    {% lucide "check" %}
    +
    +
    +{% endset %} + +{{ code_preview("input-group-default", code_default, class="w-full max-w-sm") }} + +

    Usage

    + +
    +

    Input groups are pure HTML composition using Tailwind utility classes. There is no dedicated component: use relative positioning on a wrapper, add your content with absolute positioning, and adjust input padding to make room.

    +

    Browse the examples below and copy the pattern that fits your needs.

    +
    + +

    Examples

    + +

    Icon

    + +{% set code_icon %} +
    +
    + +
    + {% lucide "search" %} +
    +
    + +
    + +
    + {% lucide "mail" %} +
    +
    + +
    + +
    + {% lucide "credit-card" %} +
    +
    + {% lucide "check" %} +
    +
    + +
    + +
    + {% lucide "star" %} + {% lucide "info" %} +
    +
    +
    +{% endset %} + +{{ code_preview("input-group-icon-prefix", code_icon, class="w-full max-w-sm") }} + +

    Text

    + +{% set code_text %} +
    +
    + +
    $
    +
    USD
    +
    + +
    + +
    https://
    +
    .com
    +
    + +
    + +
    @company.com
    +
    + +
    + +
    +
    120 characters left
    +
    +
    +
    +{% endset %} + +{{ code_preview("input-group-text", code_text, class="w-full max-w-sm") }} + +

    Button

    + +{% set code_button %} +
    +
    + + +
    + + +
    + + +
    + {% set trigger %} + {% lucide "info" %} + {% endset %} + {% call popover( + trigger=trigger, + trigger_attrs={"class": "btn-sm-icon-ghost rounded-full size-6"}, + popover_attrs={"class": "max-w-72"} + ) %} +

    Your connection is not secure.

    +

    You should not enter any sensitive information on this site.

    + {% endcall %} +
    https://
    +
    +
    + +
    + + +
    +
    +{% endset %} + +{{ code_preview("input-group-button", code_button, class="w-full max-w-sm") }} + +

    Tooltip

    + +{% set code_tooltip %} +
    +
    + +
    {% lucide "info" %}
    +
    + +
    + +
    {% lucide "circle-help" %}
    +
    + +
    + +
    {% lucide "circle-help" %}
    +
    +
    +{% endset %} + +{{ code_preview("input-group-tooltip", code_tooltip, class="w-full max-w-sm") }} + +

    Textarea

    + +{% set code_textarea %} +
    + +
    + + script.js + + +
    +
    + Line 1, Column 1 + +
    +
    +{% endset %} + +{{ code_preview("input-group-textarea", code_textarea, class="w-full max-w-md") }} + +

    Spinner

    + +{% set code_spinner %} +
    +
    + +
    + {% lucide "loader-circle", { "class": "animate-spin" } %} +
    +
    + +
    + +
    + {% lucide "loader-circle", { "class": "animate-spin" } %} +
    +
    + +
    + +
    +
    Saving...
    +
    + {% lucide "loader-circle", { "class": "animate-spin" } %} +
    +
    +
    + +
    + +
    + {% lucide "loader", { "class": "animate-spin" } %} +
    +
    + Please wait... +
    +
    +
    +{% endset %} + +{{ code_preview("input-group-spinner", code_spinner, class="w-full max-w-md") }} + +

    Dropdown

    + +{% set code_dropdown %} +
    +
    + + {% set trigger %} + {% lucide "ellipsis" %} + {% endset %} + {{ dropdown_menu( + main_attrs={"class": "absolute right-1.5 top-1/2 -translate-y-1/2 z-10"}, + trigger=trigger, + trigger_attrs={"class": "btn-sm-icon-ghost text-muted-foreground hover:text-accent-foreground size-6"}, + popover_attrs={"data-align": "end", "class": "min-w-32"}, + items=[ + { type: "item", label: "Settings" }, + { type: "item", label: "Copy path" }, + { type: "item", label: "Open location" } + ] + ) }} +
    + +
    + + {% set trigger %} + Search in... + {% lucide "chevron-down", { "class": "size-3" } %} + {% endset %} + {{ dropdown_menu( + main_attrs={"class": "absolute right-1.5 top-1/2 -translate-y-1/2"}, + trigger=trigger, + trigger_attrs={"class": "btn-sm-ghost text-muted-foreground hover:text-accent-foreground p-2 h-6"}, + popover_attrs={"data-align": "end", "class": "min-w-32"}, + items=[ + { type: "item", label: "Documentation" }, + { type: "item", label: "Blog Posts" }, + { type: "item", label: "Changelog" } + ] + ) }} +
    +
    +{% endset %} + +{{ code_preview("input-group-dropdown", code_dropdown, class="w-full max-w-sm") }} + +

    Group

    + +{% set code_group %} +
    + +
    + +
    {% lucide "link-2" %}
    +
    +
    .com
    +
    +{% endset %} + +{{ code_preview("input-group-group", code_group, class="w-full max-w-sm") }} \ No newline at end of file diff --git a/docs/src/components/item.njk b/docs/src/components/item.njk new file mode 100644 index 0000000..eacbaeb --- /dev/null +++ b/docs/src/components/item.njk @@ -0,0 +1,320 @@ +--- +layout: layouts/page.njk +title: Item +description: A versatile component that you can use to display any content. +toc: + - label: Usage + id: usage + - label: Examples + id: examples + children: + - label: Variants + id: example-variants + - label: Size + id: example-size + - label: Icon + id: example-icon + - label: Avatar + id: example-avatar + - label: Image + id: example-image + - label: Group + id: example-group + - label: Header + id: example-header + - label: Link + id: example-link +--- + +{% from "macros/code_block.njk" import code_block %} +{% from "macros/code_preview.njk" import code_preview %} + +
    + {% lucide "circle-alert" %} +

    There is no dedicated Item component in Basecoat.

    +
    + +{% set code_basic %} +
    +
    +
    +

    Basic Item

    +

    A simple item with title and description.

    +
    + +
    + + +
    + {% lucide "badge-check" %} +
    +
    +

    Your profile has been verified.

    +
    +
    + {% lucide "chevron-right" %} +
    +
    +
    +{% endset %} + +{{ code_preview("item-basic", code_basic | prettyHtml, class="w-full max-w-md") }} + +

    Usage

    + +
    +

    Items are pure HTML composition using Tailwind utility classes. Use semantic elements like <article> or <a>, apply flex layout, and compose with standard HTML elements like <h3>, <p>, <img>, or <button>.

    +

    Browse the examples below and copy the pattern that fits your needs.

    +
    + +

    Examples

    + +

    Variants

    + +{% set code_variants %} +
    +
    +
    +

    Default Variant

    +

    Standard styling with subtle background and borders.

    +
    + +
    + +
    +
    +

    Outline Variant

    +

    Outlined style with clear borders and transparent background.

    +
    + +
    + +
    +
    +

    Muted Variant

    +

    Subdued appearance with muted colors for secondary content.

    +
    + +
    +
    +{% endset %} + +{{ code_preview("item-variants", code_variants) }} + +

    Size

    + +
    +

    Use different padding and gap sizes for compact or spacious layouts.

    +
    + +{{ code_preview("item-size", code_basic) }} + +

    Icon

    + +{% set code_icon %} +
    +
    + {% lucide "shield-alert" %} +
    +
    +

    Security Alert

    +

    New login detected from unknown device.

    +
    + +
    +{% endset %} + +{{ code_preview("item-icon", code_icon, class="max-w-lg") }} + + +

    Avatar

    + +{% set code_avatar %} +
    +
    + hunvreus +
    +

    hunvreus

    +

    Last seen 5 months ago

    +
    + +
    + +
    +
    + @hunvreus + @shadcn + @adamwathan +
    +
    +

    No Team Members

    +

    Invite your team to collaborate on this project.

    +
    + +
    +
    +{% endset %} + +{{ code_preview("item-avatar", code_avatar, class="w-full max-w-lg") }} + +

    Image

    + +{% set code_image %} + +{% endset %} + +{{ code_preview("item-image", code_image) }} + + +

    Group

    + +{% set code_group %} +
    +
    + Ronan Berder +
    +

    hunvreus

    +

    hunvreus@gmail.com

    +
    + +
    + + + +
    + shadcn +
    +

    shadcn

    +

    shadcn@vercel.com

    +
    + +
    + + + +
    + Adam Wathan +
    +

    adamwathan

    +

    adam.wathan@gmail.com

    +
    + +
    +
    +{% endset %} + +{{ code_preview("item-group", code_group, class="w-full max-w-md") }} + + +

    Header

    + +{% set code_header %} +
    +
    + v0-1.5-sm +
    +

    v0-1.5-sm

    +

    Everyday tasks and UI generation.

    +
    +
    + +
    + v0-1.5-lg +
    +

    v0-1.5-lg

    +

    Advanced thinking or reasoning.

    +
    +
    + +
    + v0-2.0-mini +
    +

    v0-2.0-mini

    +

    Open Source model for everyone.

    +
    +
    +
    +{% endset %} + +{{ code_preview("item-header", code_header) }} + + + +
    +

    Items can be rendered as links by using an <a> element. Hover and focus states are applied automatically.

    +
    + +{% set code_link %} + +{% endset %} + +{{ code_preview("item-link", code_link, class="w-full max-w-md") }} \ No newline at end of file diff --git a/docs/src/components/kbd.njk b/docs/src/components/kbd.njk new file mode 100644 index 0000000..da60653 --- /dev/null +++ b/docs/src/components/kbd.njk @@ -0,0 +1,76 @@ +--- +layout: layouts/page.njk +title: Kbd +description: Used to display textual user input from keyboard. +toc: + - label: Usage + id: usage + - label: Examples + id: examples + children: + - label: Group + id: example-group + - label: Button + id: example-button +--- + +{% from "macros/code_preview.njk" import code_preview %} +{% from "macros/code_block.njk" import code_block %} + +{% set code_html %} +
    + + ⌘ + ⇧ + ⌥ + ⌃ + + + Ctrl + + + B + +
    +{% endset %} + +{{ code_preview("kbd", code_html | prettyHtml) }} + +

    Usage

    + +
    +

    Use the <kbd> element with the kbd class:

    +
    + +{% set code_simple %} +⌘K +{% endset %} + +{{ code_block(code_simple | prettyHtml, "html") }} + +

    Examples

    + +

    Group

    + +{% set group_html %} +

    + Use + + Ctrl + B + Ctrl + K + + to open the command palette +

    +{% endset %} + +{{ code_preview("kbd-group", group_html | prettyHtml) }} + +

    Button

    + +{% set button_html %} +
    + + +
    +{% endset %} + +{{ code_preview("kbd-button", button_html | prettyHtml) }} \ No newline at end of file diff --git a/docs/src/components/progress.njk b/docs/src/components/progress.njk new file mode 100644 index 0000000..b86fb5a --- /dev/null +++ b/docs/src/components/progress.njk @@ -0,0 +1,44 @@ +--- +layout: layouts/page.njk +title: Progress +description: Displays an indicator showing the completion progress of a task, typically displayed as a progress bar. +toc: + - label: Usage + id: usage +--- + +{% from "macros/code_preview.njk" import code_preview %} +{% from "macros/code_block.njk" import code_block %} + +
    + {% lucide "circle-alert" %} +

    There is no dedicated Progress component in Basecoat.

    +
    + +{% set code_default %} +
    +
    +
    + +{% endset %} + +{{ code_preview("progress-default", code_default, class="w-full max-w-sm") }} + +

    Usage

    + +
    +

    Progress bars are pure HTML composition using Tailwind utility classes. Use a container div for the track and an inner div for the indicator. Set the indicator's width with inline styles or via JavaScript.

    +
    + +{% set code_usage %} +
    +
    +
    +{% endset %} + +{{ code_block(code_usage) }} + diff --git a/docs/src/components/spinner.njk b/docs/src/components/spinner.njk new file mode 100644 index 0000000..70bc2fe --- /dev/null +++ b/docs/src/components/spinner.njk @@ -0,0 +1,214 @@ +--- +layout: layouts/page.njk +title: Spinner +description: An indicator that can be used to show a loading state. +toc: + - label: Usage + id: usage + - label: Examples + id: examples + children: + - label: Size + id: example-size + - label: Color + id: example-color + - label: Button + id: example-button + - label: Badge + id: example-badge + - label: Input Group + id: example-input-group + - label: Empty + id: example-empty + - label: Item + id: example-item +--- + +{% from "macros/code_preview.njk" import code_preview %} +{% from "macros/code_block.njk" import code_block %} + +
    + {% lucide "circle-alert" %} +

    There is no dedicated Spinner component in Basecoat.

    +
    + +{% set code_default %} +
    +
    + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %} +
    +
    +

    Processing payment...

    +
    +
    +

    $100.00

    +
    +
    +{% endset %} + +{{ code_preview("spinner-default", code_default, class="w-full max-w-xs") }} + +

    Usage

    + +
    +

    Spinners are pure HTML using the loader-circle Lucide icon with the animate-spin Tailwind utility. Add role="status" and aria-label="Loading" for accessibility.

    +

    Browse the examples below and copy the pattern that fits your needs.

    +
    + +

    Examples

    + +

    Size

    + +
    +

    Use the size-* utility class to change the size of the spinner.

    +
    + +{% set code_size %} +
    + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-3 animate-spin" } %} + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-4 animate-spin" } %} + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-6 animate-spin" } %} + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-8 animate-spin" } %} +
    +{% endset %} + +{{ code_preview("spinner-size", code_size) }} + +

    Color

    + +
    +

    Use the text-* utility class to change the color of the spinner.

    +
    + +{% set code_color %} +
    + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-6 animate-spin text-red-500" } %} + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-6 animate-spin text-green-500" } %} + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-6 animate-spin text-blue-500" } %} + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-6 animate-spin text-yellow-500" } %} + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-6 animate-spin text-purple-500" } %} +
    +{% endset %} + +{{ code_preview("spinner-color", code_color) }} + +

    Button

    + +
    +

    Add a spinner to a button to indicate a loading state.

    +
    + +{% set code_button %} +
    + + + +
    +{% endset %} + +{{ code_preview("spinner-button", code_button) }} + +

    Badge

    + +{% set code_badge %} +
    + + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %} + Syncing + + + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %} + Updating + + + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %} + Processing + +
    +{% endset %} + +{{ code_preview("spinner-badge", code_badge) }} + +

    Input Group

    + +
    +

    Input groups can have spinners positioned inside them.

    +
    + +{% set code_input_group %} +
    +
    + +
    + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %} +
    +
    + +
    + +
    + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "size-4 animate-spin text-muted-foreground" } %} + Validating... + +
    +
    +
    +{% endset %} + +{{ code_preview("spinner-input-group", code_input_group, class="w-full max-w-md") }} + +

    Empty

    + +
    +

    Use a spinner in empty states to indicate a loading or processing state.

    +
    + +{% set code_empty %} +
    +
    +
    + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin size-4" } %} +
    +

    Processing your request

    +

    Please wait while we process your request. Do not refresh the page.

    +
    +
    + +
    +
    +{% endset %} + +{{ code_preview("spinner-empty", code_empty) }} + +

    Item

    + +{% set code_item %} +
    +
    + {% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin size-4 text-muted-foreground" } %} +
    +
    +

    Downloading...

    +

    129 MB / 1000 MB

    +
    + +
    +
    +
    +
    +
    +
    +{% endset %} + +{{ code_preview("spinner-item", code_item, class="w-full max-w-md") }} \ No newline at end of file diff --git a/docs/src/installation.njk b/docs/src/installation.njk index d4eab7b..b13cb70 100644 --- a/docs/src/installation.njk +++ b/docs/src/installation.njk @@ -29,8 +29,12 @@ toc: id: install-cli - label: Use Nunjucks or Jinja macros id: install-macros + - label: Icons + id: install-icons - label: Theming id: install-theming + - label: Customization + id: install-customization --- {% from "macros/code_block.njk" import code_block %} @@ -192,6 +196,33 @@ import 'basecoat-css/tabs';{% endraw %}{% endset %} {% endcall %}{% endraw %}{% endset %} {{ code_block(code, "jinja") }} +

    Icons

    + +
    +

    Basecoat uses Lucide icons. You have three options:

    + +
      +
    • Copy SVG code: Visit lucide.dev/icons, click any icon to copy the SVG, and paste it directly in your HTML. Best for simple projects or occasional icon usage.
    • +
    • Install lucide: Follow the installation guide to install the lucide package and dynamically render icons. Best for projects that need many icons or want tree-shaking.
    • +
    • Use framework packages: Browse framework-specific packages (React, Vue, Angular, etc.) for component-based icon usage. Best if you're using a supported framework.
    • +
    +
    + + +

    Theming

    diff --git a/package-lock.json b/package-lock.json index 525ffcb..df412f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" } } diff --git a/package.json b/package.json index 3fd1c46..450f439 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "basecoat", - "version": "0.3.2", + "version": "0.3.3", "description": "shadcn/ui for the rest of us", "author": "hunvreus", "type": "module", @@ -21,8 +21,7 @@ "css", "html", "jinja", - "nunjucks", - "alpinejs" + "nunjucks" ], "license": "MIT", "repository": { diff --git a/packages/cli/package.json b/packages/cli/package.json index af8a3dd..ab9d5fc 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "basecoat-cli", - "version": "0.3.2", + "version": "0.3.3", "description": "Add Basecoat components to your project", "author": "hunvreus", "license": "MIT", @@ -30,8 +30,7 @@ "css", "html", "jinja", - "nunjucks", - "alpinejs" + "nunjucks" ], "repository": { "type": "git", diff --git a/packages/css/package.json b/packages/css/package.json index 997c21b..bbbab26 100644 --- a/packages/css/package.json +++ b/packages/css/package.json @@ -1,6 +1,6 @@ { "name": "basecoat-css", - "version": "0.3.2", + "version": "0.3.3", "description": "Tailwind CSS for Basecoat components", "author": { "name": "hunvreus", @@ -26,8 +26,7 @@ "css", "html", "jinja", - "nunjucks", - "alpinejs" + "nunjucks" ], "repository": { "type": "git", @@ -43,6 +42,8 @@ "./css": "./dist/basecoat.css", "./all": "./dist/js/all.js", "./all.min": "./dist/js/all.min.js", + "./command": "./dist/js/command.js", + "./command.min": "./dist/js/command.min.js", "./dropdown-menu": "./dist/js/dropdown-menu.js", "./dropdown-menu.min": "./dist/js/dropdown-menu.min.js", "./popover": "./dist/js/popover.js", diff --git a/src/css/basecoat.css b/src/css/basecoat.css index 809e04c..6b2b05a 100644 --- a/src/css/basecoat.css +++ b/src/css/basecoat.css @@ -146,10 +146,6 @@ border-radius: var(--scrollbar-radius); } } - /* Alpine.js */ - [x-cloak] { - display: none !important; - } } /* Alert */ @@ -182,6 +178,30 @@ } } +/* Badge */ +@layer components { + .badge, + .badge-primary, + .badge-secondary, + .badge-destructive, + .badge-outline { + @apply inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden; + } + .badge, + .badge-primary { + @apply border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90; + } + .badge-secondary { + @apply border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90; + } + .badge-destructive { + @apply border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60; + } + .badge-outline { + @apply text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground; + } +} + /* Button */ @layer components { .btn, @@ -360,27 +380,44 @@ } } -/* Badge */ -@layer components { - .badge, - .badge-primary, - .badge-secondary, - .badge-destructive, - .badge-outline { - @apply inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden; +/* Button Group */ +.button-group { + @apply inline-flex w-fit items-stretch isolate; + + > *:focus-visible, + > :is(.dropdown-menu, .popover, .select) > button:focus-visible { + @apply relative z-10; } - .badge, - .badge-primary { - @apply border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90; + + > hr[role='separator'] { + @apply w-0 h-auto self-stretch border border-input shrink-0 m-0; } - .badge-secondary { - @apply border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90; - } - .badge-destructive { - @apply border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60; - } - .badge-outline { - @apply text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground; + + &:not([data-orientation='vertical']) { + > *:not(:first-child), + > :is(.dropdown-menu, .popover, .select):not(:first-child) > button { + @apply rounded-l-none border-l-0; + } + > *:not(:last-child), + > :is(.dropdown-menu, .popover, .select):not(:last-child) > button { + @apply rounded-r-none; + } + } + &[data-orientation='vertical'] { + @apply flex-col; + + > hr[role='separator'] { + @apply w-auto h-px; + } + + > *:not(:first-child), + > :is(.dropdown-menu, .popover, .select):not(:first-child) > button { + @apply rounded-t-none border-t-0; + } + > *:not(:last-child), + > :is(.dropdown-menu, .popover, .select):not(:last-child) > button { + @apply rounded-b-none; + } } } @@ -411,7 +448,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; @@ -443,6 +480,115 @@ } } +/* Command */ +@layer components { + .command-dialog { + @apply inset-y-0 opacity-0 transition-all transition-discrete; + + &:is([open],:popover-open) { + @apply opacity-100; + + &::backdrop { + @apply opacity-100; + } + > * { + @apply scale-100; + } + + @starting-style { + @apply opacity-0; + + &::backdrop { + @apply opacity-0; + } + > * { + @apply scale-95; + } + } + } + &::backdrop { + @apply bg-black/50 opacity-0 transition-all transition-discrete; + } + > * { + @apply bg-background fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%_-_2rem)] -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-lg border shadow-lg sm:max-w-lg max-h-[min(640px,calc(100%_-_2rem))]; + @apply transition-all scale-95; + + > button, + > form[method='dialog'] { + @apply absolute top-4 right-4; + } + > button, + > form[method='dialog'] > button { + @apply ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4; + } + } + + .command { + > header input { + @apply h-12; + } + [role='menu'] { + @apply px-2; + + [role='menuitem'] { + @apply py-3 px-2 [&_svg]:size-5; + } + [role='heading'] { + @apply px-2 py-1.5 font-medium text-muted-foreground; + } + } + } + } + + .command { + > header { + @apply flex items-center border-b px-3 gap-2; + + svg { + @apply size-4 shrink-0 opacity-50; + } + input { + @apply placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50; + } + } + [role='menu'] { + @apply max-h-[300px] overflow-y-auto overflow-x-hidden p-1; + + [role='menuitem'] { + @apply aria-hidden:hidden relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none aria-disabled:opacity-50 aria-disabled:pointer-events-none disabled:opacity-50 disabled:pointer-events-none w-full truncate [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4; + + &.active { + @apply bg-accent text-accent-foreground; + } + } + [role='heading'] { + @apply text-muted-foreground flex px-2 py-1.5 text-xs font-medium; + } + [role='group']:not(:has([role='menuitem']:not([aria-hidden='true']))) { + @apply hidden; + } + [role='separator'] { + @apply border-border -mx-1 my-1; + } + &:not(:has([data-value]:not([aria-hidden='true'])))::before { + @apply flex items-center justify-center py-6 px-3 text-sm truncate -m-1; + } + &[data-empty]:not(:has([data-value]:not([aria-hidden='true'])))::before { + @apply content-[attr(data-empty)]; + } + &:not([data-empty]):not(:has([data-value]:not([aria-hidden='true'])))::before { + @apply content-['No_results_found']; + } + } + &:not([data-command-initialized]) [role='menuitem'] { + @apply hover:bg-accent hover:text-accent-foreground; + } + &:has(> header input:not(:placeholder-shown)) [role='separator'] { + @apply hidden; + } + } +} + /* Dialog */ @layer components { .dialog { @@ -454,7 +600,7 @@ &::backdrop { @apply opacity-100; } - > article { + > * { @apply scale-100; } @@ -464,7 +610,7 @@ &::backdrop { @apply opacity-0; } - > article { + > * { @apply scale-95; } } @@ -472,7 +618,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; @@ -543,21 +689,65 @@ } } +/* Field */ +@layer components { + .fieldset { + @apply flex flex-col gap-6; + + > legend { + @apply text-base font-medium mb-3; + } + } + .field { + @apply flex flex-col w-full gap-3 data-[invalid=true]:text-destructive [&>*]:w-full [&>.sr-only]:w-auto; + + h2, + h3 { + @apply flex w-fit items-center gap-2 text-sm leading-snug font-medium; + } + [role="alert"] { + @apply text-sm text-destructive font-normal; + + ul { + @apply ml-4 flex list-disc flex-col gap-1; + } + } + section { + @apply flex flex-1 flex-col gap-1.5 leading-snug; + } + + &[data-orientation='horizontal'] { + @apply flex-row items-center [&>label]:flex-auto has-[>section]:items-start has-[>section]:[&>[type=checkbox],[type=radio]]:mt-px [&_p]:text-balance; + } + } + .fieldset legend + p, + .field > p, + .field section > p { + @apply text-muted-foreground text-sm leading-normal font-normal [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4; + } + .fieldset legend + p { + @apply -mt-1.5; + } + .field > p { + @apply last:mt-0 nth-last-2:-mt-1; + } +} + /* Input */ @layer components { - .form input[type='text'], - .form input[type='email'], - .form input[type='password'], - .form input[type='number'], - .form input[type='file'], - .form input[type='tel'], - .form input[type='url'], - .form input[type='search'], - .form input[type='date'], - .form input[type='datetime-local'], - .form input[type='month'], - .form input[type='week'], - .form input[type='time'], + :is(.form, .field) input[type='text'], + :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'], @@ -577,9 +767,16 @@ } } +/* Kbd */ +@layer components { + .kbd { + @apply bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none [&_svg:not([class*='size-'])]:size-3; + } +} + /* Label */ @layer components { - .form label, + :is(.form, .field) label, .label { @apply flex items-center gap-2 text-sm leading-none font-medium select-none peer-disabled:pointer-events-none peer-disabled:opacity-50; @@ -664,7 +861,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; @@ -676,7 +873,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%; @@ -725,7 +922,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]; @@ -781,6 +978,9 @@ @apply content-['No_results_found']; } } + [data-popover]:has(> header input:not(:placeholder-shown)) [role='separator'] { + @apply hidden; + } &:not([data-select-initialized]) [data-popover] [role='option'] { @apply hover:bg-accent hover:text-accent-foreground; } @@ -909,7 +1109,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; @@ -971,7 +1171,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; } diff --git a/src/jinja/command.html.jinja b/src/jinja/command.html.jinja new file mode 100644 index 0000000..b05feca --- /dev/null +++ b/src/jinja/command.html.jinja @@ -0,0 +1,187 @@ +{# + Renders a standalone command menu component with search and keyboard navigation. + + @param id {string} [optional] - Unique identifier for the command component. + @param items {array} [optional] - Array of items to render (alternative to using caller). + @param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input. + @param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search. + @param main_attrs {object} [optional] - Additional HTML attributes for the main container div. + @param input_attrs {object} [optional] - Additional HTML attributes for the search input. + @param menu_attrs {object} [optional] - Additional HTML attributes for the menu div. +#} +{% macro command( + id=None, + items=None, + placeholder="Type a command or search...", + empty_text="No results found.", + main_attrs={}, + input_attrs={}, + menu_attrs={} +) %} +{% set id = id or ("command-" ~ range(100000, 999999) | random | string) %} +
    +
    + + +
    + +
    +{% endmacro %} + +{# + Renders a command dialog (modal command palette). + + @param id {string} [optional] - Unique identifier for the command dialog. + @param items {array} [optional] - Array of items to render (alternative to using caller). + @param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input. + @param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search. + @param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog element. + @param input_attrs {object} [optional] - Additional HTML attributes for the search input. + @param menu_attrs {object} [optional] - Additional HTML attributes for the menu div. + @param open {boolean} [optional] [default=False] - Whether the command dialog should be open initially. +#} +{% macro command_dialog( + id=None, + items=None, + placeholder="Type a command or search...", + empty_text="No results found.", + dialog_attrs={}, + input_attrs={}, + menu_attrs={}, + open=False +) %} +{% set id = id or ("command-dialog-" ~ range(100000, 999999) | random | string) %} + +
    +
    + + +
    + + +
    +
    +{% endmacro %} + +{# + Renders a list of items for the command component. + + @param items {array} - Array of items to render. Each item can be: + - { type: "item", label: "Text", 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") %} + {% for item in items %} + {% set item_id = parent_id_prefix ~ "-" ~ loop.index %} + {% if item.type == "group" %} + {% set group_label_id = item.id if item.id else "group-label-" + item_id %} +
    + {{ item.label }} + {{ render_command_items(item.items, item_id) if item.items }} +
    + {% elif item.type == "separator" %} +
    + {% elif item.type == "item" or not item.type %} + + {% endif %} + {% endfor %} +{% endmacro %} diff --git a/src/jinja/dialog.html.jinja b/src/jinja/dialog.html.jinja index 02f523e..74737b7 100644 --- a/src/jinja/dialog.html.jinja +++ b/src/jinja/dialog.html.jinja @@ -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 %} > -
    +
    {% if title or description %}
    {% endif %} -
    +
    {% endmacro %} \ No newline at end of file diff --git a/src/js/command.js b/src/js/command.js new file mode 100644 index 0000000..c4970ea --- /dev/null +++ b/src/js/command.js @@ -0,0 +1,148 @@ +(() => { + const initCommand = (container) => { + const input = container.querySelector('header input'); + const menu = container.querySelector('[role="menu"]'); + + if (!input || !menu) { + const missing = []; + if (!input) missing.push('input'); + if (!menu) missing.push('menu'); + console.error(`Command component initialization failed. Missing element(s): ${missing.join(', ')}`, container); + return; + } + + const allMenuItems = Array.from(menu.querySelectorAll('[role="menuitem"]')); + const menuItems = allMenuItems.filter(item => + !item.hasAttribute('disabled') && + item.getAttribute('aria-disabled') !== 'true' + ); + let visibleMenuItems = [...menuItems]; + let activeIndex = -1; + + const setActiveItem = (index) => { + if (activeIndex > -1 && menuItems[activeIndex]) { + menuItems[activeIndex].classList.remove('active'); + } + + activeIndex = index; + + if (activeIndex > -1) { + const activeItem = menuItems[activeIndex]; + activeItem.classList.add('active'); + if (activeItem.id) { + input.setAttribute('aria-activedescendant', activeItem.id); + } else { + input.removeAttribute('aria-activedescendant'); + } + } else { + input.removeAttribute('aria-activedescendant'); + } + }; + + const filterMenuItems = () => { + const searchTerm = input.value.trim().toLowerCase(); + + setActiveItem(-1); + + visibleMenuItems = []; + allMenuItems.forEach(item => { + 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 (visibleMenuItems.length > 0) { + setActiveItem(menuItems.indexOf(visibleMenuItems[0])); + visibleMenuItems[0].scrollIntoView({ block: 'nearest' }); + } + }; + + input.addEventListener('input', filterMenuItems); + + const handleKeyNavigation = (event) => { + if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End'].includes(event.key)) { + return; + } + + if (event.key === 'Enter') { + event.preventDefault(); + if (activeIndex > -1) { + menuItems[activeIndex]?.click(); + } + return; + } + + if (visibleMenuItems.length === 0) return; + + event.preventDefault(); + + const currentVisibleIndex = activeIndex > -1 ? visibleMenuItems.indexOf(menuItems[activeIndex]) : -1; + let nextVisibleIndex = currentVisibleIndex; + + switch (event.key) { + case 'ArrowDown': + if (currentVisibleIndex < visibleMenuItems.length - 1) { + nextVisibleIndex = currentVisibleIndex + 1; + } + break; + case 'ArrowUp': + if (currentVisibleIndex > 0) { + nextVisibleIndex = currentVisibleIndex - 1; + } else if (currentVisibleIndex === -1) { + nextVisibleIndex = 0; + } + break; + case 'Home': + nextVisibleIndex = 0; + break; + case 'End': + nextVisibleIndex = visibleMenuItems.length - 1; + break; + } + + if (nextVisibleIndex !== currentVisibleIndex) { + const newActiveItem = visibleMenuItems[nextVisibleIndex]; + setActiveItem(menuItems.indexOf(newActiveItem)); + newActiveItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + }; + + menu.addEventListener('mousemove', (event) => { + const menuItem = event.target.closest('[role="menuitem"]'); + if (menuItem && visibleMenuItems.includes(menuItem)) { + const index = menuItems.indexOf(menuItem); + if (index !== activeIndex) { + setActiveItem(index); + } + } + }); + + menu.addEventListener('click', (event) => { + const clickedItem = event.target.closest('[role="menuitem"]'); + if (clickedItem && visibleMenuItems.includes(clickedItem)) { + const dialog = container.closest('dialog.command-dialog'); + if (dialog && !clickedItem.hasAttribute('data-keep-command-open')) { + dialog.close(); + } + } + }); + + input.addEventListener('keydown', handleKeyNavigation); + + if (visibleMenuItems.length > 0) { + setActiveItem(menuItems.indexOf(visibleMenuItems[0])); + visibleMenuItems[0].scrollIntoView({ block: 'nearest' }); + } + + container.dataset.commandInitialized = true; + container.dispatchEvent(new CustomEvent('basecoat:initialized')); + }; + + if (window.basecoat) { + window.basecoat.register('command', '.command:not([data-command-initialized])', initCommand); + } +})(); diff --git a/src/js/select.js b/src/js/select.js index 8e10dc8..352dcb8 100644 --- a/src/js/select.js +++ b/src/js/select.js @@ -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); } }); diff --git a/src/nunjucks/command.njk b/src/nunjucks/command.njk new file mode 100644 index 0000000..f3b6b84 --- /dev/null +++ b/src/nunjucks/command.njk @@ -0,0 +1,187 @@ +{# + Renders a standalone command menu component with search and keyboard navigation. + + @param id {string} [optional] - Unique identifier for the command component. + @param items {array} [optional] - Array of items to render (alternative to using caller). + @param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input. + @param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search. + @param main_attrs {object} [optional] - Additional HTML attributes for the main container div. + @param input_attrs {object} [optional] - Additional HTML attributes for the search input. + @param menu_attrs {object} [optional] - Additional HTML attributes for the menu div. +#} +{% macro command( + id=None, + items=None, + placeholder="Type a command or search...", + empty_text="No results found.", + main_attrs={}, + input_attrs={}, + menu_attrs={} +) %} +{% set id = id or ("command-" + (range(100000, 999999) | random | string)) %} +
    +
    + + +
    + +
    +{% endmacro %} + +{# + Renders a command dialog (modal command palette). + + @param id {string} [optional] - Unique identifier for the command dialog. + @param items {array} [optional] - Array of items to render (alternative to using caller). + @param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input. + @param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search. + @param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog element. + @param input_attrs {object} [optional] - Additional HTML attributes for the search input. + @param menu_attrs {object} [optional] - Additional HTML attributes for the menu div. + @param open {boolean} [optional] [default=false] - Whether the command dialog should be open initially. +#} +{% macro command_dialog( + id=None, + items=None, + placeholder="Type a command or search...", + empty_text="No results found.", + dialog_attrs={}, + input_attrs={}, + menu_attrs={}, + open=false +) %} +{% set id = id or ("command-dialog-" + (range(100000, 999999) | random | string)) %} + +
    +
    + + +
    + + +
    +
    +{% endmacro %} + +{# + Renders a list of items for the command component. + + @param items {array} - Array of items to render. Each item can be: + - { type: "item", label: "Text", 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") %} + {% for item in items %} + {% set item_id = parent_id_prefix ~ "-" ~ loop.index %} + {% if item.type == "group" %} + {% set group_label_id = item.id if item.id else "group-label-" + item_id %} +
    + {{ item.label }} + {{ render_command_items(item.items, item_id) if item.items }} +
    + {% elif item.type == "separator" %} +
    + {% elif item.type == "item" or not item.type %} + + {% endif %} + {% endfor %} +{% endmacro %} diff --git a/src/nunjucks/dialog.njk b/src/nunjucks/dialog.njk index d5d05b5..52e2edb 100644 --- a/src/nunjucks/dialog.njk +++ b/src/nunjucks/dialog.njk @@ -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 %} > -
    +
    {% if title or description %}
    {% endif %} -
    +
    {% endmacro %} \ No newline at end of file