mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-11 12:48:19 +00:00
Compare commits
32
Commits
0.3.3
...
0.3.10-beta.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ec06205a0 | ||
|
|
06e7c1ec84 | ||
|
|
09f868d474 | ||
|
|
b9274702d3 | ||
|
|
83c09c1197 | ||
|
|
36bca0c6a7 | ||
|
|
167b98e573 | ||
|
|
4035870578 | ||
|
|
dea0602b03 | ||
|
|
19941561d3 | ||
|
|
f708994df9 | ||
|
|
6fcbb31e30 | ||
|
|
0dfc5e061c | ||
|
|
c771c8256b | ||
|
|
2adb4c4667 | ||
|
|
cbb1119699 | ||
|
|
88db2e84ce | ||
|
|
a7f6f717d1 | ||
|
|
91c3932fe7 | ||
|
|
f977492bf6 | ||
|
|
21ba8ecb9e | ||
|
|
c0bf373890 | ||
|
|
77680f9e2b | ||
|
|
2afa2ae02e | ||
|
|
23585e3161 | ||
|
|
7e3324edf2 | ||
|
|
cda08f3c95 | ||
|
|
0b46e05a29 | ||
|
|
7322220788 | ||
|
|
4c86ce9bfc | ||
|
|
bff89833d4 | ||
|
|
530224c587 |
+2
-1
@@ -9,6 +9,7 @@ export default async function(eleventyConfig) {
|
||||
eleventyConfig.setInputDirectory("./docs/src");
|
||||
eleventyConfig.addPassthroughCopy({"docs/src/assets": "assets"});
|
||||
eleventyConfig.addPassthroughCopy({ "src/js": "assets/js" });
|
||||
eleventyConfig.addPassthroughCopy({ "docs/src/llms.txt": "llms.txt" });
|
||||
eleventyConfig.addPlugin(eleventyLucideicons);
|
||||
eleventyConfig.addShortcode("fetchCode", function(filePath) {
|
||||
const absolutePath = path.resolve(process.cwd(), filePath);
|
||||
@@ -43,4 +44,4 @@ export default async function(eleventyConfig) {
|
||||
{ autoescape: true }
|
||||
);
|
||||
eleventyConfig.setLibrary("njk", nunjucksEnvironment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
github: [hunvreus]
|
||||
@@ -56,6 +56,19 @@ Basecoat is a vanilla JavaScript/HTML/CSS port of shadcn/ui components. It reuse
|
||||
- If it's just composition of existing elements (users can do it with standard HTML/Tailwind)
|
||||
- If behavior is fully native (e.g., fieldset, legend)
|
||||
|
||||
## Adding new components
|
||||
|
||||
Before creating a component, ask:
|
||||
1. Does shadcn/ui have it? → Port it
|
||||
2. Is it pure composition? → Document pattern, no component
|
||||
3. Does it need JS behavior? → Create component
|
||||
4. Is it just CSS? → Add to basecoat.css, document usage
|
||||
|
||||
Examples:
|
||||
- Field/Fieldset: CSS-only patterns (no JS component)
|
||||
- Empty/Item/Input Group: Pure composition (docs only)
|
||||
- Command/Select: JS behavior required (full component)
|
||||
|
||||
## Common tasks
|
||||
|
||||
- Update a component's CSS/JS: Edit `src/css/basecoat.css` and/or `src/js/*.js`.
|
||||
@@ -81,6 +94,18 @@ No automated testing configured. User validates manually via the docs site (`npm
|
||||
- Tailwind via `@tailwindcss/cli` v4.
|
||||
- Build uses Tailwind CLI and `terser`; both are provided via devDependencies.
|
||||
|
||||
## Git workflow
|
||||
|
||||
- `.gitattributes` configures merge strategies for generated files
|
||||
- `docs/src/assets/styles.css` auto-resolves to incoming changes (regenerated by Tailwind)
|
||||
- Always run `npm run build` before publishing packages
|
||||
|
||||
## Publishing packages
|
||||
|
||||
1. Bump version in root `package.json`
|
||||
2. Run `npm run build` to generate dist files
|
||||
3. Run `npm publish --workspaces` to publish both packages
|
||||
|
||||
## Package references
|
||||
|
||||
- `packages/css` usage and details: see `packages/css/README.md`.
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
## Now
|
||||
|
||||
- [ ] Add Drawer component
|
||||
- [ ] Make utility components (e.g., Item) into proper components (i.e., .item)
|
||||
- [ ] Split CSS into components
|
||||
|
||||
## Next
|
||||
|
||||
- [ ] Make Command load dynamic items (e.g. search results from remote endpoint)
|
||||
- [ ] Make Combobox load dynamic items (e.g. search results from remote endpoint)
|
||||
- [ ] Add Toggle component
|
||||
- [ ] Add Carousel component
|
||||
- [ ] Add Sheet component
|
||||
- [ ] Add Separator component
|
||||
- [ ] Adding Menubar component
|
||||
|
||||
## Later
|
||||
|
||||
- [ ] Add Calendar component (https://flatpickr.js.org/)
|
||||
- [ ] Finish Chart component (https://www.chartjs.org/)
|
||||
+1
-1
@@ -50,7 +50,7 @@
|
||||
@apply border-t pt-3 mt-3 first:mt-0;
|
||||
}
|
||||
dd {
|
||||
@apply mt-2;
|
||||
@apply mt-2 space-y-2;
|
||||
}
|
||||
dl {
|
||||
@apply mt-4 border-b-0 pb-0 pl-8 my-0;
|
||||
|
||||
+1
-137
@@ -5,140 +5,4 @@
|
||||
|
||||
@import "./themes/claude.css";
|
||||
@import "./themes/doom-64.css";
|
||||
@import "./themes/supabase.css";
|
||||
|
||||
|
||||
/* @theme inline {
|
||||
--breakpoint-3xl: 1600px;
|
||||
--breakpoint-4xl: 2000px;
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-surface: var(--surface);
|
||||
--color-surface-foreground: var(--surface-foreground);
|
||||
--color-code: var(--code);
|
||||
--color-code-foreground: var(--code-foreground);
|
||||
--color-code-highlight: var(--code-highlight);
|
||||
--color-code-number: var(--code-number);
|
||||
--color-selection: var(--selection);
|
||||
--color-selection-foreground: var(--selection-foreground);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: var(--color-blue-300);
|
||||
--chart-2: var(--color-blue-500);
|
||||
--chart-3: var(--color-blue-600);
|
||||
--chart-4: var(--color-blue-700);
|
||||
--chart-5: var(--color-blue-800);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--surface: oklch(0.98 0 0);
|
||||
--surface-foreground: var(--foreground);
|
||||
--code: var(--surface);
|
||||
--code-foreground: var(--surface-foreground);
|
||||
--code-highlight: oklch(0.96 0 0);
|
||||
--code-number: oklch(0.56 0 0);
|
||||
--selection: oklch(0.145 0 0);
|
||||
--selection-foreground: oklch(1 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.269 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.371 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: var(--color-blue-300);
|
||||
--chart-2: var(--color-blue-500);
|
||||
--chart-3: var(--color-blue-600);
|
||||
--chart-4: var(--color-blue-700);
|
||||
--chart-5: var(--color-blue-800);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
--surface: oklch(0.2 0 0);
|
||||
--surface-foreground: oklch(0.708 0 0);
|
||||
--code: var(--surface);
|
||||
--code-foreground: var(--surface-foreground);
|
||||
--code-highlight: oklch(0.27 0 0);
|
||||
--code-number: oklch(0.72 0 0);
|
||||
--selection: oklch(0.922 0 0);
|
||||
--selection-foreground: oklch(0.205 0 0);
|
||||
} */
|
||||
@import "./themes/supabase.css";
|
||||
@@ -136,4 +136,4 @@
|
||||
footer=footer,
|
||||
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
|
||||
) }}
|
||||
) }}
|
||||
|
||||
+410
-375
@@ -19,11 +19,9 @@
|
||||
--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);
|
||||
@@ -585,6 +583,9 @@
|
||||
.w-\[200px\] {
|
||||
width: 200px;
|
||||
}
|
||||
.w-\[220px\] {
|
||||
width: 220px;
|
||||
}
|
||||
.w-\[271px\] {
|
||||
width: 271px;
|
||||
}
|
||||
@@ -1275,6 +1276,9 @@
|
||||
.underline-offset-4 {
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
.accent-foreground {
|
||||
accent-color: var(--color-foreground);
|
||||
}
|
||||
.opacity-0 {
|
||||
opacity: 0%;
|
||||
}
|
||||
@@ -1303,6 +1307,10 @@
|
||||
--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.ring {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.ring-offset-background {
|
||||
--tw-ring-offset-color: var(--color-background);
|
||||
}
|
||||
@@ -2372,7 +2380,7 @@
|
||||
&>svg {
|
||||
color: currentcolor;
|
||||
}
|
||||
h2, h3 {
|
||||
> h2, > h3, > h4, > h5, > h6, > strong, > [data-title] {
|
||||
grid-column-start: 2;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
@@ -2384,7 +2392,7 @@
|
||||
--tw-tracking: var(--tracking-tight);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
}
|
||||
section {
|
||||
> section {
|
||||
grid-column-start: 2;
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
@@ -2414,7 +2422,7 @@
|
||||
&>svg {
|
||||
color: currentcolor;
|
||||
}
|
||||
section {
|
||||
> section {
|
||||
color: var(--color-destructive);
|
||||
}
|
||||
}
|
||||
@@ -2784,52 +2792,54 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.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-self: stretch;
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
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;
|
||||
@layer components {
|
||||
.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;
|
||||
}
|
||||
> *:not(:last-child), > :is(.dropdown-menu, .popover, .select):not(:last-child) > button {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
&[data-orientation='vertical'] {
|
||||
flex-direction: column;
|
||||
> hr[role='separator'] {
|
||||
height: 1px;
|
||||
width: auto;
|
||||
margin: calc(var(--spacing) * 0);
|
||||
height: auto;
|
||||
width: calc(var(--spacing) * 0);
|
||||
flex-shrink: 0;
|
||||
align-self: stretch;
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
border-color: var(--color-input);
|
||||
}
|
||||
> *: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;
|
||||
&: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;
|
||||
}
|
||||
> *:not(:last-child), > :is(.dropdown-menu, .popover, .select):not(:last-child) > button {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
> *:not(:last-child), > :is(.dropdown-menu, .popover, .select):not(:last-child) > button {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
&[data-orientation='vertical'] {
|
||||
flex-direction: column;
|
||||
> hr[role='separator'] {
|
||||
height: 1px;
|
||||
width: auto;
|
||||
}
|
||||
> *: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;
|
||||
}
|
||||
> *:not(:last-child), > :is(.dropdown-menu, .popover, .select):not(:last-child) > button {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3270,7 +3280,7 @@
|
||||
margin-block: calc(var(--spacing) * 1);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
&:not(:has([data-value]:not([aria-hidden='true'])))::before {
|
||||
&:not(:has([role='menuitem']:not([aria-hidden='true'])))::before {
|
||||
margin: calc(var(--spacing) * -1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3283,11 +3293,11 @@
|
||||
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 {
|
||||
&[data-empty]:not(:has([role='menuitem']: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 {
|
||||
&:not([data-empty]):not(:has([role='menuitem']:not([aria-hidden='true'])))::before {
|
||||
--tw-content: 'No results found';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
@@ -3691,7 +3701,7 @@
|
||||
}
|
||||
}
|
||||
@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'] {
|
||||
:is(.form, .field) input[type='text']:not(:is(.select [data-popover] > header input[role='combobox'], .command > header input, .dialog > * > header input)), :is(.form, .field) input[type='email'], :is(.form, .field) input[type='password'], :is(.form, .field) input[type='number'], :is(.form, .field) input[type='file'], :is(.form, .field) input[type='tel'], :is(.form, .field) input[type='url'], :is(.form, .field) input[type='search']:not(:is(.select [data-popover] > header input[role='combobox'], .command > header input, .dialog > * > header input)), :is(.form, .field) input[type='date'], :is(.form, .field) input[type='datetime-local'], :is(.form, .field) input[type='month'], :is(.form, .field) input[type='week'], :is(.form, .field) input[type='time'], .input[type='text'], .input[type='email'], .input[type='password'], .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%;
|
||||
@@ -4234,6 +4244,24 @@
|
||||
*:not(select).select {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
& > button {
|
||||
justify-content: space-between;
|
||||
--tw-font-weight: var(--font-weight-normal);
|
||||
font-weight: var(--font-weight-normal);
|
||||
&[aria-invalid='true'] {
|
||||
border-color: var(--color-destructive);
|
||||
--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 *) {
|
||||
--tw-ring-color: var(--color-destructive);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
--tw-ring-color: color-mix(in oklab, var(--color-destructive) 40%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
[data-popover] {
|
||||
padding: calc(var(--spacing) * 1);
|
||||
[role='option'] {
|
||||
@@ -4394,355 +4422,357 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.sidebar {
|
||||
&:not([data-sidebar-initialized]) {
|
||||
@media (width < 48rem) {
|
||||
display: none;
|
||||
@layer components {
|
||||
.sidebar {
|
||||
&:not([data-sidebar-initialized]) {
|
||||
@media (width < 48rem) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not([aria-hidden]), &[aria-hidden=false] {
|
||||
@media (width < 48rem) {
|
||||
&:not([aria-hidden]), &[aria-hidden=false] {
|
||||
@media (width < 48rem) {
|
||||
position: fixed;
|
||||
}
|
||||
@media (width < 48rem) {
|
||||
inset: calc(var(--spacing) * 0);
|
||||
}
|
||||
@media (width < 48rem) {
|
||||
z-index: 40;
|
||||
}
|
||||
@media (width < 48rem) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
nav {
|
||||
position: fixed;
|
||||
}
|
||||
@media (width < 48rem) {
|
||||
inset: calc(var(--spacing) * 0);
|
||||
}
|
||||
@media (width < 48rem) {
|
||||
z-index: 40;
|
||||
}
|
||||
@media (width < 48rem) {
|
||||
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);
|
||||
inset-block: calc(var(--spacing) * 0);
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
width: var(--sidebar-mobile-width);
|
||||
flex-direction: column;
|
||||
background-color: var(--color-sidebar);
|
||||
color: var(--color-sidebar-foreground);
|
||||
transition-property: transform, translate, scale, rotate;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
--tw-duration: 300ms;
|
||||
transition-duration: 300ms;
|
||||
--tw-ease: var(--ease-in-out);
|
||||
transition-timing-function: var(--ease-in-out);
|
||||
@media (width >= 48rem) {
|
||||
width: var(--sidebar-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
nav {
|
||||
position: fixed;
|
||||
inset-block: calc(var(--spacing) * 0);
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
width: var(--sidebar-mobile-width);
|
||||
flex-direction: column;
|
||||
background-color: var(--color-sidebar);
|
||||
color: var(--color-sidebar-foreground);
|
||||
transition-property: transform, translate, scale, rotate;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
--tw-duration: 300ms;
|
||||
transition-duration: 300ms;
|
||||
--tw-ease: var(--ease-in-out);
|
||||
transition-timing-function: var(--ease-in-out);
|
||||
@media (width >= 48rem) {
|
||||
width: var(--sidebar-width);
|
||||
}
|
||||
}
|
||||
& + * {
|
||||
transition-property: margin;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
--tw-duration: 300ms;
|
||||
transition-duration: 300ms;
|
||||
--tw-ease: var(--ease-in-out);
|
||||
transition-timing-function: var(--ease-in-out);
|
||||
}
|
||||
&:not([data-side]), &[data-side=left] {
|
||||
nav {
|
||||
left: calc(var(--spacing) * 0);
|
||||
border-right-style: var(--tw-border-style);
|
||||
border-right-width: 1px;
|
||||
}
|
||||
& + * {
|
||||
position: relative;
|
||||
@media (width >= 48rem) {
|
||||
margin-left: var(--sidebar-width);
|
||||
}
|
||||
transition-property: margin;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
--tw-duration: 300ms;
|
||||
transition-duration: 300ms;
|
||||
--tw-ease: var(--ease-in-out);
|
||||
transition-timing-function: var(--ease-in-out);
|
||||
}
|
||||
&[aria-hidden=true] {
|
||||
&:not([data-side]), &[data-side=left] {
|
||||
nav {
|
||||
--tw-translate-x: -100%;
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
left: calc(var(--spacing) * 0);
|
||||
border-right-style: var(--tw-border-style);
|
||||
border-right-width: 1px;
|
||||
}
|
||||
& + * {
|
||||
@media (width >= 48rem) {
|
||||
margin-left: calc(var(--spacing) * 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&[data-side=right] {
|
||||
nav {
|
||||
right: calc(var(--spacing) * 0);
|
||||
border-left-style: var(--tw-border-style);
|
||||
border-left-width: 1px;
|
||||
}
|
||||
& + * {
|
||||
position: relative;
|
||||
@media (width >= 48rem) {
|
||||
margin-right: var(--sidebar-width);
|
||||
}
|
||||
}
|
||||
&[aria-hidden=true] {
|
||||
nav {
|
||||
--tw-translate-x: 100%;
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
& + * {
|
||||
@media (width >= 48rem) {
|
||||
margin-right: calc(var(--spacing) * 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
nav {
|
||||
> header, > footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--spacing) * 2);
|
||||
padding: calc(var(--spacing) * 2);
|
||||
}
|
||||
[role=separator] {
|
||||
margin-inline: calc(var(--spacing) * 2);
|
||||
width: auto;
|
||||
border-color: var(--color-sidebar-border);
|
||||
}
|
||||
> section {
|
||||
display: flex;
|
||||
min-height: calc(var(--spacing) * 0);
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--spacing) * 2);
|
||||
overflow-y: auto;
|
||||
> [role=group] {
|
||||
position: relative;
|
||||
@media (width >= 48rem) {
|
||||
margin-left: var(--sidebar-width);
|
||||
}
|
||||
}
|
||||
&[aria-hidden=true] {
|
||||
nav {
|
||||
--tw-translate-x: -100%;
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
& + * {
|
||||
@media (width >= 48rem) {
|
||||
margin-left: calc(var(--spacing) * 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&[data-side=right] {
|
||||
nav {
|
||||
right: calc(var(--spacing) * 0);
|
||||
border-left-style: var(--tw-border-style);
|
||||
border-left-width: 1px;
|
||||
}
|
||||
& + * {
|
||||
position: relative;
|
||||
@media (width >= 48rem) {
|
||||
margin-right: var(--sidebar-width);
|
||||
}
|
||||
}
|
||||
&[aria-hidden=true] {
|
||||
nav {
|
||||
--tw-translate-x: 100%;
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
& + * {
|
||||
@media (width >= 48rem) {
|
||||
margin-right: calc(var(--spacing) * 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
nav {
|
||||
> header, > footer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: calc(var(--spacing) * 0);
|
||||
flex-direction: column;
|
||||
gap: calc(var(--spacing) * 2);
|
||||
padding: calc(var(--spacing) * 2);
|
||||
}
|
||||
h3 {
|
||||
display: flex;
|
||||
height: calc(var(--spacing) * 8);
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
border-radius: var(--radius-md);
|
||||
padding-inline: calc(var(--spacing) * 2);
|
||||
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-sidebar-foreground);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, var(--color-sidebar-foreground) 70%, transparent);
|
||||
}
|
||||
--tw-ring-color: var(--color-sidebar-ring);
|
||||
--tw-outline-style: none;
|
||||
outline-style: none;
|
||||
@media (forced-colors: active) {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
transition-property: margin,opacity;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
--tw-duration: 200ms;
|
||||
transition-duration: 200ms;
|
||||
--tw-ease: linear;
|
||||
transition-timing-function: linear;
|
||||
&:focus-visible {
|
||||
--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);
|
||||
}
|
||||
&>svg {
|
||||
width: calc(var(--spacing) * 4);
|
||||
height: calc(var(--spacing) * 4);
|
||||
}
|
||||
&>svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
[role=separator] {
|
||||
margin-inline: calc(var(--spacing) * 2);
|
||||
width: auto;
|
||||
border-color: var(--color-sidebar-border);
|
||||
}
|
||||
ul {
|
||||
> section {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: calc(var(--spacing) * 0);
|
||||
min-height: calc(var(--spacing) * 0);
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--spacing) * 1);
|
||||
li {
|
||||
gap: calc(var(--spacing) * 2);
|
||||
overflow-y: auto;
|
||||
> [role=group] {
|
||||
position: relative;
|
||||
> a, > details > summary {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: calc(var(--spacing) * 2);
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
padding: calc(var(--spacing) * 2);
|
||||
text-align: left;
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||
--tw-ring-color: var(--color-sidebar-ring);
|
||||
--tw-outline-style: none;
|
||||
outline-style: none;
|
||||
@media (forced-colors: active) {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
transition-property: width,height,padding;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-sidebar-accent);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-sidebar-accent-foreground);
|
||||
}
|
||||
}
|
||||
&:focus-visible {
|
||||
--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);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-sidebar-accent);
|
||||
}
|
||||
&:active {
|
||||
color: var(--color-sidebar-accent-foreground);
|
||||
}
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
}
|
||||
&[aria-disabled="true"] {
|
||||
pointer-events: none;
|
||||
}
|
||||
&[aria-disabled="true"] {
|
||||
opacity: 50%;
|
||||
}
|
||||
&>span:last-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
&>svg {
|
||||
width: calc(var(--spacing) * 4);
|
||||
height: calc(var(--spacing) * 4);
|
||||
}
|
||||
&>svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
&[aria-current=page] {
|
||||
background-color: var(--color-sidebar-accent);
|
||||
}
|
||||
&[aria-current=page] {
|
||||
--tw-font-weight: var(--font-weight-medium);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
&[aria-current=page] {
|
||||
color: var(--color-sidebar-accent-foreground);
|
||||
}
|
||||
&:not([data-variant]), &[data-variant=default] {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-sidebar-accent);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-sidebar-accent-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
&[data-variant=outline] {
|
||||
background-color: var(--color-background);
|
||||
--tw-shadow: 0 0 0 1px var(--tw-shadow-color, hsl(var(--sidebar-border)));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-sidebar-accent);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-sidebar-accent-foreground);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
--tw-shadow: 0 0 0 1px var(--tw-shadow-color, hsl(var(--sidebar-accent)));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not([data-size]), &[data-size=default] {
|
||||
height: calc(var(--spacing) * 8);
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||
}
|
||||
&[data-size=sm] {
|
||||
height: calc(var(--spacing) * 7);
|
||||
font-size: var(--text-xs);
|
||||
line-height: var(--tw-leading, var(--text-xs--line-height));
|
||||
}
|
||||
&[data-size=lg] {
|
||||
height: calc(var(--spacing) * 12);
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||
&:is(:where(.group)[data-collapsible="icon"] *) {
|
||||
padding: calc(var(--spacing) * 0) !important;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: calc(var(--spacing) * 0);
|
||||
flex-direction: column;
|
||||
padding: calc(var(--spacing) * 2);
|
||||
}
|
||||
h3 {
|
||||
display: flex;
|
||||
height: calc(var(--spacing) * 8);
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
border-radius: var(--radius-md);
|
||||
padding-inline: calc(var(--spacing) * 2);
|
||||
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-sidebar-foreground);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, var(--color-sidebar-foreground) 70%, transparent);
|
||||
}
|
||||
> details {
|
||||
&:not([open]) {
|
||||
> summary {
|
||||
&::after {
|
||||
rotate: calc(90deg * -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
> summary {
|
||||
&::after {
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
width: calc(var(--spacing) * 3.5);
|
||||
height: calc(var(--spacing) * 3.5);
|
||||
background-color: var(--color-primary);
|
||||
transition-property: transform, translate, scale, rotate;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
--tw-ease: linear;
|
||||
transition-timing-function: linear;
|
||||
--tw-content: '';
|
||||
content: var(--tw-content);
|
||||
mask-image: var(--chevron-down-icon);
|
||||
mask-size: 1rem;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
&::details-content {
|
||||
padding-inline: calc(var(--spacing) * 3.5);
|
||||
}
|
||||
--tw-ring-color: var(--color-sidebar-ring);
|
||||
--tw-outline-style: none;
|
||||
outline-style: none;
|
||||
@media (forced-colors: active) {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
transition-property: margin,opacity;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
--tw-duration: 200ms;
|
||||
transition-duration: 200ms;
|
||||
--tw-ease: linear;
|
||||
transition-timing-function: linear;
|
||||
&:focus-visible {
|
||||
--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);
|
||||
}
|
||||
&>svg {
|
||||
width: calc(var(--spacing) * 4);
|
||||
height: calc(var(--spacing) * 4);
|
||||
}
|
||||
&>svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: calc(var(--spacing) * 0);
|
||||
--tw-translate-x: 1px;
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
flex-direction: column;
|
||||
gap: calc(var(--spacing) * 1);
|
||||
border-left-style: var(--tw-border-style);
|
||||
border-left-width: 1px;
|
||||
border-color: var(--color-sidebar-border);
|
||||
padding-inline: calc(var(--spacing) * 2.5);
|
||||
padding-block: calc(var(--spacing) * 0.5);
|
||||
li {
|
||||
position: relative;
|
||||
> a, > details > summary {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: calc(var(--spacing) * 2);
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
padding: calc(var(--spacing) * 2);
|
||||
text-align: left;
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||
--tw-ring-color: var(--color-sidebar-ring);
|
||||
--tw-outline-style: none;
|
||||
outline-style: none;
|
||||
@media (forced-colors: active) {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
transition-property: width,height,padding;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-sidebar-accent);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-sidebar-accent-foreground);
|
||||
}
|
||||
}
|
||||
&:focus-visible {
|
||||
--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);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-sidebar-accent);
|
||||
}
|
||||
&:active {
|
||||
color: var(--color-sidebar-accent-foreground);
|
||||
}
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
}
|
||||
&[aria-disabled="true"] {
|
||||
pointer-events: none;
|
||||
}
|
||||
&[aria-disabled="true"] {
|
||||
opacity: 50%;
|
||||
}
|
||||
&>span:last-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
&>svg {
|
||||
width: calc(var(--spacing) * 4);
|
||||
height: calc(var(--spacing) * 4);
|
||||
}
|
||||
&>svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
&[aria-current=page] {
|
||||
background-color: var(--color-sidebar-accent);
|
||||
}
|
||||
&[aria-current=page] {
|
||||
--tw-font-weight: var(--font-weight-medium);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
&[aria-current=page] {
|
||||
color: var(--color-sidebar-accent-foreground);
|
||||
}
|
||||
&:not([data-variant]), &[data-variant=default] {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-sidebar-accent);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-sidebar-accent-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
&[data-variant=outline] {
|
||||
background-color: var(--color-background);
|
||||
--tw-shadow: 0 0 0 1px var(--tw-shadow-color, hsl(var(--sidebar-border)));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-sidebar-accent);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-sidebar-accent-foreground);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
--tw-shadow: 0 0 0 1px var(--tw-shadow-color, hsl(var(--sidebar-accent)));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not([data-size]), &[data-size=default] {
|
||||
height: calc(var(--spacing) * 8);
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||
}
|
||||
&[data-size=sm] {
|
||||
height: calc(var(--spacing) * 7);
|
||||
font-size: var(--text-xs);
|
||||
line-height: var(--tw-leading, var(--text-xs--line-height));
|
||||
}
|
||||
&[data-size=lg] {
|
||||
height: calc(var(--spacing) * 12);
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||
&:is(:where(.group)[data-collapsible="icon"] *) {
|
||||
padding: calc(var(--spacing) * 0) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
> details {
|
||||
&:not([open]) {
|
||||
> summary {
|
||||
&::after {
|
||||
rotate: calc(90deg * -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
> summary {
|
||||
&::after {
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
width: calc(var(--spacing) * 3.5);
|
||||
height: calc(var(--spacing) * 3.5);
|
||||
background-color: var(--color-primary);
|
||||
transition-property: transform, translate, scale, rotate;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
--tw-ease: linear;
|
||||
transition-timing-function: linear;
|
||||
--tw-content: '';
|
||||
content: var(--tw-content);
|
||||
mask-image: var(--chevron-down-icon);
|
||||
mask-size: 1rem;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
&::details-content {
|
||||
padding-inline: calc(var(--spacing) * 3.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: calc(var(--spacing) * 0);
|
||||
--tw-translate-x: 1px;
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
flex-direction: column;
|
||||
gap: calc(var(--spacing) * 1);
|
||||
border-left-style: var(--tw-border-style);
|
||||
border-left-width: 1px;
|
||||
border-color: var(--color-sidebar-border);
|
||||
padding-inline: calc(var(--spacing) * 2.5);
|
||||
padding-block: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5243,12 +5273,12 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--color-primary);
|
||||
background-color: var(--color-foreground);
|
||||
padding-inline: calc(var(--spacing) * 3);
|
||||
padding-block: calc(var(--spacing) * 1.5);
|
||||
font-size: var(--text-xs);
|
||||
line-height: var(--tw-leading, var(--text-xs--line-height));
|
||||
color: var(--color-primary-foreground);
|
||||
color: var(--color-background);
|
||||
opacity: 0%;
|
||||
transition-property: all;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
@@ -5523,6 +5553,11 @@
|
||||
}
|
||||
dd {
|
||||
margin-top: calc(var(--spacing) * 2);
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));
|
||||
margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
dl {
|
||||
margin-block: calc(var(--spacing) * 0);
|
||||
|
||||
@@ -76,7 +76,7 @@ toc:
|
||||
Trash
|
||||
</div>
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
{% endset %}
|
||||
|
||||
{{ code_preview("button-group-default", code_default | prettyHtml) }}
|
||||
@@ -131,7 +131,7 @@ toc:
|
||||
<button type="button" class="btn-icon-outline">{% lucide "plus" %}</button>
|
||||
</div>
|
||||
<div role="group" class="button-group">
|
||||
<button type="button" class="btn-lg-outline">Small</button>
|
||||
<button type="button" class="btn-lg-outline">Large</button>
|
||||
<button type="button" class="btn-lg-outline">Button</button>
|
||||
<button type="button" class="btn-lg-outline">Group</button>
|
||||
<button type="button" class="btn-lg-icon-outline">{% lucide "plus" %}</button>
|
||||
@@ -287,4 +287,4 @@ toc:
|
||||
</div>
|
||||
{% endset %}
|
||||
|
||||
{{ code_preview("button-group-popover", code_popover | prettyHtml) }}
|
||||
{{ code_preview("button-group-popover", code_popover | prettyHtml) }}
|
||||
|
||||
@@ -118,7 +118,14 @@ toc:
|
||||
</ul>
|
||||
<dl>
|
||||
<dt><code class="highlight language-html"><div role="option" data-value="{ VALUE }"></code></dt>
|
||||
<dd>Option that can be selected.Should have a unique id if you use the <code>aria-activedescendant</code> attribute on the trigger.</dd>
|
||||
<dd>Option that can be selected. Should have a unique id if you use the <code>aria-activedescendant</code> attribute on the trigger. Optional attributes:
|
||||
<ul>
|
||||
<li><code>data-filter</code>: custom text used for filtering (if different from displayed content, e.g., <code>data-filter="united states"</code>)</li>
|
||||
<li><code>data-keywords</code>: additional keywords for enhanced filtering (e.g., <code>data-keywords="bread fruit"</code>)</li>
|
||||
<li><code>data-force</code>: forces the option to always display, regardless of filter (e.g., <code>data-force</code>)</li>
|
||||
<li><code>aria-disabled="true"</code>: marks option as disabled (excluded from selection and keyboard navigation)</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt><code class="highlight language-html"><hr role="separator"></code> <span class="badge-secondary">Optional</span></dt>
|
||||
<dd>Separator between groups/options.</dd>
|
||||
<dt><code class="highlight language-html"><div role="group"></code> <span class="badge-secondary">Optional</span></dt>
|
||||
|
||||
@@ -33,15 +33,15 @@ toc:
|
||||
{% macro command_items(suggestions_id="suggestions", settings_id="settings") %}
|
||||
<div role="group" aria-labelledby="{{ suggestions_id }}">
|
||||
<span role="heading" id="{{ suggestions_id }}">Suggestions</span>
|
||||
<div role="menuitem" data-value="calendar" onclick="console.log('Open calendar')">
|
||||
<div role="menuitem" onclick="console.log('Open calendar')">
|
||||
{% lucide "calendar" %}
|
||||
<span>Calendar</span>
|
||||
</div>
|
||||
<div role="menuitem" data-value="search-emoji" onclick="console.log('Search emoji')">
|
||||
<div role="menuitem" onclick="console.log('Search emoji')">
|
||||
{% lucide "smile" %}
|
||||
<span>Search Emoji</span>
|
||||
</div>
|
||||
<div role="menuitem" data-value="calculator" aria-disabled="true">
|
||||
<div role="menuitem" aria-disabled="true">
|
||||
{% lucide "calculator" %}
|
||||
<span>Calculator</span>
|
||||
</div>
|
||||
@@ -49,17 +49,17 @@ toc:
|
||||
<hr role="separator" />
|
||||
<div role="group" aria-labelledby="{{ settings_id }}">
|
||||
<span role="heading" id="{{ settings_id }}">Settings</span>
|
||||
<div role="menuitem" data-value="profile" onclick="console.log('Open profile')">
|
||||
<div role="menuitem" onclick="console.log('Open profile')" data-filter="Profile">
|
||||
{% lucide "user" %}
|
||||
<span>Profile</span>
|
||||
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘P</kbd>
|
||||
</div>
|
||||
<div role="menuitem" data-value="billing" onclick="console.log('Open billing')">
|
||||
<div role="menuitem" onclick="console.log('Open billing')" data-filter="Billing">
|
||||
{% lucide "credit-card" %}
|
||||
<span>Billing</span>
|
||||
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘B</kbd>
|
||||
</div>
|
||||
<div role="menuitem" data-value="settings" onclick="console.log('Open settings')">
|
||||
<div role="menuitem" onclick="console.log('Open settings')" data-filter="Settings">
|
||||
{% lucide "settings" %}
|
||||
<span>Settings</span>
|
||||
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘S</kbd>
|
||||
@@ -155,11 +155,13 @@ toc:
|
||||
<dt><code class="highlight language-html"><div role="menu"></code></dt>
|
||||
<dd>The menu containing the command items. Should have <code>id="{ MENU_ID }"</code> refered to by the input's <code>aria-controls</code> attribute.
|
||||
<dl>
|
||||
<dt><code class="highlight language-html"><div role="menuitem"></code></dt>
|
||||
<dd>Command item that triggers an action when clicked. In a command-dialog, clicking a menuitem automatically closes the dialog unless the <code>data-keep-command-open</code> attribute is present. Optional attributes:
|
||||
<dt><code class="highlight language-html"><div role="menuitem"></code> or <code class="highlight language-html"><a role="menuitem"></code></dt>
|
||||
<dd>Command item that triggers an action when clicked. Use <code><a></code> for navigation links, or <code><div></code> for actions. In a command-dialog, clicking a menuitem automatically closes the dialog unless the <code>data-keep-command-open</code> attribute is present. Optional attributes:
|
||||
<ul>
|
||||
<li><code>data-label</code>: custom label for filtering (if different from text content)</li>
|
||||
<li><code>data-keywords</code>: additional keywords for enhanced filtering</li>
|
||||
<li><code>href</code>: If present on an <code><a></code>, the item navigates to that URL</li>
|
||||
<li><code>data-filter</code>: custom text used for filtering (if different from displayed content, e.g., <code>data-filter="calendar event"</code>)</li>
|
||||
<li><code>data-keywords</code>: additional keywords for enhanced filtering (e.g., <code>data-keywords="calendar event date"</code>)</li>
|
||||
<li><code>data-force</code>: forces the item to always display, regardless of filter (e.g., <code>data-force</code>)</li>
|
||||
<li><code>aria-disabled="true"</code>: marks item as disabled (excluded from keyboard navigation and filtering)</li>
|
||||
</ul>
|
||||
</dd>
|
||||
@@ -209,17 +211,42 @@ toc:
|
||||
</div>
|
||||
|
||||
{% set raw_code %}{% raw %}{% call command() %}
|
||||
<div role="menuitem" data-value="calendar">
|
||||
<div role="menuitem">
|
||||
<svg>...</svg>
|
||||
<span>Calendar</span>
|
||||
</div>
|
||||
<div role="menuitem" data-value="settings">
|
||||
<div role="menuitem" data-keywords="config preferences">
|
||||
<svg>...</svg>
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
{% endcall %}{% endraw %}{% endset %}
|
||||
{{ code_block(raw_code, "jinja") }}
|
||||
|
||||
<section class="prose">
|
||||
<p>You can also use the <code>items</code> parameter to pass an array of items:</p>
|
||||
</section>
|
||||
|
||||
{% set icon_calendar %}{% lucide "calendar" %}{% endset %}
|
||||
{% set icon_settings %}{% lucide "settings" %}{% endset %}
|
||||
{% set raw_code_items %}{% raw %}{% set icon_calendar %}{% lucide "calendar" %}{% endset %}
|
||||
{% set icon_settings %}{% lucide "settings" %}{% endset %}
|
||||
{% set items = [
|
||||
{ type: "group", label: "Suggestions", items: [
|
||||
{ label: "Calendar", icon: icon_calendar, keywords: "date event" },
|
||||
{ label: "Search Emoji", icon: icon_settings },
|
||||
{ label: "Calculator", disabled: true }
|
||||
]},
|
||||
{ type: "separator" },
|
||||
{ type: "group", label: "Settings", items: [
|
||||
{ label: "Profile", url: "/profile", icon: icon_settings, keywords: "user account" },
|
||||
{ label: "Billing", url: "/billing", icon: icon_settings },
|
||||
{ label: "Settings", url: "/settings", icon: icon_settings }
|
||||
]}
|
||||
] %}
|
||||
|
||||
{% call command(items=items) %}{% endcall %}{% endraw %}{% endset %}
|
||||
{{ code_block(raw_code_items, "jinja") }}
|
||||
|
||||
<h2 id="examples"><a href="#examples">Examples</a></h2>
|
||||
|
||||
<h3 id="example-dialog"><a href="#example-dialog">Dialog</a></h3>
|
||||
@@ -258,4 +285,4 @@ toc:
|
||||
</script>
|
||||
{% endset %}
|
||||
|
||||
{{ code_preview("command-dialog", code_dialog | prettyHtml) }}
|
||||
{{ code_preview("command-dialog", code_dialog | prettyHtml) }}
|
||||
|
||||
+131
-19
@@ -32,6 +32,8 @@ toc:
|
||||
id: example-disabled
|
||||
- label: With icon
|
||||
id: example-with-icon
|
||||
- label: Multiple
|
||||
id: example-multiple
|
||||
---
|
||||
|
||||
{% from "macros/code_preview.njk" import code_preview %}
|
||||
@@ -58,8 +60,8 @@ toc:
|
||||
type: "group",
|
||||
label: "Fruits",
|
||||
items: [
|
||||
{ type: "item", value: "apple", label: "Apple" },
|
||||
{ type: "item", value: "banana", label: "Banana" },
|
||||
{ type: "item", value: "apple", label: "Apple", attrs: {"data-force": "true"} },
|
||||
{ type: "item", value: "banana", label: "Banana", attrs: {"data-keywords": "bread"} },
|
||||
{ type: "item", value: "blueberry", label: "Blueberry" },
|
||||
{ type: "item", value: "pineapple", label: "Grapes" },
|
||||
{ type: "item", value: "pineapple", label: "Pineapple" }
|
||||
@@ -120,44 +122,62 @@ toc:
|
||||
<section class="prose">
|
||||
<dl>
|
||||
<dt><code class="highlight language-html"><div class="select"></code></dt>
|
||||
<dd>Wraps around the entire component.
|
||||
<dd>
|
||||
<p>Wraps around the entire component. Can have the following attributes:</p>
|
||||
<ul>
|
||||
<li><code>data-placeholder="{ TEXT }"</code> <span class="badge-secondary">Optional</span>: placeholder text shown when no options are selected (multiselect only).</li>
|
||||
</ul>
|
||||
<dl>
|
||||
<dt><code class="highlight language-html"><button type="button" popovertarget="{ POPOVER_ID }"></code></dt>
|
||||
<dt><code class="highlight language-html"><button type="button"></code></dt>
|
||||
<dd>
|
||||
<p>The trigger to open the popover, can also have the following attributes:</p>
|
||||
<p>The trigger to open the popover. Should have the following attributes:</p>
|
||||
<ul>
|
||||
<li><code>id="{BUTTON_ID}"</code>: linked to by the <code>aria-labelledby</code> attribute of the listbox.</li>
|
||||
<li><code>aria-haspopup="listbox"</code>: indicates that the button opens a listbox.</li>
|
||||
<li><code>aria-controls="{ LISTBOX_ID }"</code>: points to the listbox's id.</li>
|
||||
<li><code>aria-expanded="false"</code>: tracks the popover's state.</li>
|
||||
<li><code>aria-activedescendant="{ OPTION_ID }"</code>: points to the active option's id.</li>
|
||||
<li><code>aria-activedescendant="{ OPTION_ID }"</code> <span class="badge-secondary">Optional</span>: points to the active option's id.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt><code class="highlight language-html"><div popover class="popover" id="{ POPOVER_ID }"></code></dt>
|
||||
<dd>As with the <a href="/components/popover">Popover</a> component, you can set up the side and alignment of the popover using the <code>data-side</code> and <code>data-align</code> attributes.
|
||||
<dt><code class="highlight language-html"><div data-popover aria-hidden="true" id="{ POPOVER_ID }"></code></dt>
|
||||
<dd>The popover content. You can set up the side and alignment using the <code>data-side</code> and <code>data-align</code> attributes (see <a href="/components/popover">Popover</a> component).
|
||||
<dl>
|
||||
<dt><code class="highlight language-html"><div role="listbox"></code></dt>
|
||||
<dd>The listbox containing the options. Should have the following attributes:
|
||||
<dd>
|
||||
<p>The listbox containing the options. Should have the following attributes:</p>
|
||||
<ul>
|
||||
<li><code>id="{ LISTBOX_ID }"</code>: refered to by the <code>aria-controls</code> attribute of the trigger.</li>
|
||||
<li><code>aria-labelledby="{ BUTTON_ID }"</code>: linked to by the button's <code>id</code> attribute.</li>
|
||||
<li><code>aria-multiselectable="true"</code> <span class="badge-secondary">Optional</span>: enables multiple selection mode.</li>
|
||||
</ul>
|
||||
<dl>
|
||||
<dt><code class="highlight language-html"><div role="option" data-value="{ VALUE }"></code></dt>
|
||||
<dd>Option that can be selected.Should have a unique id if you use the <code>aria-activedescendant</code> attribute on the trigger.</dd>
|
||||
<dd>
|
||||
<p>Option that can be selected. Should have the following attributes:</p>
|
||||
<ul>
|
||||
<li><code>id="{ OPTION_ID }"</code> <span class="badge-secondary">Optional</span>: unique id for this option (needed if you use <code>aria-activedescendant</code> on the trigger).</li>
|
||||
<li><code>data-value="{ VALUE }"</code>: the value for this option.</li>
|
||||
<li><code>data-label="{ LABEL }"</code> <span class="badge-secondary">Optional</span>: the text label to use for this option when selected in multiple mode. If not provided, the option's text content will be used (HTML stripped).</li>
|
||||
<li><code>aria-selected="true"</code> <span class="badge-secondary">Optional</span>: indicates this option is selected.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt><code class="highlight language-html"><hr role="separator"></code> <span class="badge-secondary">Optional</span></dt>
|
||||
<dd>Separator between groups/options.</dd>
|
||||
<dt><code class="highlight language-html"><div role="group"></code> <span class="badge-secondary">Optional</span></dt>
|
||||
<dd>Group of options, can have a <code>aria-labelledby</code> attribute to link to a heading.</dd>
|
||||
<dt><code class="highlight language-html"><span role="heading"></code></dt>
|
||||
<dt><code class="highlight language-html"><span role="heading"></code> <span class="badge-secondary">Optional</span></dt>
|
||||
<dd>Group heading, must have an <code>id</code> attribute if you use the <code>aria-labelledby</code> attribute on the group.</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
</dl>
|
||||
</dl>
|
||||
</dd>
|
||||
<dt><code class="highlight language-html"><input type="hidden" name="{ NAME }" value="{ VALUE }"></code> <span class="badge-secondary">Optional</span></dt>
|
||||
<dd>The hidden input that holds the value of the field (if needed).</dd>
|
||||
<dt><code class="highlight language-html"><input type="hidden" name="{ NAME }" value="{ VALUE }"></code></dt>
|
||||
<dd>
|
||||
<p>The hidden input that holds the selected value.</p>
|
||||
<p>For multiselect: multiple hidden inputs with the same name will be created (one for each selected value). When no options are selected, a single input with an empty value is submitted.</p>
|
||||
<p><strong>Note:</strong> This differs slightly from native <code><select multiple></code> behavior, which submits nothing when empty. This allows the component to work without JavaScript for initial rendering while maintaining a simpler architecture.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
@@ -170,7 +190,13 @@ toc:
|
||||
<dt><code>basecoat:popover</code></dt>
|
||||
<dd>When the popover opens, the component dispatches a custom (non-bubbling) <code>basecoat:popover</code> event on <code>document</code>. Other popover components (Combobox, Dropdown Menu, Popover and Select) listen for this to close any open popovers.</dd>
|
||||
<dt><code>change</code></dt>
|
||||
<dd>When the selected value changes, the component dispatches a custom (bubbling) <code>change</code> event on itself, with the selected value in <code>event.detail</code> (e.g. <code>{ detail: { value: "something" }}</code>).</dd>
|
||||
<dd>
|
||||
<p>When the selected value changes, the component dispatches a custom (bubbling) <code>change</code> event on itself, with the selected value in <code>event.detail.value</code>:</p>
|
||||
<ul>
|
||||
<li>Single select: <code>{ detail: { value: "something" }}</code> (string)</li>
|
||||
<li>Multiple select: <code>{ detail: { value: ["item1", "item2"] }}</code> (array)</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
@@ -178,9 +204,9 @@ toc:
|
||||
|
||||
<section class="prose">
|
||||
<dl>
|
||||
<dt><code>selectByValue</code></dt>
|
||||
<dt><code>selectByValue(value)</code></dt>
|
||||
<dd>
|
||||
<p>You can call this method on the component after it is initialized to select an option by value (i.e. the option with the matching <code>data-value</code> attribute):</p>
|
||||
<p>Selects an option by value (i.e. the option with the matching <code>data-value</code> attribute). For single-select, this will close the popover. For multiselect, this adds the value to the selection without removing existing selections.</p>
|
||||
|
||||
{% set code_script %}<script>
|
||||
const selectComponent = document.querySelector('#my-select');
|
||||
@@ -190,6 +216,30 @@ toc:
|
||||
</script>{% endset %}
|
||||
{{ code_block(code_script | prettyHtml, "html") }}
|
||||
</dd>
|
||||
<dt><code>selectAll()</code> <span class="badge-secondary">Multiselect only</span></dt>
|
||||
<dd>
|
||||
<p>Selects all available options. Only available when the component is in multiselect mode (<code>aria-multiselectable="true"</code>).</p>
|
||||
|
||||
{% set code_script_all %}<script>
|
||||
const selectComponent = document.querySelector('#my-multiselect');
|
||||
selectComponent.addEventListener('basecoat:initialized', () => {
|
||||
selectComponent.selectAll();
|
||||
});
|
||||
</script>{% endset %}
|
||||
{{ code_block(code_script_all | prettyHtml, "html") }}
|
||||
</dd>
|
||||
<dt><code>selectNone()</code> <span class="badge-secondary">Multiselect only</span></dt>
|
||||
<dd>
|
||||
<p>Deselects all options. Only available when the component is in multiselect mode (<code>aria-multiselectable="true"</code>).</p>
|
||||
|
||||
{% set code_script_none %}<script>
|
||||
const selectComponent = document.querySelector('#my-multiselect');
|
||||
selectComponent.addEventListener('basecoat:initialized', () => {
|
||||
selectComponent.selectNone();
|
||||
});
|
||||
</script>{% endset %}
|
||||
{{ code_block(code_script_none | prettyHtml, "html") }}
|
||||
</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
@@ -204,11 +254,11 @@ toc:
|
||||
Use Nunjucks or Jinja macros
|
||||
{% lucide "arrow-right" %}
|
||||
</a>
|
||||
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/dialog.html.jinja" target="_blank">
|
||||
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/select.html.jinja" target="_blank">
|
||||
Jinja macro
|
||||
{% lucide "arrow-right" %}
|
||||
</a>
|
||||
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/dialog.njk" target="_blank">
|
||||
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/select.njk" target="_blank">
|
||||
Nunjucks macro
|
||||
{% lucide "arrow-right" %}
|
||||
</a>
|
||||
@@ -259,11 +309,29 @@ toc:
|
||||
|
||||
{{ code_preview("select-disabled", code | prettyHtml) }}
|
||||
|
||||
<h3 id="example-invalid-state"><a href="#example-invalid-state">Invalid State</a></h3>
|
||||
|
||||
{% set code %}
|
||||
{{ select(
|
||||
trigger_attrs={"aria-invalid": "true"},
|
||||
items=[
|
||||
{ value: "", label: "Select a role" },
|
||||
{ value: "admin", label: "Admin" },
|
||||
{ value: "editor", label: "Editor" },
|
||||
{ value: "viewer", label: "Viewer" },
|
||||
{ value: "guest", label: "Guest" }
|
||||
]
|
||||
) }}
|
||||
{% endset %}
|
||||
|
||||
{{ code_preview("select-disabled", code | prettyHtml) }}
|
||||
|
||||
<h3 id="example-with-icon"><a href="#example-with-icon">With icon</a></h3>
|
||||
|
||||
{% set code %}
|
||||
{% call select(
|
||||
id="select-with-icons",
|
||||
placeholder="Select a chart...",
|
||||
trigger_attrs={'class': 'w-[180px]'}
|
||||
) %}
|
||||
<div type="button" role="option" data-value="bar">
|
||||
@@ -287,4 +355,48 @@ toc:
|
||||
{% endcall %}
|
||||
{% endset %}
|
||||
|
||||
{{ code_preview("select-scrollable", code | prettyHtml) }}
|
||||
{{ code_preview("select-with-icons", code | prettyHtml) }}
|
||||
|
||||
<h3 id="example-multiple"><a href="#example-multiple">Multiple</a></h3>
|
||||
|
||||
<section class="prose">
|
||||
<p>Enable multiple selection by setting <code>multiple=true</code>. The selected options are displayed as a comma-separated list in the trigger button. Use <code>data-label</code> to specify clean text labels for options that contain HTML (like icons), otherwise the text content will be extracted automatically.</p>
|
||||
</section>
|
||||
|
||||
{% set code %}
|
||||
{% call select(
|
||||
id="select-multiple",
|
||||
name="chart-types",
|
||||
multiple=true,
|
||||
placeholder="Select chart types...",
|
||||
selected=["bar", "line"],
|
||||
trigger_attrs={"class": "w-[220px]"}
|
||||
) %}
|
||||
<div role="option" data-value="bar" data-label="Bar">
|
||||
<span class="flex items-center gap-2">
|
||||
{% lucide "chart-bar", {"class": "text-muted-foreground"} %}
|
||||
Bar
|
||||
</span>
|
||||
</div>
|
||||
<div role="option" data-value="line" data-label="Line">
|
||||
<span class="flex items-center gap-2">
|
||||
{% lucide "chart-line", {"class": "text-muted-foreground"} %}
|
||||
Line
|
||||
</span>
|
||||
</div>
|
||||
<div role="option" data-value="pie" data-label="Pie">
|
||||
<span class="flex items-center gap-2">
|
||||
{% lucide "chart-pie", {"class": "text-muted-foreground"} %}
|
||||
Pie
|
||||
</span>
|
||||
</div>
|
||||
<div role="option" data-value="area" data-label="Area">
|
||||
<span class="flex items-center gap-2">
|
||||
{% lucide "chart-area", {"class": "text-muted-foreground"} %}
|
||||
Area
|
||||
</span>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endset %}
|
||||
|
||||
{{ code_preview("select-multiple", code | prettyHtml) }}
|
||||
|
||||
@@ -112,7 +112,8 @@ toc:
|
||||
<div class="prose">
|
||||
<p>Or cherry-pick specific components, for example:</p>
|
||||
</div>
|
||||
{% set code %}{% raw %}import 'basecoat-css/popover';
|
||||
{% set code %}{% raw %}import 'basecoat-css/basecoat';
|
||||
import 'basecoat-css/popover';
|
||||
import 'basecoat-css/tabs';{% endraw %}{% endset %}
|
||||
{{ code_block(code, "js") }}
|
||||
<div class="prose">
|
||||
@@ -143,7 +144,7 @@ import 'basecoat-css/tabs';{% endraw %}{% endset %}
|
||||
<h2 id="install-js"><a href="#install-js">Components with JavaScript</a></h2>
|
||||
|
||||
<div class="prose">
|
||||
<p><b>A handful of components require JavaScript code to work.</b>, specifically <a href="/components/dropdown-menu">Dropdown Menu</a>, <a href="/components/popover">Popover</a>, <a href="/components/select">Select</a>, <a href="/components/sidebar">Sidebar</a>, <a href="/components/tabs">Tabs</a> and <a href="/components/toast">Toast</a></p>
|
||||
<p><b>A handful of components require JavaScript code to work</b>, specifically <a href="/components/dropdown-menu">Dropdown Menu</a>, <a href="/components/popover">Popover</a>, <a href="/components/select">Select</a>, <a href="/components/sidebar">Sidebar</a>, <a href="/components/tabs">Tabs</a> and <a href="/components/toast">Toast</a></p>
|
||||
|
||||
<p>If a component requires JavaScript, the documentation page will provide instructions. There are 2 options to add the JavaScript code to your project:</p>
|
||||
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
Basecoat – LLMs Guide
|
||||
|
||||
About
|
||||
- Basecoat is a Tailwind‑first component library using semantic HTML and small vanilla JS for interactivity. No React required. Framework‑agnostic.
|
||||
|
||||
Install
|
||||
- Tailwind project: `npm i basecoat-css`
|
||||
- In CSS: `@import "tailwindcss";` then `@import "basecoat-css";`
|
||||
|
||||
CDN
|
||||
- CSS: `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/basecoat.cdn.min.css">`
|
||||
- JS (all): `<script src="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/js/all.min.js" defer></script>`
|
||||
- JS (per component): include `basecoat.min.js` then `tabs.min.js`, `select.min.js`, etc.
|
||||
|
||||
JavaScript
|
||||
- Auto‑init on `DOMContentLoaded`; observes DOM for new nodes.
|
||||
- Manual: `window.basecoat.init('<name>')` or `window.basecoat.initAll()`.
|
||||
- Packages to import with ESM: `import 'basecoat-css/all'` or `import 'basecoat-css/tabs'`.
|
||||
|
||||
Usage
|
||||
- CSS‑only components: use classes like `btn`, `card`, `input`, etc.
|
||||
- JS components: include the JS and use required roles/attributes (see below).
|
||||
|
||||
Theming
|
||||
- Tailwind projects can override design tokens (e.g., `primary`, `muted-foreground`).
|
||||
- Without Tailwind, use CDN CSS and add overrides after the link tag.
|
||||
|
||||
Common pitfalls
|
||||
- Do not invent class names; use documented classes only.
|
||||
- The package name is `basecoat-css` (not `basecoat`, not `@basecoat/css`).
|
||||
- Interactive components require specific roles/attributes; include them.
|
||||
- Basecoat is framework‑agnostic; do not mention React/Radix dependencies.
|
||||
|
||||
Components
|
||||
- Button (CSS): `.btn`, variants like `.btn-secondary`, sizes `.btn-sm`, `.btn-lg`.
|
||||
- Button Group (CSS): `.button-group` to group adjacent buttons; supports horizontal/vertical.
|
||||
- Input (CSS): `.input` for text inputs; pairs with `.label` and `.field` patterns.
|
||||
- Textarea (CSS): `.textarea` styled multiline input.
|
||||
- Label (CSS): `.label` for associated form labels.
|
||||
- Field (CSS): `.field` wrapper for label, input, hint, error.
|
||||
- Form (CSS): `.form` patterns for layout and validation styling.
|
||||
- Checkbox (CSS): styled native checkbox; use `input[type='checkbox']`.
|
||||
- Radio Group (CSS): `.radio-group` pattern using native radios.
|
||||
- Switch (CSS): styled checkbox with `role="switch"` if needed.
|
||||
- Select (JS): `.select` custom select; trigger button + popover + `[role=listbox]` and `[role=option]`. Dispatches `change` with `detail.value`.
|
||||
- Combobox (JS): `.combobox` typeahead; similar roles to Select.
|
||||
- Command (JS): `.command` with search input and `[role=menu]`/`[role=menuitem]`; keyboard nav.
|
||||
- Tabs (JS): `.tabs` with `[role=tablist]`, `[role=tab]`, `aria-controls` panels; arrow keys switch.
|
||||
- Popover (JS): `.popover` wraps a button and `[data-popover]` content; toggles `aria-hidden`.
|
||||
- Dropdown Menu (JS): `.dropdown-menu` trigger + menu with `[role=menu]`/`[role=menuitem]`.
|
||||
- Dialog (CSS/Native): `<dialog>` styled; `.command-dialog` variant wraps `.command`.
|
||||
- Alert Dialog (CSS/Native): confirm/cancel flow using `<dialog>` semantics.
|
||||
- Drawer (CSS/Native): off‑canvas using `<dialog>`/popover; class names documented in examples.
|
||||
- Tooltip (CSS/JS-lite): `.tooltip` small info overlays.
|
||||
- Toast (JS): global notifications container `#toaster`; dispatch `basecoat:toast` with `{ detail: { config } }`.
|
||||
- Sidebar (JS): `.sidebar` with collapsible items; tracks current page via `aria-current`.
|
||||
- Breadcrumb (CSS): `.breadcrumb` list of links.
|
||||
- Pagination (CSS): `.pagination` navigation controls.
|
||||
- Card (CSS): `.card` container with header/section/footer slots.
|
||||
- Alert (CSS): `.alert` contextual banners.
|
||||
- Badge (CSS): `.badge` and `.badge-outline`.
|
||||
- Avatar (CSS): `.avatar` image + fallback.
|
||||
- Skeleton (CSS): `.skeleton` loading placeholder.
|
||||
- Progress (CSS): `.progress` bar.
|
||||
- Slider (CSS): `.slider` styled input range.
|
||||
- Spinner (CSS): `.spinner` loading indicator.
|
||||
- Kbd (CSS): `.kbd` keyboard key style.
|
||||
- Table (CSS): `.table` basic and striped tables.
|
||||
- Item (CSS): `.item` list rows with icons and actions.
|
||||
- Empty State (CSS): `.empty` placeholder content.
|
||||
- Theme Switcher (JS-lite): `.theme-switcher` toggles themes (example in docs).
|
||||
|
||||
Events
|
||||
- `basecoat:toast` (document): show a toast by dispatching `{ detail: { config } }`.
|
||||
- `basecoat:initialized` (component root): emitted after a component initializes.
|
||||
- `basecoat:popover` (document): coordination event to close other popovers.
|
||||
|
||||
Accessibility
|
||||
- Use semantic roles/attributes from examples (tabs, menus, listbox/options).
|
||||
- Disabled items: use `disabled` or `aria-disabled="true"`; JS will ignore them.
|
||||
|
||||
Troubleshooting
|
||||
- If a component doesn’t work, ensure its JS is loaded and required roles/IDs match examples.
|
||||
- For dynamic content, call `window.basecoat.init('<name>')` or `initAll()` after insertion.
|
||||
|
||||
License
|
||||
- MIT (see LICENSE.md)
|
||||
---
|
||||
description: Basecoat
|
||||
alwaysApply: true
|
||||
applyTo: "**"
|
||||
downloadedFrom: https://basecoatui.com/llms.txt
|
||||
version: 0.3.x
|
||||
---
|
||||
|
||||
# Basecoat
|
||||
Basecoat is a Tailwind CSS v4‑first UI library using semantic HTML and small vanilla JS for interactivity. No React required; framework‑agnostic.
|
||||
|
||||
- Basecoat docs: https://basecoatui.com
|
||||
- Package CSS: `basecoat-css`
|
||||
|
||||
## Basecoat install notes
|
||||
1. Basecoat targets Tailwind CSS v4.
|
||||
2. Import Tailwind in your CSS, then import Basecoat:
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "basecoat-css";
|
||||
```
|
||||
3. For interactive components, include JS. With ESM you can load all or cherry‑pick:
|
||||
```js
|
||||
import 'basecoat-css/all';
|
||||
// or cherry‑pick
|
||||
import 'basecoat-css/tabs';
|
||||
import 'basecoat-css/select';
|
||||
import 'basecoat-css/popover';
|
||||
import 'basecoat-css/dropdown-menu';
|
||||
import 'basecoat-css/sidebar';
|
||||
import 'basecoat-css/toast';
|
||||
```
|
||||
4. CDN usage (no build tool):
|
||||
```html
|
||||
<link href="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/basecoat.cdn.min.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@latest/dist/js/all.min.js" defer></script>
|
||||
```
|
||||
5. JS auto‑initializes on `DOMContentLoaded` and observes the DOM for added nodes. You can manually re‑init:
|
||||
```js
|
||||
window.basecoat.init('tabs');
|
||||
window.basecoat.initAll();
|
||||
```
|
||||
|
||||
## Basecoat usage rules
|
||||
1. Add Basecoat class names to semantic HTML. Combine with Tailwind utilities for layout/spacing.
|
||||
2. Interactive components require specific roles/attributes (see each component). Keep `aria-*` for semantics and `data-*` for visual states.
|
||||
3. Disabled elements must use `disabled` or `aria-disabled="true"`. Basecoat JS filters them from interactions.
|
||||
4. Only use documented Basecoat classes or Tailwind utilities. Don’t invent new class names.
|
||||
5. Prefer native elements (button, input, dialog) and keep markup minimal.
|
||||
|
||||
Basecoat class name categories (for reference)
|
||||
- `component`: the main component class
|
||||
- `part`: a child element/slot of a component
|
||||
- `style`: variants changing appearance
|
||||
- `size`: size variants
|
||||
- `behavior`: classes that change behavior
|
||||
- `modifier`: boolean/conditional changes
|
||||
|
||||
## Config
|
||||
Basecoat has no Tailwind plugin config. Import the CSS after Tailwind. Customize design tokens via your Tailwind theme or overrides.
|
||||
|
||||
Minimal CSS setup:
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "basecoat-css";
|
||||
```
|
||||
|
||||
## Basecoat tokens (commonly used)
|
||||
These semantic tokens are referenced by classes (examples): `background`, `foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `muted`, `muted-foreground`, `card`, `card-foreground`, `popover`, `popover-foreground`, `destructive`, `ring`, `input`, `border`.
|
||||
|
||||
Rules
|
||||
- Tokens should be defined in your Tailwind theme or be available via your design system. You can override colors using Tailwind utilities (e.g., `bg-primary`).
|
||||
- Prefer utilities for one‑off overrides; keep semantic tokens consistent.
|
||||
|
||||
## Basecoat components
|
||||
|
||||
### alert
|
||||
Alerts inform users about important events.
|
||||
|
||||
#### Class names
|
||||
- component: `alert`, `alert-destructive`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<div role="alert" class="alert">{CONTENT}</div>
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- Use `alert-destructive` for destructive context.
|
||||
|
||||
### avatar
|
||||
Avatar shows a user thumbnail.
|
||||
|
||||
#### Class names
|
||||
- component: `avatar`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<div class="avatar"><img src="{image-url}" alt="{alt}" /></div>
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- Provide meaningful `alt` text or decorative as appropriate.
|
||||
|
||||
### badge
|
||||
Badges display statuses or counts.
|
||||
|
||||
#### Class names
|
||||
- component: `badge`
|
||||
- style: `badge-outline`, `badge-destructive`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<span class="badge {MODIFIER}">{TEXT}</span>
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- {MODIFIER} is optional; combine with utilities for spacing.
|
||||
|
||||
### button
|
||||
Buttons trigger actions.
|
||||
|
||||
#### Class names
|
||||
- component: `btn`
|
||||
- style: `btn-primary`, `btn-secondary`, `btn-outline`, `btn-ghost`, `btn-link`, `btn-destructive`
|
||||
- size: `btn-sm`, `btn-lg`
|
||||
- icon: `btn-icon`, `btn-sm-icon`, `btn-lg-icon`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<button class="btn {MODIFIER}">{CONTENT}</button>
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- Use `disabled` attribute for disabled state. Use `aria-pressed="true"` for toggle buttons.
|
||||
|
||||
### button-group
|
||||
Groups adjacent buttons into a single control.
|
||||
|
||||
#### Class names
|
||||
- component: `button-group`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<div class="button-group" data-orientation="{horizontal|vertical}">{BUTTONS}</div>
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- Use separators with `<hr role="separator">` inside the group if needed.
|
||||
|
||||
### card
|
||||
Card container with optional header/section/footer.
|
||||
|
||||
#### Class names
|
||||
- component: `card`
|
||||
- part: `> header`, `> section`, `> footer`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<article class="card">
|
||||
<header><h2>{TITLE}</h2><p>{SUBTITLE}</p></header>
|
||||
<section>{CONTENT}</section>
|
||||
<footer>{ACTIONS}</footer>
|
||||
</article>
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- Parts are optional; structure with semantic elements.
|
||||
|
||||
### checkbox (CSS)
|
||||
Styled native checkbox.
|
||||
|
||||
#### Class names
|
||||
- component: `.input[type='checkbox']:not([role='switch'])`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<input type="checkbox" class="input" />
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- Use standard checkbox semantics; pair with `<label>`.
|
||||
|
||||
### command (JS)
|
||||
Command palette with search and results.
|
||||
|
||||
#### Class names
|
||||
- component: `command`, wrapper variant: `command-dialog`
|
||||
- part: `> header input`, `[role='menu']`, `[role='menuitem']`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<div class="command">
|
||||
<header><input type="text" placeholder="Search" /></header>
|
||||
<div role="menu">
|
||||
<button role="menuitem">{ITEM}</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- Requires JS. Keyboard navigation supported. Disabled items must use `aria-disabled="true"` or `disabled` and are ignored.
|
||||
|
||||
### dropdown-menu (JS)
|
||||
Contextual menu triggered by a button.
|
||||
|
||||
#### Class names
|
||||
- component: `dropdown-menu`
|
||||
- part: `> button` (trigger), `[role='menu']`, `[role='menuitem']`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<div class="dropdown-menu">
|
||||
<button type="button">{TRIGGER}</button>
|
||||
<div role="menu"> <button role="menuitem">{ITEM}</button> </div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- Requires JS. Uses `basecoat:popover` to coordinate with other popovers.
|
||||
|
||||
### input (CSS)
|
||||
Styled text input fields.
|
||||
|
||||
#### Class names
|
||||
- component: `input` (applied to native `<input>` types)
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<input type="text" class="input" placeholder="{Placeholder}" />
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- Supports common input types; pair with `.label` and `.field` patterns.
|
||||
|
||||
### kbd (CSS)
|
||||
Keyboard key visual.
|
||||
|
||||
#### Class names
|
||||
- component: `kbd`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<kbd class="kbd">⌘K</kbd>
|
||||
```
|
||||
|
||||
### label (CSS)
|
||||
Styled form labels.
|
||||
|
||||
#### Class names
|
||||
- component: `label`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<label class="label" for="{id}">{TEXT}</label>
|
||||
```
|
||||
|
||||
### popover (JS)
|
||||
Small overlay anchored to a trigger.
|
||||
|
||||
#### Class names
|
||||
- component: `popover`
|
||||
- part: `> button` (trigger), `[data-popover]` (content)
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<div class="popover">
|
||||
<button type="button">{TRIGGER}</button>
|
||||
<section data-popover aria-hidden="true">{CONTENT}</section>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- Requires JS. Toggling sets `aria-hidden` on content and `aria-expanded` on trigger.
|
||||
|
||||
### radio-group (CSS)
|
||||
Styled native radio inputs.
|
||||
|
||||
#### Class names
|
||||
- component: `.input[type='radio']`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<label><input type="radio" class="input" name="{name}" /> {LABEL}</label>
|
||||
```
|
||||
|
||||
### select (JS)
|
||||
Custom select with searchable popover and listbox.
|
||||
|
||||
#### Class names
|
||||
- component: `select`
|
||||
- part: `> button` (trigger with label span), `[data-popover]` (panel), `[role='listbox']`, `[role='option']`, `> input[type='hidden']`, optional `header input[type='text']` filter
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<div class="select">
|
||||
<button type="button" aria-expanded="false"><span>{LABEL}</span></button>
|
||||
<input type="hidden" name="{name}" value="{value}" />
|
||||
<section data-popover aria-hidden="true">
|
||||
<header><input type="text" placeholder="Filter" /></header>
|
||||
<div role="listbox">
|
||||
<div role="option" data-value="{value}" data-label="{label}">{label}</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- Requires JS. Dispatches `change` on root with `detail.value`. Disabled options use `aria-disabled="true"`.
|
||||
|
||||
### sidebar (JS)
|
||||
Collapsible navigation sidebar.
|
||||
|
||||
#### Class names
|
||||
- component: `sidebar`
|
||||
|
||||
#### Rules
|
||||
- Requires JS. Uses `aria-current="page"` for active link styling.
|
||||
|
||||
### switch (CSS)
|
||||
Styled switch using checkbox with `role="switch"`.
|
||||
|
||||
#### Class names
|
||||
- component: `.input[type='checkbox'][role='switch']`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<input type="checkbox" role="switch" class="input" />
|
||||
```
|
||||
|
||||
### table (CSS)
|
||||
Styled tables.
|
||||
|
||||
#### Class names
|
||||
- component: `table`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<table class="table">{ROWS}</table>
|
||||
```
|
||||
|
||||
### tabs (JS)
|
||||
Tabbed interface with keyboard navigation.
|
||||
|
||||
#### Class names
|
||||
- component: `tabs`
|
||||
- part: `[role='tablist']`, `[role='tab']`, `[role='tabpanel']`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<div class="tabs">
|
||||
<div role="tablist">
|
||||
<button role="tab" aria-controls="panel-1" aria-selected="true" tabindex="0">Tab 1</button>
|
||||
<button role="tab" aria-controls="panel-2" aria-selected="false" tabindex="-1">Tab 2</button>
|
||||
</div>
|
||||
<section id="panel-1" role="tabpanel">...</section>
|
||||
<section id="panel-2" role="tabpanel" hidden>...</section>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- Requires JS. Arrow keys move focus; keep `aria-controls` and `aria-selected` accurate.
|
||||
|
||||
### textarea (CSS)
|
||||
Styled multiline text input.
|
||||
|
||||
#### Class names
|
||||
- component: `textarea`
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<textarea class="textarea" rows="{n}"></textarea>
|
||||
```
|
||||
|
||||
### toast (JS)
|
||||
Global notifications.
|
||||
|
||||
#### Class names
|
||||
- component: `#toaster` (container), `toast` (item)
|
||||
|
||||
#### Syntax
|
||||
```html
|
||||
<section id="toaster"></section>
|
||||
```
|
||||
Show a toast:
|
||||
```js
|
||||
document.dispatchEvent(new CustomEvent('basecoat:toast', { detail: { config: { title: 'Saved', description: 'Changes persisted', category: 'success' } } }))
|
||||
```
|
||||
|
||||
#### Rules
|
||||
- Requires JS. Uses timeouts; hovering pauses timers.
|
||||
|
||||
### popover/dialog variants (CSS/JS‑lite)
|
||||
Dialog and drawer patterns built on native elements, styled by Basecoat.
|
||||
|
||||
#### Class names
|
||||
- wrapper: `command-dialog` (for Command palette in a dialog)
|
||||
|
||||
#### Rules
|
||||
- Use native `<dialog>` or popover attributes where applicable.
|
||||
|
||||
### other CSS components
|
||||
Additional CSS components include: `progress`, `skeleton`, `slider`, `spinner`, `pagination`, `breadcrumb`, `item`, `empty`, `theme-switcher`, `form`, `field`, `input-group`, `radio-group`, `switch`, `avatar`, `badge`, `alert`, `card`, `kbd`, `table`.
|
||||
|
||||
## Events
|
||||
- `basecoat:initialized` (component root): emitted after a component initializes.
|
||||
- `basecoat:popover` (document): used to close other popovers when one opens.
|
||||
- `basecoat:toast` (document): creates a toast using `{ detail: { config } }`.
|
||||
|
||||
## Accessibility
|
||||
- Use semantic roles/labels per component. Keep `aria-*` for semantics and `data-*` for visual states.
|
||||
- Disabled items must use `disabled` or `aria-disabled="true"` to be filtered from interactions.
|
||||
|
||||
## Troubleshooting
|
||||
- If interactivity doesn’t work, ensure the component’s JS is included and required roles/IDs match the examples.
|
||||
- For dynamic content, call `window.basecoat.init('<name>')` or `window.basecoat.initAll()` after insertion.
|
||||
|
||||
## License
|
||||
MIT (see LICENSE.md)
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "basecoat",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.10-beta.1",
|
||||
"description": "shadcn/ui for the rest of us",
|
||||
"author": "hunvreus",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "basecoat-cli",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.10-beta.1",
|
||||
"description": "Add Basecoat components to your project",
|
||||
"author": "hunvreus",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "basecoat-css",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.10-beta.1",
|
||||
"description": "Tailwind CSS for Basecoat components",
|
||||
"author": {
|
||||
"name": "hunvreus",
|
||||
@@ -40,6 +40,8 @@
|
||||
"exports": {
|
||||
".": "./dist/basecoat.css",
|
||||
"./css": "./dist/basecoat.css",
|
||||
"./basecoat": "./dist/js/basecoat.js",
|
||||
"./basecoat.min": "./dist/js/basecoat.min.js",
|
||||
"./all": "./dist/js/all.js",
|
||||
"./all.min": "./dist/js/all.min.js",
|
||||
"./command": "./dist/js/command.js",
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ async function build() {
|
||||
|
||||
// Create combined component files
|
||||
console.log('Creating combined component files...');
|
||||
const componentsToCombine = ['basecoat.js', 'dropdown-menu.js', 'popover.js', 'select.js', 'sidebar.js', 'tabs.js', 'toast.js'];
|
||||
const componentsToCombine = ['basecoat.js', 'command.js', 'dropdown-menu.js', 'popover.js', 'select.js', 'sidebar.js', 'tabs.js', 'toast.js'];
|
||||
const componentPaths = componentsToCombine.map(f => path.join(srcJsDir, f));
|
||||
|
||||
// Create non-minified bundle
|
||||
|
||||
+158
-141
@@ -154,11 +154,16 @@
|
||||
.alert-destructive {
|
||||
@apply relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current;
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
> h2,
|
||||
> h3,
|
||||
> h4,
|
||||
> h5,
|
||||
> h6,
|
||||
> strong,
|
||||
> [data-title] {
|
||||
@apply col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight;
|
||||
}
|
||||
section {
|
||||
> section {
|
||||
@apply text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed;
|
||||
|
||||
ul {
|
||||
@@ -172,7 +177,7 @@
|
||||
.alert-destructive {
|
||||
@apply text-destructive bg-card [&>svg]:text-current;
|
||||
|
||||
section {
|
||||
> section {
|
||||
@apply text-destructive;
|
||||
}
|
||||
}
|
||||
@@ -381,42 +386,44 @@
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
> hr[role='separator'] {
|
||||
@apply w-0 h-auto self-stretch border border-input shrink-0 m-0;
|
||||
}
|
||||
|
||||
&:not([data-orientation='vertical']) {
|
||||
> *:not(:first-child),
|
||||
> :is(.dropdown-menu, .popover, .select):not(:first-child) > button {
|
||||
@apply rounded-l-none border-l-0;
|
||||
@layer components {
|
||||
.button-group {
|
||||
@apply inline-flex w-fit items-stretch isolate;
|
||||
|
||||
> *:focus-visible,
|
||||
> :is(.dropdown-menu, .popover, .select) > button:focus-visible {
|
||||
@apply relative z-10;
|
||||
}
|
||||
> *: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;
|
||||
@apply w-0 h-auto self-stretch border border-input shrink-0 m-0;
|
||||
}
|
||||
|
||||
> *: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;
|
||||
|
||||
&: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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -570,13 +577,13 @@
|
||||
[role='separator'] {
|
||||
@apply border-border -mx-1 my-1;
|
||||
}
|
||||
&:not(:has([data-value]:not([aria-hidden='true'])))::before {
|
||||
&:not(:has([role='menuitem']:not([aria-hidden='true'])))::before {
|
||||
@apply flex items-center justify-center py-6 px-3 text-sm truncate -m-1;
|
||||
}
|
||||
&[data-empty]:not(:has([data-value]:not([aria-hidden='true'])))::before {
|
||||
&[data-empty]:not(:has([role='menuitem']:not([aria-hidden='true'])))::before {
|
||||
@apply content-[attr(data-empty)];
|
||||
}
|
||||
&:not([data-empty]):not(:has([data-value]:not([aria-hidden='true'])))::before {
|
||||
&:not([data-empty]):not(:has([role='menuitem']:not([aria-hidden='true'])))::before {
|
||||
@apply content-['No_results_found'];
|
||||
}
|
||||
}
|
||||
@@ -735,14 +742,14 @@
|
||||
|
||||
/* Input */
|
||||
@layer components {
|
||||
:is(.form, .field) input[type='text'],
|
||||
:is(.form, .field) input[type='text']:not(:is(.select [data-popover] > header input[role='combobox'], .command > header input, .dialog > * > header input)),
|
||||
:is(.form, .field) input[type='email'],
|
||||
:is(.form, .field) input[type='password'],
|
||||
:is(.form, .field) input[type='number'],
|
||||
:is(.form, .field) input[type='file'],
|
||||
:is(.form, .field) input[type='tel'],
|
||||
:is(.form, .field) input[type='url'],
|
||||
:is(.form, .field) input[type='search'],
|
||||
:is(.form, .field) input[type='search']:not(:is(.select [data-popover] > header input[role='combobox'], .command > header input, .dialog > * > header input)),
|
||||
:is(.form, .field) input[type='date'],
|
||||
:is(.form, .field) input[type='datetime-local'],
|
||||
:is(.form, .field) input[type='month'],
|
||||
@@ -934,6 +941,14 @@
|
||||
}
|
||||
*:not(select).select {
|
||||
@apply relative inline-flex;
|
||||
|
||||
& > button {
|
||||
@apply justify-between font-normal;
|
||||
|
||||
&[aria-invalid='true'] {
|
||||
@apply ring-destructive/20 dark:ring-destructive/40 border-destructive;
|
||||
}
|
||||
}
|
||||
|
||||
[data-popover] {
|
||||
@apply p-1;
|
||||
@@ -988,119 +1003,121 @@
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
&:not([data-sidebar-initialized]) {
|
||||
@apply max-md:hidden;
|
||||
}
|
||||
&:not([aria-hidden]),
|
||||
&[aria-hidden=false] {
|
||||
@apply max-md:bg-black/50 max-md:fixed max-md:inset-0 max-md:z-40;
|
||||
}
|
||||
nav {
|
||||
@apply bg-sidebar text-sidebar-foreground flex flex-col w-(--sidebar-mobile-width) md:w-(--sidebar-width) fixed inset-y-0 z-50 transition-transform ease-in-out duration-300;
|
||||
}
|
||||
& + * {
|
||||
@apply transition-[margin] ease-in-out duration-300;
|
||||
}
|
||||
&:not([data-side]),
|
||||
&[data-side=left] {
|
||||
@layer components {
|
||||
.sidebar {
|
||||
&:not([data-sidebar-initialized]) {
|
||||
@apply max-md:hidden;
|
||||
}
|
||||
&:not([aria-hidden]),
|
||||
&[aria-hidden=false] {
|
||||
@apply max-md:bg-black/50 max-md:fixed max-md:inset-0 max-md:z-40;
|
||||
}
|
||||
nav {
|
||||
@apply left-0 border-r;
|
||||
@apply bg-sidebar text-sidebar-foreground flex flex-col w-(--sidebar-mobile-width) md:w-(--sidebar-width) fixed inset-y-0 z-50 transition-transform ease-in-out duration-300;
|
||||
}
|
||||
& + * {
|
||||
@apply relative md:ml-(--sidebar-width);
|
||||
@apply transition-[margin] ease-in-out duration-300;
|
||||
}
|
||||
&[aria-hidden=true] {
|
||||
&:not([data-side]),
|
||||
&[data-side=left] {
|
||||
nav {
|
||||
@apply -translate-x-full;
|
||||
@apply left-0 border-r;
|
||||
}
|
||||
& + * {
|
||||
@apply md:ml-0;
|
||||
@apply relative md:ml-(--sidebar-width);
|
||||
}
|
||||
&[aria-hidden=true] {
|
||||
nav {
|
||||
@apply -translate-x-full;
|
||||
}
|
||||
& + * {
|
||||
@apply md:ml-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&[data-side=right] {
|
||||
nav {
|
||||
@apply right-0 border-l;
|
||||
}
|
||||
& + * {
|
||||
@apply relative md:mr-(--sidebar-width);
|
||||
}
|
||||
&[aria-hidden=true] {
|
||||
nav {
|
||||
@apply translate-x-full;
|
||||
}
|
||||
& + * {
|
||||
@apply md:mr-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&[data-side=right] {
|
||||
nav {
|
||||
@apply right-0 border-l;
|
||||
}
|
||||
& + * {
|
||||
@apply relative md:mr-(--sidebar-width);
|
||||
}
|
||||
&[aria-hidden=true] {
|
||||
nav {
|
||||
@apply translate-x-full;
|
||||
> header,
|
||||
> footer {
|
||||
@apply flex flex-col gap-2 p-2;
|
||||
}
|
||||
& + * {
|
||||
@apply md:mr-0;
|
||||
[role=separator] {
|
||||
@apply border-sidebar-border mx-2 w-auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
nav {
|
||||
> header,
|
||||
> footer {
|
||||
@apply flex flex-col gap-2 p-2;
|
||||
}
|
||||
[role=separator] {
|
||||
@apply border-sidebar-border mx-2 w-auto;
|
||||
}
|
||||
> section {
|
||||
@apply flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto;
|
||||
> section {
|
||||
@apply flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto;
|
||||
|
||||
> [role=group] {
|
||||
@apply relative flex w-full min-w-0 flex-col p-2;
|
||||
}
|
||||
h3 {
|
||||
@apply text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0;
|
||||
}
|
||||
ul {
|
||||
@apply flex w-full min-w-0 flex-col gap-1;
|
||||
|
||||
li {
|
||||
@apply relative;
|
||||
|
||||
> a,
|
||||
> details > summary {
|
||||
@apply flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&[aria-current=page]]:bg-sidebar-accent [&[aria-current=page]]:font-medium [&[aria-current=page]]:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0;
|
||||
|
||||
&:not([data-variant]),
|
||||
&[data-variant=default] {
|
||||
@apply hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
|
||||
}
|
||||
&[data-variant=outline] {
|
||||
@apply bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))];
|
||||
}
|
||||
&:not([data-size]),
|
||||
&[data-size=default] {
|
||||
@apply h-8 text-sm;
|
||||
}
|
||||
&[data-size=sm] {
|
||||
@apply h-7 text-xs;
|
||||
}
|
||||
&[data-size=lg] {
|
||||
@apply h-12 text-sm group-data-[collapsible=icon]:p-0!;
|
||||
}
|
||||
}
|
||||
> details {
|
||||
&:not([open]) {
|
||||
> summary {
|
||||
&::after {
|
||||
@apply -rotate-90;
|
||||
}
|
||||
}
|
||||
}
|
||||
> summary {
|
||||
&::after {
|
||||
@apply content-[''] block size-3.5 bg-primary ml-auto transition-transform ease-linear;
|
||||
@apply mask-[image:var(--chevron-down-icon)] mask-size-[1rem] mask-no-repeat mask-center;
|
||||
}
|
||||
}
|
||||
&::details-content {
|
||||
@apply px-3.5;
|
||||
}
|
||||
}
|
||||
> [role=group] {
|
||||
@apply relative flex w-full min-w-0 flex-col p-2;
|
||||
}
|
||||
h3 {
|
||||
@apply text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0;
|
||||
}
|
||||
ul {
|
||||
@apply border-sidebar-border flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5 w-full;
|
||||
@apply flex w-full min-w-0 flex-col gap-1;
|
||||
|
||||
li {
|
||||
@apply relative;
|
||||
|
||||
> a,
|
||||
> details > summary {
|
||||
@apply flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&[aria-current=page]]:bg-sidebar-accent [&[aria-current=page]]:font-medium [&[aria-current=page]]:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0;
|
||||
|
||||
&:not([data-variant]),
|
||||
&[data-variant=default] {
|
||||
@apply hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
|
||||
}
|
||||
&[data-variant=outline] {
|
||||
@apply bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))];
|
||||
}
|
||||
&:not([data-size]),
|
||||
&[data-size=default] {
|
||||
@apply h-8 text-sm;
|
||||
}
|
||||
&[data-size=sm] {
|
||||
@apply h-7 text-xs;
|
||||
}
|
||||
&[data-size=lg] {
|
||||
@apply h-12 text-sm group-data-[collapsible=icon]:p-0!;
|
||||
}
|
||||
}
|
||||
> details {
|
||||
&:not([open]) {
|
||||
> summary {
|
||||
&::after {
|
||||
@apply -rotate-90;
|
||||
}
|
||||
}
|
||||
}
|
||||
> summary {
|
||||
&::after {
|
||||
@apply content-[''] block size-3.5 bg-primary ml-auto transition-transform ease-linear;
|
||||
@apply mask-[image:var(--chevron-down-icon)] mask-size-[1rem] mask-no-repeat mask-center;
|
||||
}
|
||||
}
|
||||
&::details-content {
|
||||
@apply px-3.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
ul {
|
||||
@apply border-sidebar-border flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5 w-full;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1241,7 +1258,7 @@
|
||||
@apply relative;
|
||||
|
||||
&:before {
|
||||
@apply absolute content-[attr(data-tooltip)] bg-primary text-primary-foreground z-[60] truncate max-w-xs w-fit rounded-md px-3 py-1.5 text-xs invisible opacity-0 scale-95 transition-all pointer-events-none;
|
||||
@apply absolute content-[attr(data-tooltip)] bg-foreground text-background z-[60] truncate max-w-xs w-fit rounded-md px-3 py-1.5 text-xs invisible opacity-0 scale-95 transition-all pointer-events-none;
|
||||
}
|
||||
&:hover:before {
|
||||
@apply visible opacity-100 scale-100;
|
||||
@@ -1290,4 +1307,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,10 +143,10 @@
|
||||
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: "item", label: "Text or HTML", icon: "HTML", url: "href", keywords: "search terms", disabled: true, attrs: {} }
|
||||
- { type: "group", label: "Group Title", items: [...] }
|
||||
- { type: "separator" }
|
||||
Note: Use attrs to add data-label for alternative search text if needed.
|
||||
Note: Use attrs to add data-label for alternative search text if needed. If url is provided, renders as <a role="menuitem"> instead of <div role="menuitem">. Label supports HTML (marked as safe).
|
||||
@param parent_id_prefix {string} [optional] - The prefix for the item id.
|
||||
#}
|
||||
{% macro render_command_items(items, parent_id_prefix="items") %}
|
||||
@@ -163,25 +163,44 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
<span role="heading" id="{{ group_label_id }}">{{ item.label }}</span>
|
||||
{{ render_command_items(item.items, item_id) if item.items }}
|
||||
<span role="heading" id="{{ group_label_id }}">{{ item.label | safe }}</span>
|
||||
{{ render_command_items(item['items'], item_id) if item['items'] }}
|
||||
</div>
|
||||
{% elif item.type == "separator" %}
|
||||
<hr role="separator" />
|
||||
{% elif item.type == "item" or not item.type %}
|
||||
<div
|
||||
id="{{ item_id }}"
|
||||
role="menuitem"
|
||||
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
|
||||
{% if item.disabled %}aria-disabled="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{{ item.content | safe if item.content else item.label }}
|
||||
</div>
|
||||
{% if item.url %}
|
||||
<a
|
||||
id="{{ item_id }}"
|
||||
href="{{ item.url }}"
|
||||
role="menuitem"
|
||||
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
|
||||
{% if item.disabled %}aria-disabled="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{% if key != "href" %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div
|
||||
id="{{ item_id }}"
|
||||
role="menuitem"
|
||||
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
|
||||
{% if item.disabled %}aria-disabled="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<div>
|
||||
{% if title or description %}
|
||||
<header
|
||||
{% for key, value in header_attrs %}
|
||||
{% for key, value in header_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
{% endif %}
|
||||
>
|
||||
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
|
||||
{{ render_dropdown_items(item.items, item_id) if item.items }}
|
||||
{{ render_dropdown_items(item['items'], item_id) if item['items'] }}
|
||||
</div>
|
||||
{% elif item.type == "separator" %}
|
||||
<hr role="separator" />
|
||||
@@ -102,6 +102,7 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
@@ -114,9 +115,10 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
||||
+50
-22
@@ -2,8 +2,10 @@
|
||||
Renders a select or combobox component.
|
||||
|
||||
@param id {string} [optional] - Unique identifier for the select component.
|
||||
@param selected {string} [optional] - The initially selected value.
|
||||
@param selected {string|array} [optional] - The initially selected value(s).
|
||||
@param name {string} [optional] - The name attribute for the hidden input storing the selected value.
|
||||
@param multiple {boolean} [optional] [default=false] - Enables multiple selection mode.
|
||||
@param placeholder {string} [optional] - Placeholder text shown when no options are selected (multiple mode only).
|
||||
@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 popover_attrs {object} [optional] - Additional HTML attributes for the popover content div.
|
||||
@@ -17,6 +19,8 @@
|
||||
selected=None,
|
||||
name=None,
|
||||
items=[],
|
||||
multiple=false,
|
||||
placeholder=None,
|
||||
main_attrs={},
|
||||
trigger_attrs={},
|
||||
popover_attrs={},
|
||||
@@ -27,43 +31,52 @@
|
||||
) %}
|
||||
{% set id = id or ("select-" + (range(100000, 999999) | random | string)) %}
|
||||
|
||||
{% set selectedArray = ((selected if (selected is iterable and selected is not string) else [selected]) if selected else []) %}
|
||||
{% set first_option = namespace(item=None) %}
|
||||
{% set selected_option = namespace(item=None) %}
|
||||
{% set selected_options = namespace(items=[]) %}
|
||||
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
{% if item.type == "group" %}
|
||||
{% for sub_item in item.items %}
|
||||
{% for sub_item in item['items'] %}
|
||||
{% if not first_option.item %}
|
||||
{% set first_option.item = sub_item %}
|
||||
{% endif %}
|
||||
{% if selected and sub_item.value == selected and not selected_option.item %}
|
||||
{% set selected_option.item = sub_item %}
|
||||
{% if sub_item.value in selectedArray %}
|
||||
{% set selected_options.items = selected_options.items + [sub_item] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% if not first_option.item %}
|
||||
{% set first_option.item = item %}
|
||||
{% endif %}
|
||||
{% if selected and item.value == selected and not selected_option.item %}
|
||||
{% set selected_option.item = item %}
|
||||
{% if item.value in selectedArray %}
|
||||
{% set selected_options.items = selected_options.items + [item] %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% set default_option = selected_option.item or first_option.item or None %}
|
||||
{% set default_option = (selected_options.items[0] if selected_options.items else first_option.item) %}
|
||||
{% set labels = [] %}
|
||||
{% if multiple and selected_options.items %}
|
||||
{% for opt in selected_options.items %}
|
||||
{% set labels = labels + [opt.label] %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% set default_label = (', '.join(labels) if (multiple and selected_options.items) else ((placeholder or '') if (multiple and not selected_options.items) else (default_option.label if default_option else ''))) %}
|
||||
|
||||
<div
|
||||
id="{{ id }}"
|
||||
class="select {{ main_attrs.class }}"
|
||||
{% if multiple and placeholder %}data-placeholder="{{ placeholder }}"{% endif %}
|
||||
{% for key, value in main_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline justify-between font-normal {{ trigger_attrs.class }}"
|
||||
class="btn-outline {{ trigger_attrs.class }}"
|
||||
id="{{ id }}-trigger"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded="false"
|
||||
@@ -72,7 +85,7 @@
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<span class="truncate">{{ default_option.label }}</span>
|
||||
<span class="truncate">{{ default_label }}</span>
|
||||
{% if is_combobox %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevrons-up-down-icon lucide-chevrons-up-down text-muted-foreground opacity-50 shrink-0"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
||||
{% else %}
|
||||
@@ -110,25 +123,40 @@
|
||||
id="{{ id }}-listbox"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="{{ id }}-trigger"
|
||||
{% if multiple %}aria-multiselectable="true"{% endif %}
|
||||
{% for key, value in listbox_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{% if items %}
|
||||
{{ render_select_items(items, default_option.value, id ~ "-items" if id else "items") }}
|
||||
{{ render_select_items(items, selectedArray, id ~ "-items" if id else "items") }}
|
||||
{% else %}
|
||||
{{ caller() if caller }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
name="{{ name or id ~ '-value' }}"
|
||||
value="{{ (default_option.value if default_option) or '' }}"
|
||||
{% for key, value in input_attrs.items() %}
|
||||
{% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% if multiple and selectedArray %}
|
||||
{% for val in selectedArray %}
|
||||
<input
|
||||
type="hidden"
|
||||
name="{{ name or id ~ '-value' }}"
|
||||
value="{{ val }}"
|
||||
{% if loop.index0 > 0 %}id="{{ id }}-value-{{ loop.index0 }}"{% endif %}
|
||||
{% for key, value in input_attrs.items() %}
|
||||
{% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% endfor %}
|
||||
>
|
||||
{% else %}
|
||||
<input
|
||||
type="hidden"
|
||||
name="{{ name or id ~ '-value' }}"
|
||||
value="{{ (default_option.value if default_option) or '' }}"
|
||||
{% for key, value in input_attrs.items() %}
|
||||
{% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -138,7 +166,7 @@
|
||||
@param items {array} - The array of items to render.
|
||||
@param parent_id_prefix {string} [optional] - The prefix for the item id.
|
||||
#}
|
||||
{% macro render_select_items(items, selected, parent_id_prefix="items") %}
|
||||
{% macro render_select_items(items, selectedArray, parent_id_prefix="items") %}
|
||||
{% for item in items %}
|
||||
{% set item_id = parent_id_prefix ~ "-" ~ loop.index %}
|
||||
{% if item.type == "group" %}
|
||||
@@ -153,7 +181,7 @@
|
||||
{% endif %}
|
||||
>
|
||||
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
|
||||
{{ render_select_items(item.items, selected, item_id) if item.items }}
|
||||
{{ render_select_items(item['items'], selectedArray, item_id) if item['items'] }}
|
||||
</div>
|
||||
{% elif item.type == "separator" %}
|
||||
<hr role="separator" />
|
||||
@@ -162,7 +190,7 @@
|
||||
id="{{ item_id }}"
|
||||
role="option"
|
||||
data-value="{{ item.value }}"
|
||||
{% if selected == item.value %}aria-selected="true"{% endif %}
|
||||
{% if item.value in selectedArray %}aria-selected="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
@@ -173,4 +201,4 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
>
|
||||
<summary aria-controls="submenu-{{ item_id }}-content">
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label }}
|
||||
{{ item.label | safe }}
|
||||
</summary>
|
||||
<ul id="submenu-{{ item_id }}-content">
|
||||
{{ render_sidebar_content(item["items"], item_id) if item["items"] }}
|
||||
@@ -136,7 +136,7 @@
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
<span>{{ item.label }}</span>
|
||||
<span>{{ item.label | safe }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
@param id {string} [optional] [default="toaster"] - Unique identifier for the toaster container.
|
||||
@param toasts {array} [optional] - An array of toast objects to render initially. See the <code>toast()</code> macro for more details.
|
||||
@param attrs {object} [optional] - Additional HTML attributes for the main toaster container div.
|
||||
|
||||
Supports caller() for custom toast markup.
|
||||
#}
|
||||
{% macro toaster(
|
||||
id="toaster",
|
||||
@@ -24,9 +26,11 @@
|
||||
title=item.title,
|
||||
description=item.description,
|
||||
action=item.action,
|
||||
cancel=item.cancel
|
||||
cancel=item.cancel,
|
||||
attrs=item.attrs
|
||||
) }}
|
||||
{% endfor %}
|
||||
{{ caller() if caller }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -44,20 +48,25 @@
|
||||
- label {string}: Button text.
|
||||
- onclick {string}: JavaScript code to execute on click.
|
||||
- url {string}: URL for an anchor link button.
|
||||
@param attrs {object} [optional] - Additional HTML attributes for the toast div.
|
||||
#}
|
||||
{% macro toast(
|
||||
category="success",
|
||||
title="",
|
||||
description="",
|
||||
action=None,
|
||||
cancel=None
|
||||
cancel=None,
|
||||
attrs={}
|
||||
) %}
|
||||
<div
|
||||
class="toast"
|
||||
class="toast {{ attrs.class }}"
|
||||
role="{{ 'alert' if category == 'error' else 'status' }}"
|
||||
aria-atomic="true"
|
||||
aria-hidden="false"
|
||||
{% if category %}data-category="{{ category }}"{% endif %}
|
||||
{% for key, value in attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<div class="toast-content">
|
||||
{% if category in ["error", "success", "info", "warning"] %}
|
||||
|
||||
+15
-3
@@ -46,9 +46,21 @@
|
||||
|
||||
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);
|
||||
if (item.hasAttribute('data-force')) {
|
||||
item.setAttribute('aria-hidden', 'false');
|
||||
if (menuItems.includes(item)) {
|
||||
visibleMenuItems.push(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const itemText = (item.dataset.filter || item.textContent).trim().toLowerCase();
|
||||
const keywordList = (item.dataset.keywords || '')
|
||||
.toLowerCase()
|
||||
.split(/[\s,]+/)
|
||||
.filter(Boolean);
|
||||
const matchesKeyword = keywordList.some(keyword => keyword.includes(searchTerm));
|
||||
const matches = itemText.includes(searchTerm) || matchesKeyword;
|
||||
item.setAttribute('aria-hidden', String(!matches));
|
||||
if (matches && menuItems.includes(item)) {
|
||||
visibleMenuItems.push(item);
|
||||
|
||||
+176
-29
@@ -3,23 +3,31 @@
|
||||
const trigger = selectComponent.querySelector(':scope > button');
|
||||
const selectedLabel = trigger.querySelector(':scope > span');
|
||||
const popover = selectComponent.querySelector(':scope > [data-popover]');
|
||||
const listbox = popover.querySelector('[role="listbox"]');
|
||||
const listbox = popover ? popover.querySelector('[role="listbox"]') : null;
|
||||
const input = selectComponent.querySelector(':scope > input[type="hidden"]');
|
||||
const filter = selectComponent.querySelector('header input[type="text"]');
|
||||
|
||||
if (!trigger || !popover || !listbox || !input) {
|
||||
const missing = [];
|
||||
if (!trigger) missing.push('trigger');
|
||||
if (!popover) missing.push('popover');
|
||||
if (!listbox) missing.push('listbox');
|
||||
if (!input) missing.push('input');
|
||||
if (!input) missing.push('input');
|
||||
console.error(`Select component initialisation failed. Missing element(s): ${missing.join(', ')}`, selectComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const allOptions = Array.from(listbox.querySelectorAll('[role="option"]'));
|
||||
const options = allOptions.filter(opt => opt.getAttribute('aria-disabled') !== 'true');
|
||||
let visibleOptions = [...options];
|
||||
let activeIndex = -1;
|
||||
const isMultiple = listbox.getAttribute('aria-multiselectable') === 'true';
|
||||
let selectedValues = isMultiple ? new Set() : null;
|
||||
let placeholder = null;
|
||||
|
||||
if (isMultiple) {
|
||||
placeholder = selectComponent.dataset.placeholder || '';
|
||||
}
|
||||
|
||||
const setActiveOption = (index) => {
|
||||
if (activeIndex > -1 && options[activeIndex]) {
|
||||
@@ -46,23 +54,92 @@
|
||||
return parseFloat(style.transitionDuration) > 0 || parseFloat(style.transitionDelay) > 0;
|
||||
};
|
||||
|
||||
const updateValue = (option, triggerEvent = true) => {
|
||||
if (option) {
|
||||
selectedLabel.innerHTML = option.dataset.label || option.innerHTML;
|
||||
input.value = option.dataset.value;
|
||||
listbox.querySelector('[role="option"][aria-selected="true"]')?.removeAttribute('aria-selected');
|
||||
option.setAttribute('aria-selected', 'true');
|
||||
|
||||
if (triggerEvent) {
|
||||
const event = new CustomEvent('change', {
|
||||
detail: { value: option.dataset.value },
|
||||
bubbles: true
|
||||
});
|
||||
selectComponent.dispatchEvent(event);
|
||||
const syncMultipleInputs = () => {
|
||||
if (!isMultiple) return;
|
||||
const values = Array.from(selectedValues);
|
||||
const inputs = Array.from(selectComponent.querySelectorAll(':scope > input[type="hidden"]'));
|
||||
inputs.slice(1).forEach(inp => inp.remove());
|
||||
|
||||
if (values.length === 0) {
|
||||
input.value = '';
|
||||
} else {
|
||||
input.value = values[0];
|
||||
let insertAfter = input;
|
||||
for (let i = 1; i < values.length; i++) {
|
||||
const clone = input.cloneNode(true);
|
||||
clone.removeAttribute('id');
|
||||
clone.value = values[i];
|
||||
insertAfter.after(clone);
|
||||
insertAfter = clone;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateMultipleLabel = () => {
|
||||
if (!isMultiple) return;
|
||||
const selected = options.filter(opt => selectedValues.has(opt.dataset.value));
|
||||
if (selected.length === 0) {
|
||||
selectedLabel.textContent = placeholder;
|
||||
selectedLabel.classList.add('text-muted-foreground');
|
||||
} else {
|
||||
selectedLabel.textContent = selected.map(opt => opt.dataset.label || opt.textContent.trim()).join(', ');
|
||||
selectedLabel.classList.remove('text-muted-foreground');
|
||||
}
|
||||
};
|
||||
|
||||
const updateValue = (optionOrOptions, triggerEvent = true) => {
|
||||
let value;
|
||||
|
||||
if (isMultiple) {
|
||||
const opts = Array.isArray(optionOrOptions) ? optionOrOptions : [];
|
||||
selectedValues = new Set(opts.map(opt => opt.dataset.value));
|
||||
options.forEach(opt => {
|
||||
if (selectedValues.has(opt.dataset.value)) {
|
||||
opt.setAttribute('aria-selected', 'true');
|
||||
} else {
|
||||
opt.removeAttribute('aria-selected');
|
||||
}
|
||||
});
|
||||
updateMultipleLabel();
|
||||
syncMultipleInputs();
|
||||
value = Array.from(selectedValues);
|
||||
} else {
|
||||
const option = optionOrOptions;
|
||||
if (!option) return;
|
||||
selectedLabel.innerHTML = option.innerHTML;
|
||||
input.value = option.dataset.value;
|
||||
options.forEach(opt => {
|
||||
if (opt === option) {
|
||||
opt.setAttribute('aria-selected', 'true');
|
||||
} else {
|
||||
opt.removeAttribute('aria-selected');
|
||||
}
|
||||
});
|
||||
value = option.dataset.value;
|
||||
}
|
||||
|
||||
if (triggerEvent) {
|
||||
selectComponent.dispatchEvent(new CustomEvent('change', {
|
||||
detail: { value },
|
||||
bubbles: true
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMultipleValue = (value, triggerEvent = true) => {
|
||||
if (!isMultiple || value == null) return;
|
||||
|
||||
const newValues = new Set(selectedValues);
|
||||
if (newValues.has(value)) {
|
||||
newValues.delete(value);
|
||||
} else {
|
||||
newValues.add(value);
|
||||
}
|
||||
|
||||
const selectedOptions = options.filter(opt => newValues.has(opt.dataset.value));
|
||||
updateValue(selectedOptions, triggerEvent);
|
||||
};
|
||||
|
||||
const closePopover = (focusOnTrigger = true) => {
|
||||
if (popover.getAttribute('aria-hidden') === 'true') return;
|
||||
|
||||
@@ -101,7 +178,27 @@
|
||||
|
||||
const selectByValue = (value) => {
|
||||
const option = options.find(opt => opt.dataset.value === value);
|
||||
selectOption(option);
|
||||
if (isMultiple) {
|
||||
if (value != null && selectedValues.has(value)) return;
|
||||
if (option && value != null) {
|
||||
const newValues = new Set(selectedValues);
|
||||
newValues.add(value);
|
||||
const selectedOptions = options.filter(opt => newValues.has(opt.dataset.value));
|
||||
updateValue(selectedOptions);
|
||||
}
|
||||
} else {
|
||||
selectOption(option);
|
||||
}
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
if (!isMultiple) return;
|
||||
updateValue(options.filter(opt => opt.dataset.value != null));
|
||||
};
|
||||
|
||||
const selectNone = () => {
|
||||
if (!isMultiple) return;
|
||||
updateValue([]);
|
||||
};
|
||||
|
||||
if (filter) {
|
||||
@@ -112,8 +209,21 @@
|
||||
|
||||
visibleOptions = [];
|
||||
allOptions.forEach(option => {
|
||||
const optionText = (option.dataset.label || option.textContent).trim().toLowerCase();
|
||||
const matches = optionText.includes(searchTerm);
|
||||
if (option.hasAttribute('data-force')) {
|
||||
option.setAttribute('aria-hidden', 'false');
|
||||
if (options.includes(option)) {
|
||||
visibleOptions.push(option);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const optionText = (option.dataset.filter || option.textContent).trim().toLowerCase();
|
||||
const keywordList = (option.dataset.keywords || '')
|
||||
.toLowerCase()
|
||||
.split(/[\s,]+/)
|
||||
.filter(Boolean);
|
||||
const matchesKeyword = keywordList.some(keyword => keyword.includes(searchTerm));
|
||||
const matches = optionText.includes(searchTerm) || matchesKeyword;
|
||||
option.setAttribute('aria-hidden', String(!matches));
|
||||
if (matches && options.includes(option)) {
|
||||
visibleOptions.push(option);
|
||||
@@ -124,13 +234,32 @@
|
||||
filter.addEventListener('input', filterOptions);
|
||||
}
|
||||
|
||||
let initialOption = options.find(opt => opt.dataset.value === input.value);
|
||||
|
||||
if (!initialOption) {
|
||||
initialOption = options.find(opt => opt.dataset.value !== undefined) ?? options[0];
|
||||
}
|
||||
if (isMultiple) {
|
||||
const validValues = new Set(options.map(opt => opt.dataset.value).filter(v => v != null));
|
||||
const inputs = Array.from(selectComponent.querySelectorAll(':scope > input[type="hidden"]'));
|
||||
const initialValues = inputs
|
||||
.map(inp => inp.value)
|
||||
.filter(v => v != null && validValues.has(v));
|
||||
|
||||
updateValue(initialOption, false);
|
||||
let initialOptions;
|
||||
if (initialValues.length > 0) {
|
||||
initialOptions = options.filter(opt => initialValues.includes(opt.dataset.value));
|
||||
} else {
|
||||
initialOptions = options.filter(opt => opt.getAttribute('aria-selected') === 'true');
|
||||
}
|
||||
|
||||
updateValue(initialOptions, false);
|
||||
} else {
|
||||
let initialOption = options.find(opt => opt.dataset.value === input.value);
|
||||
|
||||
if (!initialOption) {
|
||||
initialOption = options.find(opt => opt.dataset.value !== undefined) ?? options[0];
|
||||
}
|
||||
|
||||
if (initialOption) {
|
||||
updateValue(initialOption, false);
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyNavigation = (event) => {
|
||||
const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false';
|
||||
@@ -156,7 +285,11 @@
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
if (activeIndex > -1) {
|
||||
selectOption(options[activeIndex]);
|
||||
if (isMultiple) {
|
||||
toggleMultipleValue(options[activeIndex].dataset.value);
|
||||
} else {
|
||||
selectOption(options[activeIndex]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -255,7 +388,17 @@
|
||||
listbox.addEventListener('click', (event) => {
|
||||
const clickedOption = event.target.closest('[role="option"]');
|
||||
if (clickedOption) {
|
||||
selectOption(clickedOption);
|
||||
if (isMultiple) {
|
||||
toggleMultipleValue(clickedOption.dataset.value);
|
||||
setActiveOption(options.indexOf(clickedOption));
|
||||
if (filter) {
|
||||
filter.focus();
|
||||
} else {
|
||||
trigger.focus();
|
||||
}
|
||||
} else {
|
||||
selectOption(clickedOption);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -272,8 +415,12 @@
|
||||
});
|
||||
|
||||
popover.setAttribute('aria-hidden', 'true');
|
||||
|
||||
|
||||
selectComponent.selectByValue = selectByValue;
|
||||
if (isMultiple) {
|
||||
selectComponent.selectAll = selectAll;
|
||||
selectComponent.selectNone = selectNone;
|
||||
}
|
||||
selectComponent.dataset.selectInitialized = true;
|
||||
selectComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
|
||||
};
|
||||
@@ -281,4 +428,4 @@
|
||||
if (window.basecoat) {
|
||||
window.basecoat.register('select', 'div.select:not([data-select-initialized])', initSelect);
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
||||
+35
-16
@@ -143,10 +143,10 @@
|
||||
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: "item", label: "Text or HTML", icon: "HTML", url: "href", keywords: "search terms", disabled: true, attrs: {} }
|
||||
- { type: "group", label: "Group Title", items: [...] }
|
||||
- { type: "separator" }
|
||||
Note: Use attrs to add data-label for alternative search text if needed.
|
||||
Note: Use attrs to add data-label for alternative search text if needed. If url is provided, renders as <a role="menuitem"> instead of <div role="menuitem">. Label supports HTML (marked as safe).
|
||||
@param parent_id_prefix {string} [optional] - The prefix for the item id.
|
||||
#}
|
||||
{% macro render_command_items(items, parent_id_prefix="items") %}
|
||||
@@ -163,25 +163,44 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
<span role="heading" id="{{ group_label_id }}">{{ item.label }}</span>
|
||||
<span role="heading" id="{{ group_label_id }}">{{ item.label | safe }}</span>
|
||||
{{ render_command_items(item.items, item_id) if item.items }}
|
||||
</div>
|
||||
{% elif item.type == "separator" %}
|
||||
<hr role="separator" />
|
||||
{% elif item.type == "item" or not item.type %}
|
||||
<div
|
||||
id="{{ item_id }}"
|
||||
role="menuitem"
|
||||
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
|
||||
{% if item.disabled %}aria-disabled="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{{ item.content | safe if item.content else item.label }}
|
||||
</div>
|
||||
{% if item.url %}
|
||||
<a
|
||||
id="{{ item_id }}"
|
||||
href="{{ item.url }}"
|
||||
role="menuitem"
|
||||
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
|
||||
{% if item.disabled %}aria-disabled="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs %}
|
||||
{% if key != "href" %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div
|
||||
id="{{ item_id }}"
|
||||
role="menuitem"
|
||||
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
|
||||
{% if item.disabled %}aria-disabled="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
@@ -114,9 +115,10 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
||||
+48
-20
@@ -2,8 +2,10 @@
|
||||
Renders a select or combobox component.
|
||||
|
||||
@param id {string} [optional] - Unique identifier for the select component.
|
||||
@param selected {string} [optional] - The initially selected value.
|
||||
@param selected {string|array} [optional] - The initially selected value(s).
|
||||
@param name {string} [optional] - The name attribute for the hidden input storing the selected value.
|
||||
@param multiple {boolean} [optional] [default=false] - Enables multiple selection mode.
|
||||
@param placeholder {string} [optional] - Placeholder text shown when no options are selected (multiple mode only).
|
||||
@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 popover_attrs {object} [optional] - Additional HTML attributes for the popover content div.
|
||||
@@ -17,6 +19,8 @@
|
||||
selected=None,
|
||||
name=None,
|
||||
items=None,
|
||||
multiple=false,
|
||||
placeholder=None,
|
||||
main_attrs={},
|
||||
trigger_attrs={},
|
||||
popover_attrs={},
|
||||
@@ -27,8 +31,9 @@
|
||||
) %}
|
||||
{% set id = id or ("select-" + (range(100000, 999999) | random | string)) %}
|
||||
|
||||
{% set selectedArray = ((selected if (selected is iterable and selected is not string) else [selected]) if selected else []) %}
|
||||
{% set first_option = [] %}
|
||||
{% set selected_option = [] %}
|
||||
{% set selected_options = [] %}
|
||||
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
@@ -37,33 +42,41 @@
|
||||
{% if not first_option[0] %}
|
||||
{% set first_option = (first_option.push(sub_item), first_option) %}
|
||||
{% endif %}
|
||||
{% if selected and sub_item.value == selected and not selected_option[0] %}
|
||||
{% set selected_option = (selected_option.push(sub_item), selected_option) %}
|
||||
{% if sub_item.value in selectedArray %}
|
||||
{% set selected_options = (selected_options.push(sub_item), selected_options) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% if not first_option[0] %}
|
||||
{% set first_option = (first_option.push(item), first_option) %}
|
||||
{% endif %}
|
||||
{% if selected and item.value == selected and not selected_option[0] %}
|
||||
{% set selected_option = (selected_option.push(item), selected_option) %}
|
||||
{% if item.value in selectedArray %}
|
||||
{% set selected_options = (selected_options.push(item), selected_options) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% set default_option = selected_option[0] or first_option[0] or None %}
|
||||
{% set default_option = (selected_options[0] if selected_options[0] else (first_option[0] if first_option[0] else None)) %}
|
||||
{% set labels = [] %}
|
||||
{% if multiple and selected_options %}
|
||||
{% for opt in selected_options %}
|
||||
{% set labels = (labels.push(opt.label), labels) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% set default_label = (labels | join(', ') if (multiple and selected_options) else ((placeholder or '') if (multiple and not selected_options) else (default_option.label if default_option else ''))) %}
|
||||
|
||||
<div
|
||||
id="{{ id }}"
|
||||
class="select {{ main_attrs.class }}"
|
||||
{% if multiple and placeholder %}data-placeholder="{{ placeholder }}"{% endif %}
|
||||
{% for key, value in main_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline justify-between font-normal {{ trigger_attrs.class }}"
|
||||
class="btn-outline {{ trigger_attrs.class }}"
|
||||
id="{{ id }}-trigger"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded="false"
|
||||
@@ -72,7 +85,7 @@
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<span class="truncate">{{ default_option.label }}</span>
|
||||
<span class="truncate">{{ default_label }}</span>
|
||||
{% if is_combobox %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevrons-up-down-icon lucide-chevrons-up-down text-muted-foreground opacity-50 shrink-0"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
||||
{% else %}
|
||||
@@ -110,25 +123,40 @@
|
||||
id="{{ id }}-listbox"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="{{ id }}-trigger"
|
||||
{% if multiple %}aria-multiselectable="true"{% endif %}
|
||||
{% for key, value in listbox_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{% if items and items.length > 0 %}
|
||||
{{ render_select_items(items, default_option.value, id ~ "-items" if id else "items") }}
|
||||
{{ render_select_items(items, selectedArray, id ~ "-items" if id else "items") }}
|
||||
{% else %}
|
||||
{{ caller() if caller }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
name="{{ name or id ~ '-value' }}"
|
||||
value="{{ (default_option.value if default_option) or '' }}"
|
||||
{% for key, value in input_attrs %}
|
||||
{% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% if multiple and selectedArray %}
|
||||
{% for val in selectedArray %}
|
||||
<input
|
||||
type="hidden"
|
||||
name="{{ name or id ~ '-value' }}"
|
||||
value="{{ val }}"
|
||||
{% if loop.index0 > 0 %}id="{{ id }}-value-{{ loop.index0 }}"{% endif %}
|
||||
{% for key, value in input_attrs %}
|
||||
{% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% endfor %}
|
||||
>
|
||||
{% else %}
|
||||
<input
|
||||
type="hidden"
|
||||
name="{{ name or id ~ '-value' }}"
|
||||
value="{{ (default_option.value if default_option) or '' }}"
|
||||
{% for key, value in input_attrs %}
|
||||
{% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -138,7 +166,7 @@
|
||||
@param items {array} - The array of items to render.
|
||||
@param parent_id_prefix {string} [optional] - The prefix for the item id.
|
||||
#}
|
||||
{% macro render_select_items(items, selected, parent_id_prefix="items") %}
|
||||
{% macro render_select_items(items, selectedArray, parent_id_prefix="items") %}
|
||||
{% for item in items %}
|
||||
{% set item_id = parent_id_prefix ~ "-" ~ loop.index %}
|
||||
{% if item.type == "group" %}
|
||||
@@ -153,7 +181,7 @@
|
||||
{% endif %}
|
||||
>
|
||||
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
|
||||
{{ render_select_items(item.items, selected, item_id) if item.items }}
|
||||
{{ render_select_items(item.items, selectedArray, item_id) if item.items }}
|
||||
</div>
|
||||
{% elif item.type == "separator" %}
|
||||
<hr role="separator" />
|
||||
@@ -162,7 +190,7 @@
|
||||
id="{{ item_id }}"
|
||||
role="option"
|
||||
data-value="{{ item.value }}"
|
||||
{% if selected == item.value %}aria-selected="true"{% endif %}
|
||||
{% if item.value in selectedArray %}aria-selected="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
>
|
||||
<summary aria-controls="submenu-{{ item_id }}-content">
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label }}
|
||||
{{ item.label | safe }}
|
||||
</summary>
|
||||
<ul id="submenu-{{ item_id }}-content">
|
||||
{{ render_sidebar_content(item.items, item_id) if item.items }}
|
||||
@@ -136,7 +136,7 @@
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
<span>{{ item.label }}</span>
|
||||
<span>{{ item.label | safe }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
+12
-3
@@ -5,6 +5,8 @@
|
||||
@param id {string} [optional] [default="toaster"] - Unique identifier for the toaster container.
|
||||
@param toasts {array} [optional] - An array of toast objects to render initially. See the <code>toast()</code> macro for more details.
|
||||
@param attrs {object} [optional] - Additional HTML attributes for the main toaster container div.
|
||||
|
||||
Supports caller() for custom toast markup.
|
||||
#}
|
||||
{% macro toaster(
|
||||
id="toaster",
|
||||
@@ -24,9 +26,11 @@
|
||||
title=item.title,
|
||||
description=item.description,
|
||||
action=item.action,
|
||||
cancel=item.cancel
|
||||
cancel=item.cancel,
|
||||
attrs=item.attrs
|
||||
) }}
|
||||
{% endfor %}
|
||||
{{ caller() if caller }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -44,20 +48,25 @@
|
||||
- label {string}: Button text.
|
||||
- onclick {string}: JavaScript code to execute on click.
|
||||
- url {string}: URL for an anchor link button.
|
||||
@param attrs {object} [optional] - Additional HTML attributes for the toast div.
|
||||
#}
|
||||
{% macro toast(
|
||||
category="success",
|
||||
title="",
|
||||
description="",
|
||||
action=None,
|
||||
cancel=None
|
||||
cancel=None,
|
||||
attrs={}
|
||||
) %}
|
||||
<div
|
||||
class="toast"
|
||||
class="toast {{ attrs.class }}"
|
||||
role="{{ 'alert' if category == 'error' else 'status' }}"
|
||||
aria-atomic="true"
|
||||
aria-hidden="false"
|
||||
{% if category %}data-category="{{ category }}"{% endif %}
|
||||
{% for key, value in attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<div class="toast-content">
|
||||
{% if category in ["error", "success", "info", "warning"] %}
|
||||
|
||||
Reference in New Issue
Block a user