mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-11 04:38:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
713c163fb8 | ||
|
|
a7f686e71b | ||
|
|
128f79c2e4 | ||
|
|
ee63071f87 | ||
|
|
def943d74e | ||
|
|
f43c20948b | ||
|
|
3ec06205a0 | ||
|
|
06e7c1ec84 | ||
|
|
09f868d474 | ||
|
|
b9274702d3 | ||
|
|
83c09c1197 | ||
|
|
36bca0c6a7 | ||
|
|
167b98e573 | ||
|
|
4035870578 | ||
|
|
dea0602b03 | ||
|
|
19941561d3 | ||
|
|
f708994df9 | ||
|
|
6fcbb31e30 | ||
|
|
0dfc5e061c | ||
|
|
c771c8256b | ||
|
|
2adb4c4667 | ||
|
|
cbb1119699 | ||
|
|
88db2e84ce | ||
|
|
a7f6f717d1 | ||
|
|
91c3932fe7 | ||
|
|
f977492bf6 | ||
|
|
530224c587 |
@@ -1 +0,0 @@
|
||||
github: [hunvreus]
|
||||
+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";
|
||||
@@ -69,14 +69,14 @@
|
||||
{# CSS #}
|
||||
<link rel="stylesheet" href="/assets/styles.css">
|
||||
{# JS #}
|
||||
<script src="/assets/js/basecoat.js" defer></script>
|
||||
<script src="/assets/js/command.js" defer></script>
|
||||
<script src="/assets/js/dropdown-menu.js" defer></script>
|
||||
<script src="/assets/js/popover.js" defer></script>
|
||||
<script src="/assets/js/select.js" defer></script>
|
||||
<script src="/assets/js/sidebar.js" defer></script>
|
||||
<script src="/assets/js/tabs.js" defer></script>
|
||||
<script src="/assets/js/toast.js" defer></script>
|
||||
<script src="/assets/js/basecoat.js?v={{ pkg.version }}" defer></script>
|
||||
<script src="/assets/js/command.js?v={{ pkg.version }}" defer></script>
|
||||
<script src="/assets/js/dropdown-menu.js?v={{ pkg.version }}" defer></script>
|
||||
<script src="/assets/js/popover.js?v={{ pkg.version }}" defer></script>
|
||||
<script src="/assets/js/select.js?v={{ pkg.version }}" defer></script>
|
||||
<script src="/assets/js/sidebar.js?v={{ pkg.version }}" defer></script>
|
||||
<script src="/assets/js/tabs.js?v={{ pkg.version }}" defer></script>
|
||||
<script src="/assets/js/toast.js?v={{ pkg.version }}" defer></script>
|
||||
{# HTMX #}
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
{# Highlight.js #}
|
||||
|
||||
@@ -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
|
||||
) }}
|
||||
) }}
|
||||
|
||||
+28
-30
@@ -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);
|
||||
@@ -260,9 +258,6 @@
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
@@ -528,12 +523,6 @@
|
||||
.max-h-\[200px\] {
|
||||
max-height: 200px;
|
||||
}
|
||||
.max-h-\[260px\] {
|
||||
max-height: 260px;
|
||||
}
|
||||
.max-h-\[320px\] {
|
||||
max-height: 320px;
|
||||
}
|
||||
.max-h-\[612px\] {
|
||||
max-height: 612px;
|
||||
}
|
||||
@@ -594,8 +583,8 @@
|
||||
.w-\[200px\] {
|
||||
width: 200px;
|
||||
}
|
||||
.w-\[260px\] {
|
||||
width: 260px;
|
||||
.w-\[220px\] {
|
||||
width: 220px;
|
||||
}
|
||||
.w-\[271px\] {
|
||||
width: 271px;
|
||||
@@ -624,15 +613,9 @@
|
||||
.max-w-\[300px\] {
|
||||
max-width: 300px;
|
||||
}
|
||||
.max-w-\[320px\] {
|
||||
max-width: 320px;
|
||||
}
|
||||
.max-w-\[450px\] {
|
||||
max-width: 450px;
|
||||
}
|
||||
.max-w-\[720px\] {
|
||||
max-width: 720px;
|
||||
}
|
||||
.max-w-lg {
|
||||
max-width: var(--container-lg);
|
||||
}
|
||||
@@ -669,9 +652,6 @@
|
||||
.min-w-\[8rem\] {
|
||||
min-width: 8rem;
|
||||
}
|
||||
.min-w-\[260px\] {
|
||||
min-width: 260px;
|
||||
}
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1319,10 +1299,6 @@
|
||||
--tw-shadow: 0 0 #0000;
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.shadow-sm {
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.shadow-xl {
|
||||
--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
@@ -2818,7 +2794,6 @@
|
||||
}
|
||||
@layer components {
|
||||
.button-group {
|
||||
isolation: isolate;
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
align-items: stretch;
|
||||
@@ -3725,7 +3700,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%;
|
||||
@@ -4268,6 +4243,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'] {
|
||||
@@ -5279,12 +5272,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));
|
||||
@@ -5559,6 +5552,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) }}
|
||||
|
||||
+191
-24
@@ -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,10 +60,10 @@ toc:
|
||||
type: "group",
|
||||
label: "Fruits",
|
||||
items: [
|
||||
{ type: "item", value: "apple", label: "Apple", attrs: {"data-force": "true"} },
|
||||
{ type: "item", value: "banana", label: "Banana", attrs: {"data-keywords": "bread"} },
|
||||
{ type: "item", value: "apple", label: "Apple" },
|
||||
{ type: "item", value: "banana", label: "Banana" },
|
||||
{ type: "item", value: "blueberry", label: "Blueberry" },
|
||||
{ type: "item", value: "pineapple", label: "Grapes" },
|
||||
{ type: "item", value: "grapes", label: "Grapes" },
|
||||
{ type: "item", value: "pineapple", label: "Pineapple" }
|
||||
]
|
||||
}
|
||||
@@ -120,44 +122,70 @@ 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>
|
||||
<li><code>data-close-on-select="true"</code> <span class="badge-secondary">Optional</span>: closes the popover when selecting an option in multiselect mode.</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 single-select: contains the selected option's value as a string.</p>
|
||||
<p>For multiselect: contains a <strong>JSON array</strong> of selected values (e.g., <code>["apple","banana"]</code>). When no options are selected, contains an empty array (<code>[]</code>).</p>
|
||||
<p><strong>Backend handling:</strong> Parse the JSON value on the server side. For example:</p>
|
||||
<ul>
|
||||
<li><strong>Python/Flask:</strong> <code>values = json.loads(request.form.get('field', '[]'))</code></li>
|
||||
<li><strong>PHP:</strong> <code>$values = json_decode($_POST['field'] ?? '[]', true);</code></li>
|
||||
<li><strong>Node.js:</strong> <code>const values = JSON.parse(req.body.field || '[]');</code></li>
|
||||
<li><strong>Ruby/Rails:</strong> <code>values = JSON.parse(params[:field] || '[]')</code></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
@@ -170,25 +198,103 @@ 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>
|
||||
|
||||
<h4 id="usage-html-js-5"><a href="#usage-html-js-5">JavaScript methods</a></h4>
|
||||
<h4 id="usage-html-js-5"><a href="#usage-html-js-5">JavaScript methods and properties</a></h4>
|
||||
|
||||
<section class="prose">
|
||||
<dl>
|
||||
<dt><code>selectByValue</code></dt>
|
||||
<dt><code>value</code> (property)</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>Get or set the current value. For single-select, this is a string. For multiselect, this is an array. The multiselect setter accepts both strings and arrays.</p>
|
||||
|
||||
{% set code_script %}<script>
|
||||
{% set code_value %}<script>
|
||||
const selectComponent = document.querySelector('#my-select');
|
||||
selectComponent.addEventListener('basecoat:initialized', () => {
|
||||
selectComponent.selectByValue('apple');
|
||||
// Get value
|
||||
console.log(selectComponent.value); // 'apple'
|
||||
|
||||
// Set value (single-select)
|
||||
selectComponent.value = 'banana';
|
||||
|
||||
// Set value (multiselect - accepts string or array)
|
||||
selectComponent.value = ['apple', 'banana'];
|
||||
selectComponent.value = 'apple'; // normalized to ['apple']
|
||||
});
|
||||
</script>{% endset %}
|
||||
{{ code_block(code_script | prettyHtml, "html") }}
|
||||
{{ code_block(code_value | prettyHtml, "html") }}
|
||||
</dd>
|
||||
<dt><code>select(value)</code></dt>
|
||||
<dd>
|
||||
<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 if not already selected.</p>
|
||||
|
||||
{% set code_select %}<script>
|
||||
const selectComponent = document.querySelector('#my-select');
|
||||
selectComponent.addEventListener('basecoat:initialized', () => {
|
||||
selectComponent.select('apple');
|
||||
});
|
||||
</script>{% endset %}
|
||||
{{ code_block(code_select | prettyHtml, "html") }}
|
||||
</dd>
|
||||
<dt><code>deselect(value)</code> <span class="badge-secondary">Multiselect only</span></dt>
|
||||
<dd>
|
||||
<p>Removes a specific value from the selection. Only available when the component is in multiselect mode (<code>aria-multiselectable="true"</code>).</p>
|
||||
|
||||
{% set code_deselect %}<script>
|
||||
const selectComponent = document.querySelector('#my-multiselect');
|
||||
selectComponent.addEventListener('basecoat:initialized', () => {
|
||||
selectComponent.deselect('apple');
|
||||
});
|
||||
</script>{% endset %}
|
||||
{{ code_block(code_deselect | prettyHtml, "html") }}
|
||||
</dd>
|
||||
<dt><code>toggle(value)</code> <span class="badge-secondary">Multiselect only</span></dt>
|
||||
<dd>
|
||||
<p>Toggles a specific value in the selection (adds if not present, removes if present). Only available when the component is in multiselect mode (<code>aria-multiselectable="true"</code>).</p>
|
||||
|
||||
{% set code_toggle %}<script>
|
||||
const selectComponent = document.querySelector('#my-multiselect');
|
||||
selectComponent.addEventListener('basecoat:initialized', () => {
|
||||
selectComponent.toggle('apple'); // adds if not selected, removes if selected
|
||||
});
|
||||
</script>{% endset %}
|
||||
{{ code_block(code_toggle | 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>
|
||||
<dt><code>selectByValue(value)</code></dt>
|
||||
<dd>
|
||||
<p><strong>Deprecated:</strong> Alias for <code>select(value)</code>. Kept for backward compatibility.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
</section>
|
||||
@@ -204,11 +310,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,6 +365,23 @@ 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 %}
|
||||
@@ -287,4 +410,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 adding <code>aria-multiselectable="true"</code> to the <code>listbox</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) }}
|
||||
|
||||
@@ -144,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>
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "basecoat",
|
||||
"version": "0.3.7",
|
||||
"version": "0.3.10",
|
||||
"description": "shadcn/ui for the rest of us",
|
||||
"author": "hunvreus",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "basecoat-cli",
|
||||
"version": "0.3.7",
|
||||
"version": "0.3.10",
|
||||
"description": "Add Basecoat components to your project",
|
||||
"author": "hunvreus",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "basecoat-css",
|
||||
"version": "0.3.7",
|
||||
"version": "0.3.10",
|
||||
"description": "Tailwind CSS for Basecoat components",
|
||||
"author": {
|
||||
"name": "hunvreus",
|
||||
|
||||
+14
-5
@@ -388,7 +388,8 @@
|
||||
/* Button Group */
|
||||
@layer components {
|
||||
.button-group {
|
||||
@apply inline-flex w-fit items-stretch isolate;
|
||||
@apply inline-flex w-fit items-stretch;
|
||||
/* isolate */
|
||||
|
||||
> *:focus-visible,
|
||||
> :is(.dropdown-menu, .popover, .select) > button:focus-visible {
|
||||
@@ -742,14 +743,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'],
|
||||
@@ -941,6 +942,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;
|
||||
@@ -1250,7 +1259,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;
|
||||
@@ -1299,4 +1308,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
{% endif %}
|
||||
>
|
||||
<span role="heading" id="{{ group_label_id }}">{{ item.label | safe }}</span>
|
||||
{{ render_command_items(item.items, item_id) if item.items }}
|
||||
{{ render_command_items(item['items'], item_id) if item['items'] }}
|
||||
</div>
|
||||
{% elif item.type == "separator" %}
|
||||
<hr role="separator" />
|
||||
@@ -183,7 +183,7 @@
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.content | safe if item.content else (item.label | safe) }}
|
||||
{{ item.label | safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div
|
||||
@@ -198,7 +198,7 @@
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.content | safe if item.content else (item.label | safe) }}
|
||||
{{ item.label | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -83,12 +83,10 @@
|
||||
</footer>
|
||||
{% endif %}
|
||||
{% if close_button %}
|
||||
<form method="dialog">
|
||||
<button aria-label="Close dialog">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
<button type="button" aria-label="Close dialog" onclick="this.closest('dialog').close()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</dialog>
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
+40
-17
@@ -2,8 +2,11 @@
|
||||
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 close_on_select {boolean} [optional] [default=false] - Closes the popover when selecting an option in multiple mode.
|
||||
@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 +20,9 @@
|
||||
selected=None,
|
||||
name=None,
|
||||
items=[],
|
||||
multiple=false,
|
||||
placeholder=None,
|
||||
close_on_select=false,
|
||||
main_attrs={},
|
||||
trigger_attrs={},
|
||||
popover_attrs={},
|
||||
@@ -27,43 +33,59 @@
|
||||
) %}
|
||||
{% set id = id or ("select-" + (range(100000, 999999) | random | string)) %}
|
||||
|
||||
{% if selected is defined and selected is iterable and selected is not string %}
|
||||
{% set selectedArray = selected %}
|
||||
{% elif selected is defined %}
|
||||
{% set selectedArray = [selected] %}
|
||||
{% else %}
|
||||
{% set selectedArray = [] %}
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
{% if multiple and close_on_select %}data-close-on-select="true"{% 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 +94,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,12 +132,13 @@
|
||||
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 %}
|
||||
@@ -124,7 +147,7 @@
|
||||
<input
|
||||
type="hidden"
|
||||
name="{{ name or id ~ '-value' }}"
|
||||
value="{{ (default_option.value if default_option) or '' }}"
|
||||
value="{% if multiple %}{{ selectedArray | tojson }}{% else %}{{ (default_option.value if default_option) or '' }}{% endif %}"
|
||||
{% for key, value in input_attrs.items() %}
|
||||
{% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
@@ -138,7 +161,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 +176,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" />
|
||||
@@ -161,8 +184,8 @@
|
||||
<div
|
||||
id="{{ item_id }}"
|
||||
role="option"
|
||||
data-value="{{ item.value }}"
|
||||
{% if selected == item.value %}aria-selected="true"{% endif %}
|
||||
{% if item.value is defined and item.value is not none %}data-value="{{ item.value }}"{% endif %}
|
||||
{% if item.value in selectedArray %}aria-selected="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
@@ -173,4 +196,4 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
||||
+197
-54
@@ -3,31 +3,38 @@
|
||||
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';
|
||||
const selectedOptions = isMultiple ? new Set() : null;
|
||||
const placeholder = isMultiple ? (selectComponent.dataset.placeholder || '') : null;
|
||||
const closeOnSelect = selectComponent.dataset.closeOnSelect === 'true';
|
||||
|
||||
const getValue = (opt) => opt.dataset.value ?? opt.textContent.trim();
|
||||
|
||||
const setActiveOption = (index) => {
|
||||
if (activeIndex > -1 && options[activeIndex]) {
|
||||
options[activeIndex].classList.remove('active');
|
||||
}
|
||||
|
||||
|
||||
activeIndex = index;
|
||||
|
||||
|
||||
if (activeIndex > -1) {
|
||||
const activeOption = options[activeIndex];
|
||||
activeOption.classList.add('active');
|
||||
@@ -46,68 +53,118 @@
|
||||
return parseFloat(style.transitionDuration) > 0 || parseFloat(style.transitionDelay) > 0;
|
||||
};
|
||||
|
||||
const updateValue = (option, triggerEvent = true) => {
|
||||
if (option) {
|
||||
selectedLabel.innerHTML = 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 updateValue = (optionOrOptions, triggerEvent = true) => {
|
||||
let value;
|
||||
|
||||
if (isMultiple) {
|
||||
const opts = Array.isArray(optionOrOptions) ? optionOrOptions : [];
|
||||
selectedOptions.clear();
|
||||
opts.forEach(opt => selectedOptions.add(opt));
|
||||
|
||||
// Get selected options in DOM order
|
||||
const selected = options.filter(opt => selectedOptions.has(opt));
|
||||
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');
|
||||
}
|
||||
|
||||
value = selected.map(getValue);
|
||||
input.value = JSON.stringify(value);
|
||||
} else {
|
||||
const option = optionOrOptions;
|
||||
if (!option) return;
|
||||
selectedLabel.innerHTML = option.innerHTML;
|
||||
value = getValue(option);
|
||||
input.value = value;
|
||||
}
|
||||
|
||||
options.forEach(opt => {
|
||||
const isSelected = isMultiple ? selectedOptions.has(opt) : opt === optionOrOptions;
|
||||
if (isSelected) {
|
||||
opt.setAttribute('aria-selected', 'true');
|
||||
} else {
|
||||
opt.removeAttribute('aria-selected');
|
||||
}
|
||||
});
|
||||
|
||||
if (triggerEvent) {
|
||||
selectComponent.dispatchEvent(new CustomEvent('change', {
|
||||
detail: { value },
|
||||
bubbles: true
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const closePopover = (focusOnTrigger = true) => {
|
||||
if (popover.getAttribute('aria-hidden') === 'true') return;
|
||||
|
||||
|
||||
if (filter) {
|
||||
const resetFilter = () => {
|
||||
filter.value = '';
|
||||
visibleOptions = [...options];
|
||||
allOptions.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
|
||||
};
|
||||
|
||||
|
||||
if (hasTransition()) {
|
||||
popover.addEventListener('transitionend', resetFilter, { once: true });
|
||||
} else {
|
||||
resetFilter();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (focusOnTrigger) trigger.focus();
|
||||
popover.setAttribute('aria-hidden', 'true');
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
setActiveOption(-1);
|
||||
}
|
||||
|
||||
const selectOption = (option) => {
|
||||
if (!option) return;
|
||||
|
||||
const oldValue = input.value;
|
||||
const newValue = option.dataset.value;
|
||||
|
||||
if (newValue != null && newValue !== oldValue) {
|
||||
updateValue(option);
|
||||
}
|
||||
|
||||
closePopover();
|
||||
};
|
||||
|
||||
const selectByValue = (value) => {
|
||||
const option = options.find(opt => opt.dataset.value === value);
|
||||
selectOption(option);
|
||||
const toggleMultipleValue = (option) => {
|
||||
if (selectedOptions.has(option)) {
|
||||
selectedOptions.delete(option);
|
||||
} else {
|
||||
selectedOptions.add(option);
|
||||
}
|
||||
updateValue(options.filter(opt => selectedOptions.has(opt)));
|
||||
};
|
||||
|
||||
const select = (value) => {
|
||||
if (isMultiple) {
|
||||
const option = options.find(opt => getValue(opt) === value && !selectedOptions.has(opt));
|
||||
if (!option) return;
|
||||
selectedOptions.add(option);
|
||||
updateValue(options.filter(opt => selectedOptions.has(opt)));
|
||||
} else {
|
||||
const option = options.find(opt => getValue(opt) === value);
|
||||
if (!option) return;
|
||||
if (input.value !== value) {
|
||||
updateValue(option);
|
||||
}
|
||||
closePopover();
|
||||
}
|
||||
};
|
||||
|
||||
const deselect = (value) => {
|
||||
if (!isMultiple) return;
|
||||
const option = options.find(opt => getValue(opt) === value && selectedOptions.has(opt));
|
||||
if (!option) return;
|
||||
selectedOptions.delete(option);
|
||||
updateValue(options.filter(opt => selectedOptions.has(opt)));
|
||||
};
|
||||
|
||||
const toggle = (value) => {
|
||||
if (!isMultiple) return;
|
||||
const option = options.find(opt => getValue(opt) === value);
|
||||
if (!option) return;
|
||||
toggleMultipleValue(option);
|
||||
};
|
||||
|
||||
if (filter) {
|
||||
const filterOptions = () => {
|
||||
const searchTerm = filter.value.trim().toLowerCase();
|
||||
|
||||
|
||||
setActiveOption(-1);
|
||||
|
||||
visibleOptions = [];
|
||||
@@ -133,17 +190,37 @@
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
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];
|
||||
}
|
||||
// Initialization
|
||||
if (isMultiple) {
|
||||
const ariaSelected = options.filter(opt => opt.getAttribute('aria-selected') === 'true');
|
||||
try {
|
||||
const parsed = JSON.parse(input.value || '[]');
|
||||
const validValues = new Set(options.map(getValue));
|
||||
const initialValues = Array.isArray(parsed) ? parsed.filter(v => validValues.has(v)) : [];
|
||||
|
||||
updateValue(initialOption, false);
|
||||
const initialOptions = [];
|
||||
if (initialValues.length > 0) {
|
||||
// Match values to options in order, allowing duplicates
|
||||
initialValues.forEach(val => {
|
||||
const opt = options.find(o => getValue(o) === val && !initialOptions.includes(o));
|
||||
if (opt) initialOptions.push(opt);
|
||||
});
|
||||
} else {
|
||||
initialOptions.push(...ariaSelected);
|
||||
}
|
||||
|
||||
updateValue(initialOptions, false);
|
||||
} catch (e) {
|
||||
updateValue(ariaSelected, false);
|
||||
}
|
||||
} else {
|
||||
const initialOption = options.find(opt => getValue(opt) === input.value) || options[0];
|
||||
if (initialOption) updateValue(initialOption, false);
|
||||
}
|
||||
|
||||
const handleKeyNavigation = (event) => {
|
||||
const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false';
|
||||
@@ -159,17 +236,28 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
closePopover();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
if (activeIndex > -1) {
|
||||
selectOption(options[activeIndex]);
|
||||
const option = options[activeIndex];
|
||||
if (isMultiple) {
|
||||
toggleMultipleValue(option);
|
||||
if (closeOnSelect) {
|
||||
closePopover();
|
||||
}
|
||||
} else {
|
||||
if (input.value !== getValue(option)) {
|
||||
updateValue(option);
|
||||
}
|
||||
closePopover();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -235,7 +323,7 @@
|
||||
document.dispatchEvent(new CustomEvent('basecoat:popover', {
|
||||
detail: { source: selectComponent }
|
||||
}));
|
||||
|
||||
|
||||
if (filter) {
|
||||
if (hasTransition()) {
|
||||
popover.addEventListener('transitionend', () => {
|
||||
@@ -248,7 +336,7 @@
|
||||
|
||||
popover.setAttribute('aria-hidden', 'false');
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
|
||||
|
||||
const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
|
||||
if (selectedOption) {
|
||||
setActiveOption(options.indexOf(selectedOption));
|
||||
@@ -267,8 +355,28 @@
|
||||
|
||||
listbox.addEventListener('click', (event) => {
|
||||
const clickedOption = event.target.closest('[role="option"]');
|
||||
if (clickedOption) {
|
||||
selectOption(clickedOption);
|
||||
if (!clickedOption) return;
|
||||
|
||||
const option = options.find(opt => opt === clickedOption);
|
||||
if (!option) return;
|
||||
|
||||
if (isMultiple) {
|
||||
toggleMultipleValue(option);
|
||||
if (closeOnSelect) {
|
||||
closePopover();
|
||||
} else {
|
||||
setActiveOption(options.indexOf(option));
|
||||
if (filter) {
|
||||
filter.focus();
|
||||
} else {
|
||||
trigger.focus();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (input.value !== getValue(option)) {
|
||||
updateValue(option);
|
||||
}
|
||||
closePopover();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -285,8 +393,43 @@
|
||||
});
|
||||
|
||||
popover.setAttribute('aria-hidden', 'true');
|
||||
|
||||
selectComponent.selectByValue = selectByValue;
|
||||
|
||||
// Public API
|
||||
Object.defineProperty(selectComponent, 'value', {
|
||||
get: () => {
|
||||
if (isMultiple) {
|
||||
return options.filter(opt => selectedOptions.has(opt)).map(getValue);
|
||||
} else {
|
||||
return input.value;
|
||||
}
|
||||
},
|
||||
set: (val) => {
|
||||
if (isMultiple) {
|
||||
const values = Array.isArray(val) ? val : (val != null ? [val] : []);
|
||||
const opts = [];
|
||||
values.forEach(v => {
|
||||
const opt = options.find(o => getValue(o) === v && !opts.includes(o));
|
||||
if (opt) opts.push(opt);
|
||||
});
|
||||
updateValue(opts);
|
||||
} else {
|
||||
const option = options.find(opt => getValue(opt) === val);
|
||||
if (option) {
|
||||
updateValue(option);
|
||||
closePopover();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
selectComponent.select = select;
|
||||
selectComponent.selectByValue = select; // Backward compatibility alias
|
||||
if (isMultiple) {
|
||||
selectComponent.deselect = deselect;
|
||||
selectComponent.toggle = toggle;
|
||||
selectComponent.selectAll = () => updateValue(options);
|
||||
selectComponent.selectNone = () => updateValue([]);
|
||||
}
|
||||
selectComponent.dataset.selectInitialized = true;
|
||||
selectComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
|
||||
};
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.content | safe if item.content else (item.label | safe) }}
|
||||
{{ item.label | safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div
|
||||
@@ -198,7 +198,7 @@
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.content | safe if item.content else (item.label | safe) }}
|
||||
{{ item.label | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
+39
-16
@@ -2,8 +2,11 @@
|
||||
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 close_on_select {boolean} [optional] [default=false] - Closes the popover when selecting an option in multiple mode.
|
||||
@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 +20,9 @@
|
||||
selected=None,
|
||||
name=None,
|
||||
items=None,
|
||||
multiple=false,
|
||||
placeholder=None,
|
||||
close_on_select=false,
|
||||
main_attrs={},
|
||||
trigger_attrs={},
|
||||
popover_attrs={},
|
||||
@@ -27,8 +33,15 @@
|
||||
) %}
|
||||
{% set id = id or ("select-" + (range(100000, 999999) | random | string)) %}
|
||||
|
||||
{% if selected is defined and selected is iterable and selected is not string %}
|
||||
{% set selectedArray = selected %}
|
||||
{% elif selected is defined %}
|
||||
{% set selectedArray = [selected] %}
|
||||
{% else %}
|
||||
{% set selectedArray = [] %}
|
||||
{% endif %}
|
||||
{% set first_option = [] %}
|
||||
{% set selected_option = [] %}
|
||||
{% set selected_options = [] %}
|
||||
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
@@ -37,33 +50,42 @@
|
||||
{% 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 %}
|
||||
{% if multiple and close_on_select %}data-close-on-select="true"{% 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 +94,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,12 +132,13 @@
|
||||
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 %}
|
||||
@@ -124,7 +147,7 @@
|
||||
<input
|
||||
type="hidden"
|
||||
name="{{ name or id ~ '-value' }}"
|
||||
value="{{ (default_option.value if default_option) or '' }}"
|
||||
value="{% if multiple %}{{ selectedArray | dump }}{% else %}{{ (default_option.value if default_option) or '' }}{% endif %}"
|
||||
{% for key, value in input_attrs %}
|
||||
{% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
@@ -138,7 +161,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 +176,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" />
|
||||
@@ -161,8 +184,8 @@
|
||||
<div
|
||||
id="{{ item_id }}"
|
||||
role="option"
|
||||
data-value="{{ item.value }}"
|
||||
{% if selected == item.value %}aria-selected="true"{% endif %}
|
||||
{% if item.value is defined and item.value is not none %}data-value="{{ item.value }}"{% endif %}
|
||||
{% if item.value in selectedArray %}aria-selected="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
@@ -173,4 +196,4 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
||||
Reference in New Issue
Block a user