More components and refactoring select

This commit is contained in:
Ronan Berder
2025-04-21 10:37:15 +08:00
parent ec2f68d3b0
commit 88af4152ab
23 changed files with 2357 additions and 1576 deletions
+887 -823
View File
File diff suppressed because it is too large Load Diff
+210 -151
View File
@@ -136,20 +136,22 @@
body {
@apply bg-background text-foreground overscroll-none antialiased;
}
::-webkit-scrollbar {
}
@utility scrollbar {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
&::-webkit-scrollbar {
width: var(--scrollbar-width);
}
::-webkit-scrollbar-track {
&::-webkit-scrollbar-track {
background: var(--scrollbar-track);
}
::-webkit-scrollbar-thumb {
&::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: var(--scrollbar-radius);
}
* {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
}
@layer utilities {
@@ -375,17 +377,20 @@
details {
&::details-content {
block-size: 0;
transition-behavior: allow-discrete;
@apply block opacity-0 transition-all;
@apply block opacity-0 transition-discrete
transition-all;
}
&[open]::details-content {
block-size: auto;
block-size: calc-size(auto, size);
@apply opacity-100;
}
summary {
@apply inline-flex items-center cursor-pointer;
}
}
details[open]::details-content {
block-size: auto;
block-size: calc-size(auto, size);
@apply opacity-100;
}
details > summary {
@apply inline-flex items-center cursor-pointer;
details > summary::-webkit-details-marker {
display: none;
}
}
@@ -429,97 +434,6 @@
}
}
/* Dropdown */
@layer components {
.dropdown {
@apply relative inline-flex;
[role='menu'],
[role='listbox'] {
@apply absolute bg-popover text-popover-foreground overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md z-50 w-max visible opacity-100 scale-100 transition-all;
&[aria-hidden='true'] {
@apply invisible opacity-0 scale-95;
&:not([data-side]),
&[data-side='bottom'] {
@apply -translate-y-2;
}
&[data-side='top'] {
@apply translate-y-2;
}
&[data-side='left'] {
@apply translate-x-2;
}
&[data-side='right'] {
@apply -translate-x-2;
}
}
&:not([data-side]),
&[data-side='bottom'] {
@apply mt-1 top-full;
}
&[data-side='top'] {
@apply mb-1 bottom-full;
}
&[data-side='left'] {
@apply mr-1 right-full;
}
&[data-side='right'] {
@apply ml-1 left-full;
}
&:not([data-side]),
&[data-side='bottom'],
&[data-side='top'] {
&:not([data-align]),
&[data-align='start'] {
@apply left-0;
}
&[data-align='end'] {
@apply right-0;
}
&[data-align='center'] {
@apply left-1/2 -translate-x-1/2;
}
}
&[data-side='left'],
&[data-side='right'] {
&:not([data-align]),
&[data-align='start'] {
@apply top-0;
}
&[data-align='end'] {
@apply bottom-0;
}
&[data-align='center'] {
@apply top-1/2 -translate-y-1/2;
}
}
[role='menuitem'],
[role='option'] {
@apply hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground [&_svg]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none [&_svg]:shrink-0 [&_svg]:size-4 disabled:opacity-50 disabled:pointer-events-none w-full truncate;
}
[role='option'] {
@apply pr-7.5;
&[aria-selected='true'] {
@apply bg-[image:var(--check-icon)] bg-no-repeat bg-position-[center_right_0.5rem] bg-size-[0.875rem];
}
}
[role='separator'] {
@apply border-border -mx-1 my-1;
}
}
[role='menu'] [role='heading'] {
@apply flex px-2 py-1.5 text-sm font-medium;
}
[role='listbox'] [role='heading'] {
@apply flex text-muted-foreground px-2 py-1.5 text-xs;
}
}
}
/* Input */
@layer components {
.form input[type='text'],
@@ -579,6 +493,123 @@
}
}
/* Popover */
@layer components {
.popover {
@apply relative inline-flex;
> [data-popover] {
@apply absolute bg-popover text-popover-foreground overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md z-50 w-max visible opacity-100 scale-100 min-w-full w-max transition-all;
&[aria-hidden='true'] {
@apply invisible opacity-0 scale-95;
&:not([data-side]),
&[data-side='bottom'] {
@apply -translate-y-2;
}
&[data-side='top'] {
@apply translate-y-2;
}
&[data-side='left'] {
@apply translate-x-2;
}
&[data-side='right'] {
@apply -translate-x-2;
}
}
&:not([data-side]),
&[data-side='bottom'] {
@apply mt-1 top-full;
}
&[data-side='top'] {
@apply mb-1 bottom-full;
}
&[data-side='left'] {
@apply mr-1 right-full;
}
&[data-side='right'] {
@apply ml-1 left-full;
}
&:not([data-side]),
&[data-side='bottom'],
&[data-side='top'] {
&:not([data-align]),
&[data-align='start'] {
@apply left-0;
}
&[data-align='end'] {
@apply right-0;
}
&[data-align='center'] {
@apply left-1/2 -translate-x-1/2;
}
}
&[data-side='left'],
&[data-side='right'] {
&:not([data-align]),
&[data-align='start'] {
@apply top-0;
}
&[data-align='end'] {
@apply bottom-0;
}
&[data-align='center'] {
@apply top-1/2 -translate-y-1/2;
}
}
[role='menuitem'],
[role='option'] {
@apply aria-hidden:hidden [&_svg]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none [&_svg]:shrink-0 [&_svg]:size-4 aria-disabled:opacity-50 aria-disabled:pointer-events-none w-full truncate;
}
[role='menuitem'] {
@apply hover:bg-accent hover:text-accent-foreground;
}
[role='option'] {
@apply data-[focus]:bg-accent data-[focus]:text-accent-foreground pr-7.5;
&[aria-selected='true'] {
@apply bg-[image:var(--check-icon)] bg-no-repeat bg-position-[center_right_0.5rem] bg-size-[0.875rem];
}
}
[role='menu'] [role='heading'] {
@apply flex px-2 py-1.5 text-sm font-medium;
}
[role='listbox'] [role='heading'] {
@apply flex text-muted-foreground px-2 py-1.5 text-xs;
}
[role='listbox'] [role='group']:not(:has([role='option']:not([aria-hidden='true']))) {
@apply hidden;
}
[role='separator'] {
@apply border-border -mx-1 my-1;
}
> header {
@apply flex h-9 items-center gap-2 border-b px-3 -mx-1 -mt-1 mb-1;
svg {
@apply size-4 shrink-0 opacity-50;
}
input[role='combobox'] {
@apply placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50;
}
}
/* Combobox */
[role='listbox']:not(:has([data-value]:not([aria-hidden='true'])))::before {
@apply flex items-center justify-center py-6 text-sm truncate;
}
[role='listbox'][data-empty]:not(:has([data-value]:not([aria-hidden='true'])))::before {
@apply content-[attr(data-empty)];
}
[role='listbox']:not([data-empty]):not(:has([data-value]:not([aria-hidden='true'])))::before {
@apply content-['No_results_found'];
}
}
}
}
/* Radio */
@layer components {
.form input[type='radio'],
@@ -595,49 +626,48 @@
@layer components {
.form input[type='range'],
.input[type='range'] {
@apply appearance-none flex items-center;
@apply appearance-none flex items-center p-0 outline-none;
--slider-value: 20%;
&::-webkit-slider-thumb {
@apply appearance-none border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50;
&:focus {
@apply ring-2 ring-offset-2 ring-offset-background ring-primary transition-all;
&:hover,
&:focus-visible {
&::-webkit-slider-thumb {
@apply ring-4;
}
&::-moz-range-thumb {
@apply ring-4;
}
&::-ms-thumb {
@apply ring-4;
}
}
&::-webkit-slider-thumb {
@apply appearance-none border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm -mt-1.25;
}
&::-webkit-slider-runnable-track {
@apply appearance-none bg-primary h-1.5 rounded-full;
@apply appearance-none rounded-full h-1.5 w-full;
background: linear-gradient(to right, var(--primary) var(--slider-value), var(--muted) var(--slider-value));
}
&::-moz-range-thumb {
@apply appearance-none border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm -mt-1.25;
}
/* .input[type="range"]:focus::-webkit-slider-thumb {
@apply ring-2 ring-offset-2 ring-offset-background ring-primary transition-all;
&::-moz-range-track {
@apply appearance-none rounded-full h-1.5 w-full;
background: linear-gradient(to right, var(--primary) var(--slider-value), var(--muted) var(--slider-value));
}
.input[type="range"]::-webkit-slider-runnable-track {
@apply bg-input h-2 rounded-full;
&::-ms-thumb {
@apply appearance-none border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm -mt-1.25;
}
.input[type="range"]::-moz-range-thumb {
@apply outline-none appearance-none bg-primary w-6 h-6 rounded-full cursor-grab p-0 m-0 border-0 focus:ring-2 hover:bg-primary/90 transition-all;
&::-ms-track {
@apply appearance-none rounded-full h-1.5 w-full;
}
.input[type="range"]:focus::-moz-range-thumb {
@apply ring-2 ring-offset-2 ring-offset-background ring-primary transition-all;
}
.input[type="range"]::-moz-range-track {
@apply bg-input h-2 rounded-full border-0;
}
.input[type="range"]::-ms-thumb {
@apply outline-none appearance-none bg-primary w-6 h-6 rounded-full cursor-grab p-0 m-0 border-0 focus:ring-2 hover:bg-primary/90 transition-all;
}
.input[type="range"]:focus::-ms-thumb {
@apply ring-2 ring-offset-2 ring-offset-background ring-primary transition-all;
}
.input[type="range"]::-ms-track {
@apply bg-input h-2 rounded-full border-0 text-transparent;
}
.input[type="range"]::-ms-fill-lower {
&::-ms-fill-lower {
@apply bg-primary rounded-full;
}
.input[type="range"]::-ms-fill-upper {
@apply bg-input rounded-full;
} */
&::-ms-fill-upper {
@apply bg-muted rounded-full;
}
}
}
@@ -645,7 +675,7 @@
@layer components {
.form select,
.select {
@apply appearance-none border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent pl-3 pr-9 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 h-9 leading-none;
@apply appearance-none border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent pl-3 pr-9 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 h-9;
@apply bg-[image:var(--chevron-down-icon)] bg-no-repeat bg-position-[center_right_0.75rem] bg-size-[1rem];
}
}
@@ -776,16 +806,45 @@
/* Tabs */
@layer components {
ul[role='tablist'] {
@apply flex items-center border-b border-border;
.tabs {
[role='tablist'] {
@apply bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px];
[role='tab'] {
@apply focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4;
&[aria-selected='true'] {
@apply bg-background dark:text-foreground dark:border-input dark:bg-input/30 shadow-sm;
}
}
}
[role='tabpanel'] {
@apply flex-1 outline-none;
}
}
ul[role='tablist'] li {
@apply flex items-center gap-x-2 -mb-[1px];
}
/* Card */
@layer components {
.card {
@apply bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm;
> header {
@apply @container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6;
h2 {
@apply leading-none font-semibold;
}
p {
@apply text-muted-foreground text-sm;
}
}
> section {
@apply px-6;
}
> footer {
@apply flex items-center px-6 [.border-t]:pt-6;
}
}
ul[role='tablist'] li [role='tab'] {
@apply flex items-center gap-x-2 font-medium text-muted-foreground px-3 py-2.5 hover:text-foreground transition-colors border-b-2 border-transparent;
}
ul[role='tablist'] li [role='tab'][aria-selected='true'] {
@apply text-foreground border-b-primary;
}
}
}
+15 -16
View File
@@ -1,20 +1,19 @@
{% macro dialog(
id,
trigger=None,
title=None,
description=None,
footer=None,
main_attrs={},
trigger_attrs={},
content_attrs={},
content_header_attrs={},
content_body_attrs={},
content_footer_attrs={},
open=false,
close_button=true,
close_on_overlay_click=true
)
%}
id,
trigger=None,
title=None,
description=None,
footer=None,
main_attrs={},
trigger_attrs={},
content_attrs={},
content_header_attrs={},
content_body_attrs={},
content_footer_attrs={},
open=false,
close_button=true,
close_on_overlay_click=true
) %}
{% if registerComponent('dialog') %}
<script>
document.addEventListener('alpine:init', () => {
+129
View File
@@ -0,0 +1,129 @@
{% macro popover(
id=None,
trigger=None,
main_attrs=None,
trigger_attrs=None,
content_attrs=None
) %}
{% if registerComponent('popover') %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('popover', () => ({
open: false,
focusedIndex: null,
menuItems: [],
init() {
this.$nextTick(() => {
this.menuItems = Array.from(this.$el.querySelectorAll('[role=menuitem]'));
});
},
handleTriggerClick() {
this.open = !this.open;
},
handleEscape() {
if (this.open) this.open = false;
},
focusNavItem() {
if (this.menuItems.length === 0) return;
if (this.focusedIndex >= this.menuItems.length) {
this.focusedIndex = this.menuItems.length - 1;
}
if (this.focusedIndex < 0) {
this.focusedIndex = 0;
}
this.menuItems[this.focusedIndex].focus();
},
nextNavItem() {
if (this.menuItems.length === 0) return;
if (this.focusedIndex === null) {
this.focusedIndex = 0;
} else {
this.focusedIndex++;
}
this.focusNavItem();
},
prevNavItem() {
if (this.menuItems.length === 0) return;
if (this.focusedIndex === null) {
this.focusedIndex = 0;
} else {
this.focusedIndex--;
}
this.focusNavItem();
},
handleNavItemClick(event) {
if (this.menuItems.length === 0) return;
const menuItem = event.target.closest('[role=menuitem]');
if (menuItem && !menuItem.disabled) {
menuItem.blur();
this.open = false;
}
},
$trigger: {
'@click'() { this.handleTriggerClick() },
'@keydown.down.prevent'() { this.open && this.nextNavItem() },
':aria-expanded'() { return this.open },
},
$content: {
'@click'(e) { this.handleNavItemClick(e) },
'@keydown.escape.window'() { this.handleEscape() },
'@keydown.down.prevent'() { this.nextNavItem() },
'@keydown.up.prevent'() { this.prevNavItem() },
'@keydown.home.prevent'() { this.focusNavItem(0) },
'@keydown.end.prevent'() { this.focusNavItem(this.menuItems.length - 1) },
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
}));
});
</script>
{% endif %}
<div
class="popover {% if main_attrs %}{{ main_attrs.pop('class', '') }}{% endif %}"
x-data="popover"
@click.away="open = false"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in main_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if trigger %}
<button
type="button"
aria-haspopup="menu"
aria-expanded="false"
x-bind="$trigger"
{% if id %}
id="{{ id }}-trigger"
aria-controls="{{ id }}-menu"
{% endif %}
{% for key, value in trigger_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ trigger | safe }}
</button>
{% endif %}
<div
data-popover
aria-hidden="true"
x-bind="$content"
{% if id %}id="{{ id }}-menu"{% endif %}
{% for key, value in content_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ caller() }}
</div>
</div>
{% endmacro %}
+177 -97
View File
@@ -1,144 +1,224 @@
{% macro select(
id=None,
main_attrs=None,
trigger_attrs=None,
menu_attrs=None,
value=None,
input_name=None
)
%}
id=None,
name=None,
selected=None,
main_attrs=None,
trigger_attrs=None,
content_attrs=None,
listbox_attrs=None,
is_combobox=False
) %}
{% if registerComponent('select') %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('select', (initialValue = null) => ({
Alpine.data('select', (name = null, initialValue = null) => ({
open: false,
focusedIndex: -1,
name: null,
options: [],
selectedLabel: initialValue,
selectedValue: initialValue,
disabledOptions: [],
focusedIndex: null,
selectedLabel: null,
selectedValue: null,
query: '',
init() {
this.$nextTick(() => {
const listboxElement = this.$el.querySelector('[role="listbox"]');
this.options = Array.from(listboxElement.querySelectorAll('[role=option]'));
this.selectOptionByValue(initialValue);
if (name) this.name = name;
this.options = Array.from(this.$el.querySelectorAll('[role=option]:not([aria-disabled])'));
this.disabledOptions = Array.from(this.$el.querySelectorAll('[role=option][aria-disabled=true]'));
if (this.options.length === 0) return;
if (initialValue) {
const option = this.options.find(opt => opt.getAttribute('data-value') === initialValue);
this.selectOption(option);
this.focusedIndex = this.options.indexOf(option);
this.focusOption();
} else {
this.selectOption(this.options[0]);
}
});
},
$trigger: {
'@click'() { this.handleClick() },
'@keydown.down.prevent'() { this.open && this.nextOption() },
':aria-expanded'() { return this.open },
':aria-pressed'() { return this.open }
focusOption() {
if (this.options.length === 0) return;
if (this.focusedIndex >= this.options.length) {
this.focusedIndex = this.options.length - 1;
} else if (this.focusedIndex < 0 || this.focusedIndex === null) {
this.focusedIndex = 0;
}
this.options.forEach(opt => opt.removeAttribute('data-focus'));
this.options[this.focusedIndex].setAttribute('data-focus', '');
},
$listbox: {
'@click'(e) { this.handleOptionClick(e) },
'@keydown.escape.window'() { this.handleEscape() },
'@keydown.down.prevent'() { this.nextOption() },
'@keydown.up.prevent'() { this.prevOption() },
focusOnSelectedOption() {
if (this.options.length === 0 || this.selectedValue === null) return;
const option = this.options.find(opt => opt.getAttribute('data-value') === this.selectedValue);
if (!option) return;
this.focusedIndex = this.options.indexOf(option);
this.focusOption();
},
moveOptionFocus(delta) {
if (this.options.length === 0) return;
if (!this.open) {
this.open = true;
this.focusOnSelectedOption();
} else {
this.focusedIndex = this.focusedIndex === null
? 0
: this.focusedIndex + delta;
}
this.focusOption();
},
handleOptionClick(event) {
const option = event.target.closest('[role=option]');
if (option && option.getAttribute('aria-disabled') !== 'true') {
this.selectOption(option);
this.open = false;
this.$nextTick(() => this.$refs.trigger.focus());
}
},
handleOptionMousemove(event) {
console.log("here");
const option = event.target.closest('[role=option]');
if (option && option.getAttribute('aria-disabled') !== 'true') {
this.focusedIndex = this.options.indexOf(option);
this.focusOption();
}
},
handleOptionEnter(event) {
this.selectOption(this.options[this.focusedIndex]);
this.open = !this.open;
},
selectOption(option) {
if (this.options.length === 0 || !option || option.disabled || !option.getAttribute('data-value')
) return;
this.options.forEach(opt => {
opt.setAttribute('aria-selected', opt === option);
});
this.selectedLabel = option.innerHTML;
this.selectedValue = option.getAttribute('data-value');
this.$dispatch('select:change', {
value: this.selectedValue,
label: this.selectedLabel
});
},
filterOptions(query) {
if (query.length > 0) {
this.disabledOptions.forEach(opt => { opt.setAttribute('aria-hidden', 'true'); });
} else {
this.disabledOptions.forEach(opt => { opt.removeAttribute('aria-hidden'); });
}
this.options.forEach(opt => {
opt.removeAttribute('aria-hidden');
if (opt.getAttribute('data-value') != null && !opt.innerHTML.toLowerCase().includes(query.toLowerCase())) {
opt.setAttribute('aria-hidden', 'true');
}
});
},
$trigger: {
'@click'() { this.open = !this.open; this.focusOnSelectedOption(); },
'@keydown.escape.window'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveOptionFocus(+1); },
'@keydown.up.prevent'() { this.moveOptionFocus(-1); },
'@keydown.home.prevent'() { this.focusOption(0) },
'@keydown.end.prevent'() { this.focusOption(this.options.length - 1) },
'@keydown.enter.prevent'() { this.handleOptionEnter() },
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@click'(e) { this.handleOptionClick(e) },
'@mousemove'(e) { this.handleOptionMousemove(e) },
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
selectOption(option) {
if (option && !option.disabled) {
this.options.forEach(opt => {
opt.setAttribute('aria-selected', opt === option);
});
this.selectedLabel = option.innerHTML;
this.selectedValue = option.getAttribute('data-value');
option.blur();
this.open = false;
} else if (this.options.length > 0) {
this.selectOption(this.options[0]);
}
},
selectOptionByValue(value) {
const option = this.options.find(opt => opt.getAttribute('data-value') === value);
this.selectOption(option);
},
focusOption(index) {
if (index >= 0 && index < this.options.length) {
this.focusedIndex = index;
this.options[index].focus();
}
},
nextOption() {
this.focusOption((this.focusedIndex + 1) % this.options.length);
},
prevOption() {
this.focusOption(this.focusedIndex <= 0 ? this.options.length - 1 : this.focusedIndex - 1);
},
handleClick() {
this.open = !this.open;
this.$nextTick(() => {
if(this.open && this.options.length) this.focusOption(0);
});
},
handleEscape() {
if (this.open) this.open = false;
},
handleOptionClick(event) {
const option = event.target.closest('[role=option]');
this.selectOption(option);
},
$filter: {
'@input'(e) { this.filterOptions(e.target.value) },
'@keydown.down.prevent'() { this.moveOptionFocus(+1); },
'@keydown.up.prevent'() { this.moveOptionFocus(-1); },
'@keydown.home.prevent'() { this.focusOption(0) },
'@keydown.end.prevent'() { this.focusOption(this.options.length - 1) },
'@keydown.enter.prevent'() { this.handleOptionEnter() },
}
}));
});
</script>
{% endif %}
<div
class="dropdown {% if main_attrs %}{{ main_attrs.pop('class', '') }}{% endif %}"
x-data="select('{{ value if value else '' }}')"
class="popover {{ main_attrs.class }}"
x-data="select('{{ name }}', '{{ selected }}')"
@click.away="open = false"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in main_attrs %}
{{ key }}="{{ value }}"
{% for key, value in trigger_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<button
type="button"
aria-haspopup="menu"
aria-haspopup="listbox"
aria-expanded="false"
x-bind="$trigger"
class="btn-outline justify-between font-normal"
{% if id %}
id="{{ id }}-trigger"
aria-controls="{{ id }}-menu"
aria-controls="{{ id }}-content"
{% endif %}
class="btn-outline justify-between font-normal {{ trigger_attrs.class }}"
{% for key, value in trigger_attrs %}
{{ key }}="{{ value }}"
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<span x-html="selectedLabel" class="flex items-center gap-x-2"></span>
<span class="text-muted-foreground opacity-50">{% lucide "chevron-down" %}</span>
<div x-html="selectedLabel" class="flex items-center gap-x-2"
></div>
{% lucide "chevron-down", class="text-muted-foreground opacity-50 shrink-0" %}
</button>
<div
class="min-w-full w-fit"
role="listbox"
aria-orientation="vertical"
data-popover
aria-hidden="true"
x-bind="$listbox"
{% if id %}id="{{ id }}-menu"{% endif %}
{% for key, value in menu_attrs %}
x-bind="$content"
{% if id %}id="{{ id }}-content"{% endif %}
{% for key, value in content_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ caller() }}
{% if is_combobox %}
<header>
{% lucide "search" %}
<input
type="text"
value=""
placeholder="Search framework..."
autocomplete="off"
autocorrect="off"
spellcheck="false"
aria-autocomplete="list"
role="combobox"
aria-expanded="true"
aria-controls="{{ id }}-content"
aria-labelledby="{{ id }}-trigger"
x-model="query"
x-bind="$filter"
>
</header>
{% endif %}
<div
role="listbox"
aria-orientation="vertical"
{% for key, value in listbox_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ caller() }}
</div>
</div>
{% if input_name %}
<input type="hidden" name="{{ input_name }}" x-model="selectedValue">
{% if name %}
<input type="hidden" name="{{ name }}" x-model="selectedValue">
{% endif %}
</div>
{% endmacro %}
@@ -1,15 +1,14 @@
{% macro dropdown(
id=None,
trigger=None,
{% macro tabs(
tabs,
main_attrs=None,
trigger_attrs=None,
menu_attrs=None
content_attrs=None
)
%}
{% if registerComponent('dropdown') %}
{% if registerComponent('toast') %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
Alpine.data('toast', () => ({
open: false,
focusedIndex: -1,
menuItems: [],
@@ -78,8 +77,8 @@
{% endif %}
<div
class="dropdown {% if main_attrs %}{{ main_attrs.pop('class', '') }}{% endif %}"
x-data="dropdown"
class="toast {% if main_attrs %}{{ main_attrs.pop('class', '') }}{% endif %}"
x-data="toast"
@click.away="open = false"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in main_attrs %}
+76 -42
View File
@@ -1,15 +1,12 @@
{% macro dropdown(
id=None,
trigger=None,
main_attrs=None,
trigger_attrs=None,
menu_attrs=None
)
%}
{% if registerComponent('dropdown') %}
{% macro toast(
id='toasts',
messages=None,
main_attrs=None
) %}
{% if registerComponent('toast') %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
Alpine.data('toast', () => ({
open: false,
focusedIndex: -1,
menuItems: [],
@@ -78,44 +75,81 @@
{% endif %}
<div
class="dropdown {% if main_attrs %}{{ main_attrs.pop('class', '') }}{% endif %}"
x-data="dropdown"
@click.away="open = false"
{% if id %}id="{{ id }}"{% endif %}
id="{{ id }}"
class="toasts"
x-data="toast"
{% for key, value in main_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if trigger %}
<button
type="button"
aria-haspopup="menu"
aria-expanded="false"
aria-pressed="false"
x-bind="$trigger"
{% if id %}
id="{{ id }}-trigger"
aria-controls="{{ id }}-menu"
{% endif %}
{% for key, value in trigger_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ trigger | safe }}
</button>
{% endif %}
<nav
role="menu"
aria-orientation="vertical"
{% for category, message in toasts %}
<div
x-data="{
active: true,
init() {
if ('{{ category }}' !== 'error') setTimeout(() => this.active = false, 3000)
}
}"
role="{{ 'alert' if category == 'error' else 'status' }}"
aria-atomic="true"
aria-hidden="true"
x-bind="$menu"
{% if id %}id="{{ id }}-menu"{% endif %}
{% for key, value in menu_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
:aria-hidden="!active"
:inert="!active"
>
{{ caller() }}
</nav>
<button
x-on:click="active = !active"
aria-label="{{ _('Dismiss') }}"
>
{% include "icons/x.svg" %}
</button>
<section class="w-full text-left flex-1">
{% if message is string %}
<p class="font-medium w-full text-left flex-1">{{ message }}</p>
{% else %}
<h4 class="font-medium mb-1">{{ message.title }}</h4>
<p class="text-muted-foreground">{{ message.description }}</p>
{% if message.action %}
<a
href="{{ message.action.url }}"
class="btn-outline h-8 px-2.5 mt-4"
>{{ message.action.label }}</a>
{% endif %}
{% endif %}
</section>
{% if category in ('error', 'success') %}
{% if category == 'error' %}
<div class="text-danger py-0.5">
{% include "icons/circle-x.svg" %}
</div>
{% elif category == 'success' %}
<div class="text-success py-0.5">
{% include "icons/circle-check.svg" %}
</div>
{% endif %}
{% endif %}
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro toasts(
id=None,
messages=None,
flash=True
) %}
{% if flash %}
{% set toasts = (messages or []) + get_flashed_messages(with_categories=true) %}
{% else %}
{% set toasts = messages or [] %}
{% endif %}
<div
class="toasts"
id="{{ id or 'toasts' }}"
{}
>
</div>
{% endmacro %}
+34
View File
@@ -0,0 +1,34 @@
<section id="avatar" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Avatar</h2>
</header>
<div class="p-4">
<div class="flex flex-row flex-wrap items-center gap-4">
<img class="size-8 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">
<span class="size-8 shrink-0 bg-muted flex items-center justify-center rounded-full">CN</span>
<img class="size-12 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">
<img class="size-8 shrink-0 object-cover rounded-lg" alt="@hunvreus" src="https://github.com/hunvreus.png">
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-8 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-12 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
<div class="flex -space-x-2 hover:space-x-1 [&_img]:ring-background [&_img]:size-12 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full [&_img]:ring-2 [&_img]:grayscale [&_img]:transition-all [&_img]:ease-in-out [&_img]:duration-300">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
</div>
</div>
</section>
+8 -6
View File
@@ -12,14 +12,16 @@
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{% call dropdown(
id="dropdown-menu",
{% call popover(
id="demo-breadcrumb-menu",
trigger=trigger,
trigger_attrs={"class": "flex size-9 items-center justify-center h-4 w-4"}
trigger_attrs={"class": "flex size-9 items-center justify-center h-4 w-4 hover:text-foreground cursor-pointer"}
) %}
<button type="button" role="menuitem">Documentation</button>
<button type="button" role="menuitem">Themes</button>
<button type="button" role="menuitem">GitHub</button>
<nav role="menu">
<button type="button" role="menuitem">Documentation</button>
<button type="button" role="menuitem">Themes</button>
<button type="button" role="menuitem">GitHub</button>
</nav>
{% endcall %}
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
+144
View File
@@ -0,0 +1,144 @@
<section id="card" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Card</h2>
</header>
<div class="p-4">
<div class="flex flex-col items-start gap-4">
<div class="card w-full max-w-sm">
<header>
<h2>Login to your account</h2>
<p>Enter your details below to login to your account</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-2">
<label for="demo-card-form-email">Email</label>
<input type="email" id="demo-card-form-email">
</div>
<div class="grid gap-2">
<div class="flex items-center gap-2">
<label for="demo-card-form-password">Password</label>
<a href="#" class="ml-auto inline-block text-sm underline-offset-4 hover:underline">Forgot your password?</a>
</div>
<input type="password" id="demo-card-form-password">
</div>
</form>
</section>
<footer class="flex flex-col items-center gap-2">
<button type="button" class="btn w-full">Login</button>
<button type="button" class="btn-outline w-full">Login with Google</button>
<p class="mt-4 text-center text-sm">Don't have an account? <a href="#" class="underline-offset-4 hover:underline">Sign up</a></p>
</footer>
</div>
<div class="card">
<header>
<h2>Meeting Notes</h2>
<p>Transcript from the meeting with the client.</p>
</header>
<section class="text-sm">
<p>Client requested dashboard redesign with focus on mobile responsiveness.</p>
<ol class="mt-4 flex list-decimal flex-col gap-2 pl-6">
<li>New analytics widgets for daily/weekly metrics</li>
<li>Simplified navigation menu</li>
<li>Dark mode support</li>
<li>Timeline: 6 weeks</li>
<li>Follow-up meeting scheduled for next Tuesday</li>
</ol>
</section>
<footer class="flex items-center">
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-8 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
</footer>
</div>
<div class="card">
<header>
<h2>Is this an image?</h2>
<p>This is a card with an image.</p>
</header>
<section class="px-0">
<img
alt="Photo by Drew Beamer"
loading="lazy"
width="500"
height="500"
class="aspect-video object-cover" style="color:transparent"
srcset="
https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=640&q=75 1x,
https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=1080&q=75 2x
"
src="https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=1080&q=75"
/>
</section>
<footer class="flex items-center gap-2">
<span class="badge-outline">
{% lucide "bed" %}
1
</span>
<span class="badge-outline">
{% lucide "bath" %}
2
</span>
<span class="badge-outline">
{% lucide "land-plot" %}
350m²
</span>
<span class="ml-auto font-medium tabular-nums">
$135,000
</span>
</footer>
</div>
<div class="flex w-full flex-wrap items-start gap-8 md:*:[.card]:basis-1/4">
<div class="card">
<section>Content Only</section>
</div>
<div class="card">
<header>
<h2>Header Only</h2>
<p>This is a card with a header and a description.</p>
</header>
</div>
<div class="card">
<header>
<h2>Header and Content</h2>
<p>This is a card with a header and a content.</p>
</header>
<section>Content only.</section>
</div>
<div class="card">
<footer>Footer Only</footer>
</div>
<div class="card">
<header>
<h2>Header + Footer</h2>
<p>This is a card with a header and a footer.</p>
</header>
<footer>Footer</footer>
</div>
<div class="card">
<section>Content</section>
<footer>Footer</footer>
</div>
<div class="card">
<header>
<h2>Header + Content + Footer</h2>
<p>This is a card with a header, content and footer.</p>
</header>
<section>Content</section>
<footer>Footer</footer>
</div>
</div>
</div>
</div>
</section>
+66
View File
@@ -0,0 +1,66 @@
<section id="combobox" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Combobox</h2>
</header>
<div class="p-4">
{% set options_timezones = [
{
label: "Americas",
timezones: [
{ value: "America/New_York", label: "(GMT-5) New York" },
{ value: "America/Los_Angeles", label: "(GMT-8) Los Angeles" },
{ value: "America/Chicago", label: "(GMT-6) Chicago" },
{ value: "America/Toronto", label: "(GMT-5) Toronto" },
{ value: "America/Vancouver", label: "(GMT-8) Vancouver" },
{ value: "America/Sao_Paulo", label: "(GMT-3) São Paulo" }
]
},
{
label: "Europe",
timezones: [
{ value: "Europe/London", label: "(GMT+0) London" },
{ value: "Europe/Paris", label: "(GMT+1) Paris" },
{ value: "Europe/Berlin", label: "(GMT+1) Berlin" },
{ value: "Europe/Rome", label: "(GMT+1) Rome" },
{ value: "Europe/Madrid", label: "(GMT+1) Madrid" },
{ value: "Europe/Amsterdam", label: "(GMT+1) Amsterdam" }
]
},
{
label: "Asia/Pacific",
timezones: [
{ value: "Asia/Tokyo", label: "(GMT+9) Tokyo" },
{ value: "Asia/Shanghai", label: "(GMT+8) Shanghai" },
{ value: "Asia/Singapore", label: "(GMT+8) Singapore" },
{ value: "Asia/Dubai", label: "(GMT+4) Dubai" },
{ value: "Australia/Sydney", label: "(GMT+11) Sydney" },
{ value: "Asia/Seoul", label: "(GMT+9) Seoul" }
]
}
] %}
{% call select(
content_attrs={"class": "w-72"},
listbox_attrs={"data-empty": "No timezone found."},
is_combobox=true
) %}
<div class="max-h-64 overflow-y-auto scrollbar">
{% for group in options_timezones %}
<div role="group" aria-labelledby="demo-combobox-timezones-group-{{ loop.index0 }}">
<span id="demo-combobox-timezones-group-{{ loop.index0 }}" role="heading">{{ group.label }}</span>
{% for timezone in group.timezones %}
<div role="option" data-value="{{ timezone.value}}">
{{ timezone.label }}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
<hr role="separator">
<div role="option">
{% lucide "circle-plus" %}
Create timezone
</div>
{% endcall %}
</div>
</section>
+150
View File
@@ -0,0 +1,150 @@
<section id="dropdown-menu" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Dropdown Menu</h2>
</header>
<div class="p-4">
<div class="flex flex-wrap items-start gap-4">
{% call popover(
id="dropdown-menu",
trigger="Open Menu",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "min-w-56"}
) %}
<nav role="menu">
<div role="group" aria-labelledby="account-options">
<span id="account-options" role="heading">Account Options</span>
<button type="button" role="menuitem">
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
<button type="button" role="menuitem">
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</button>
<button type="button" role="menuitem">
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</button>
<button type="button" role="menuitem">
Keyboard shortcuts
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘K</span>
</button>
</div>
<hr role="separator">
<button type="button" role="menuitem">GitHub</button>
<button type="button" role="menuitem">Support</button>
<button type="button" role="menuitem" disabled>API</button>
<hr role="separator">
<button type="button" role="menuitem">
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
</nav>
{% endcall %}
{% set profile_menu %}
<nav role="menu">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<img alt="@hunvreus" src="https://github.com/hunvreus.png" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">hunvreus</span>
<span class="text-muted-foreground truncate text-xs">hunvreus@example.com</span>
</div>
</div>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "star" %}
Upgrade to Pro
</button>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "user" %}
Account
</button>
<button type="button" role="menuitem">
{% lucide "credit-card" %}
Billing
</button>
<button type="button" role="menuitem">
{% lucide "bell" %}
Notifications
</button>
<hr role="separator">
<div role="group" aria-labelledby="theme-options">
<span id="theme-options" role="heading">Theme</span>
<button type="button" role="menuitem">
{% lucide "check", { "class": "text-foreground" } %}
Light
</button>
<button type="button" role="menuitem" >
{% lucide "check", { "class": "text-transparent" } %}
Dark
</button>
<button type="button" role="menuitem">
{% lucide "check", { "class": "text-transparent" } %}
System
</button>
</div>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "log-out" %}
Signout
</button>
</nav>
{% endset %}
{% set trigger %}
<img alt="@hunvreus" src="https://github.com/hunvreus.png" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">hunvreus</span>
<span class="text-muted-foreground truncate text-xs">hunvreus@example.com</span>
</div>
{% lucide "chevrons-up-down", { "class": "text-muted-foreground ml-auto" } %}
{% endset %}
{% call popover(
id="dropdown-checkboxes",
trigger=trigger,
trigger_attrs={"class": "btn-outline h-12 justify-start px-2 md:max-w-[200px]"}
) %}
{{ profile_menu | safe }}
{% endcall %}
{% set trigger %}
<img alt="@hunvreus" src="https://github.com/hunvreus.png" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
{% endset %}
{% call popover(
id="dropdown-checkboxes",
trigger=trigger,
trigger_attrs={"class": "btn-icon-ghost rounded-full size-8"}
) %}
{{ profile_menu | safe }}
{% endcall %}
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{% call popover(
id="dropdown-checkboxes",
trigger=trigger,
trigger_attrs={"class": "btn-icon-ghost"},
content_attrs={"class": "min-w-32"}
) %}
<nav role="menu">
<button type="button" role="menuitem">
{% lucide "pencil" %}
Edit
</button>
<button type="button" role="menuitem">
{% lucide "share" %}
Share
</button>
<hr role="separator">
<button type="button" role="menuitem" class="text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/20 focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive [&_svg]:!text-destructive">
{% lucide "trash-2" %}
Delete
</button>
</nav>
{% endcall %}
</div>
</div>
</section>
-144
View File
@@ -1,144 +0,0 @@
<section id="dropdown" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Dropdown</h2>
</header>
<div class="p-4">
<div class="flex flex-wrap items-start gap-4">
{% call dropdown(
id="dropdown-menu",
trigger="Open Menu",
trigger_attrs={"class": "btn-outline"},
menu_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
<span id="account-options" role="heading">Account Options</span>
<button type="button" role="menuitem">
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
<button type="button" role="menuitem">
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</button>
<button type="button" role="menuitem">
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</button>
<button type="button" role="menuitem">
Keyboard shortcuts
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘K</span>
</button>
</div>
<hr role="separator">
<button type="button" role="menuitem">GitHub</button>
<button type="button" role="menuitem">Support</button>
<button type="button" role="menuitem" disabled>API</button>
<hr role="separator">
<button type="button" role="menuitem">
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
{% endcall %}
{% set profile_menu %}
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<img src="https://avatar.vercel.sh/ronan_berder" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">hunvreus</span>
<span class="text-muted-foreground truncate text-xs">hunvreus@example.com</span>
</div>
</div>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "star" %}
Upgrade to Pro
</button>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "user" %}
Account
</button>
<button type="button" role="menuitem">
{% lucide "credit-card" %}
Billing
</button>
<button type="button" role="menuitem">
{% lucide "bell" %}
Notifications
</button>
<hr role="separator">
<div role="group" aria-labelledby="theme-options">
<span id="theme-options" role="heading">Theme</span>
<button type="button" role="menuitem">
{% lucide "check", { "class": "text-foreground" } %}
Light
</button>
<button type="button" role="menuitem" >
{% lucide "check", { "class": "text-transparent" } %}
Dark
</button>
<button type="button" role="menuitem">
{% lucide "check", { "class": "text-transparent" } %}
System
</button>
</div>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "log-out" %}
Signout
</button>
{% endset %}
{% set trigger %}
<img src="https://avatar.vercel.sh/ronan_berder" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">hunvreus</span>
<span class="text-muted-foreground truncate text-xs">hunvreus@example.com</span>
</div>
{% lucide "chevrons-up-down", { "class": "text-muted-foreground ml-auto" } %}
{% endset %}
{% call dropdown(
id="dropdown-checkboxes",
trigger=trigger,
trigger_attrs={"class": "btn-outline h-12 justify-start px-2 md:max-w-[200px]"}
) %}
{{ profile_menu | safe }}
{% endcall %}
{% set trigger %}
<img src="https://avatar.vercel.sh/ronan_berder" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
{% endset %}
{% call dropdown(
id="dropdown-checkboxes",
trigger=trigger,
trigger_attrs={"class": "btn-icon-ghost rounded-full size-8"}
) %}
{{ profile_menu | safe }}
{% endcall %}
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{% call dropdown(
id="dropdown-checkboxes",
trigger=trigger,
trigger_attrs={"class": "btn-icon-ghost"},
menu_attrs={"class": "min-w-32"}
) %}
<button type="button" role="menuitem">
{% lucide "pencil" %}
Edit
</button>
<button type="button" role="menuitem">
{% lucide "share" %}
Share
</button>
<hr role="separator">
<button type="button" role="menuitem" class="text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/20 focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive [&_svg]:!text-destructive">
{% lucide "trash-2" %}
Delete
</button>
{% endcall %}
</div>
</div>
</section>
+24 -22
View File
@@ -3,27 +3,29 @@
<h2 class="text-sm font-medium">Pagination</h2>
</header>
<div class="p-4">
<nav role="navigation" aria-label="pagination" class="mx-auto flex w-full justify-center">
<ul class="flex flex-row items-center gap-1">
<li>
<a href="#" class="btn-ghost">{% lucide "chevron-left" %} Previous</a>
</li>
<li>
<a href="#" class="btn-ghost size-9">1</a>
</li>
<li>
<a href="#" class="btn-outline size-9">2</a>
</li>
<li>
<a href="#" class="btn-ghost size-9">3</a>
</li>
<li>
<a href="#" class="btn-icon-ghost">{% lucide "ellipsis" %}</a>
</li>
<li>
<a href="#" class="btn-ghost">Next {% lucide "chevron-right" %}</a>
</li>
</ul>
</nav>
<div class="inline-flex">
<nav role="navigation" aria-label="pagination" class="mx-auto flex w-full justify-center">
<ul class="flex flex-row items-center gap-1">
<li>
<a href="#" class="btn-ghost">{% lucide "chevron-left" %} Previous</a>
</li>
<li>
<a href="#" class="btn-ghost size-9">1</a>
</li>
<li>
<a href="#" class="btn-outline size-9">2</a>
</li>
<li>
<a href="#" class="btn-ghost size-9">3</a>
</li>
<li>
<a href="#" class="btn-icon-ghost">{% lucide "ellipsis" %}</a>
</li>
<li>
<a href="#" class="btn-ghost">Next {% lucide "chevron-right" %}</a>
</li>
</ul>
</nav>
</div>
</div>
</section>
+38
View File
@@ -0,0 +1,38 @@
<section id="popover" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Popover</h2>
</header>
<div class="p-4">
{% call popover(
id="demo-popover",
trigger="Open popover",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "w-80 p-4"}
) %}
<div class="grid gap-4">
<header class="grid gap-1.5">
<h4 class="leading-none font-medium">Dimensions</h4>
<p class="text-muted-foreground text-sm">Set the dimensions for the layer.</p>
</header>
<form class="form grid gap-2">
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-width">Width</label>
<input type="text" id="demo-popover-width" value="100%" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-max-width">Max. width</label>
<input type="text" id="demo-popover-max-width" value="300px" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-height">Height</label>
<input type="text" id="demo-popover-height" value="25px" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-max-height">Max. height</label>
<input type="text" id="demo-popover-max-height" value="none" class="col-span-2 h-8"/>
</div>
</form>
</div>
{% endcall %}
</div>
</section>
+5 -5
View File
@@ -5,20 +5,20 @@
<div class="p-4">
<div class="flex flex-col gap-y-6">
<fieldset class="grid gap-3">
<label class="label"><input type="radio" name="radio-demo" value="yes" class="input">Yes</label>
<label class="label"><input type="radio" name="radio-demo" value="no" class="input">No</label>
<label class="label"><input type="radio" name="radio-demo" value="maybe" class="input">Maybe</label>
<label class="label"><input type="radio" name="demo-radio-group" value="yes" class="input">Yes</label>
<label class="label"><input type="radio" name="demo-radio-group" value="no" class="input">No</label>
<label class="label"><input type="radio" name="demo-radio-group" value="maybe" class="input">Maybe</label>
</fieldset>
<fieldset class="grid gap-3 max-w-sm">
<label class="label gap-3 items-start hover:bg-accent/50 rounded-lg border p-4 has-[input[type='radio']:checked]:border-green-600 has-[input[type='radio']:checked]:bg-green-50 dark:has-[input[type='radio']:checked]:border-green-900 dark:has-[input[type='radio']:checked]:bg-green-950">
<input type="radio" name="radio-demo" value="no" class="input checked:border-green-600">
<input type="radio" name="demo-radio-group-styled" value="no" class="input checked:bg-green-600 checked:border-green-600 dark:checked:bg-input/30 checked:before:bg-background dark:checked:before:bg-primary">
<div class="grid gap-1 font-normal">
<h2 class="font-medium">Starter Plan</h2>
<p class="text-muted-foreground leading-snug">Perfect for small businesses getting started with our platform</p>
</div>
</label>
<label class="label gap-3 items-start hover:bg-accent/50 rounded-lg border p-4 has-[input[type='radio']:checked]:border-green-600 has-[input[type='radio']:checked]:bg-green-50 dark:has-[input[type='radio']:checked]:border-green-900 dark:has-[input[type='radio']:checked]:bg-green-950">
<input type="radio" name="radio-demo" value="no" class="input checked:border-green-600">
<input type="radio" name="demo-radio-group-styled" value="no" class="input checked:bg-green-600 checked:border-green-600 dark:checked:bg-input/30 checked:before:bg-background dark:checked:before:bg-primary">
<div class="grid gap-1 font-normal">
<h2 class="font-medium">Pro Plan</h2>
<p class="text-muted-foreground leading-snug">Advanced features for growing businesses with higher demands</p>
+38 -13
View File
@@ -5,26 +5,51 @@
<div class="p-4">
<div class="flex flex-wrap items-center gap-4">
<select class="select">
<option>Orange</option>
<option>Apple</option>
<option>Strawberry</option>
<optgroup label="Colors">
<option>Fuchsia</option>
<option selected>Cyan</option>
<option>Amber</option>
</optgroup>
<optgroup label="Gradients">
<option>Fuchsia to Cyan</option>
<option>Cyan to Amber</option>
<option>Amber to Fuchsia</option>
</optgroup>
</select>
{% call select(
value="banana",
input_name="demo-food-select"
selected="cyan"
) %}
<div role="group" aria-labelledby="fruit-options-label">
<span id="fruit-options-label" role="heading">Fruits</span>
<button type="button" role="option" data-value="orange">🍊 Orange</button>
<button type="button" role="option" data-value="apple">🍎 Apple</button>
<button type="button" role="option" data-value="banana">🍌 Banana</button>
<span id="fruit-options-label" role="heading">Colors</span>
<div type="button" role="option" data-value="fuchsia">
<span class="rounded-full size-4 bg-fuchsia-500"></span>
Fuchsia
</div>
<div type="button" role="option" data-value="cyan">
<span class="rounded-full size-4 bg-cyan-500"></span>
Cyan
</div>
<div type="button" role="option" data-value="amber">
<span class="rounded-full size-4 bg-amber-500"></span>
Amber
</div>
</div>
<div role="group" aria-labelledby="vegetable-options-label">
<span id="vegetable-options-label" role="heading">Vegetables</span>
<button type="button" role="option" data-value="carrot">🥕 Carrot</button>
<button type="button" role="option" data-value="potato">🥔 Potato</button>
<button type="button" role="option" data-value="broccoli">🥦 Broccoli</button>
<span id="vegetable-options-label" role="heading">Gradients</span>
<div type="button" role="option" data-value="fuchsia-to-cyan">
<span class="rounded-full size-4 bg-linear-to-bl from-fuchsia-500 to-cyan-500"></span>
Amber to Cyan
</div>
<div type="button" role="option" data-value="cyan-to-amber">
<span class="rounded-full size-4 bg-linear-to-bl from-cyan-500 to-amber-500"></span>
Cyan to Amber
</div>
<div type="button" role="option" data-value="amber-to-fuchsia">
<span class="rounded-full size-4 bg-linear-to-bl from-amber-500 to-fuchsia-500"></span>
Amber to Fuchsia
</div>
</div>
{% endcall %}
</div>
+37
View File
@@ -0,0 +1,37 @@
<section id="skeleton" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Skeleton</h2>
</header>
<div class="p-4">
<div class="flex flex-col gap-4">
<div class="flex items-center gap-4">
<div class="bg-accent animate-pulse size-10 shrink-0 rounded-full"></div>
<div class="grid gap-2">
<div class="bg-accent animate-pulse rounded-md h-4 w-[150px]"></div>
<div class="bg-accent animate-pulse rounded-md h-4 w-[100px]"></div>
</div>
</div>
<div class="flex gap-4 w-full">
<div class="card w-full @md:w-auto @md:min-w-sm">
<header>
<div class="bg-accent animate-pulse rounded-md h-4 w-2/3"></div>
<div class="bg-accent animate-pulse rounded-md h-4 w-1/2"></div>
</header>
<section>
<div class="bg-accent animate-pulse rounded-md aspect-square w-full"></div>
</section>
</div>
<div class="card w-full @md:w-auto @md:min-w-sm">
<header>
<div class="bg-accent animate-pulse rounded-md h-4 w-2/3"></div>
<div class="bg-accent animate-pulse rounded-md h-4 w-1/2"></div>
</header>
<section>
<div class="bg-accent animate-pulse rounded-md aspect-square w-full"></div>
</section>
</div>
</div>
</div>
</section>
+20 -1
View File
@@ -3,6 +3,25 @@
<h2 class="text-sm font-medium">Slider</h2>
</header>
<div class="p-4">
<input type="range" class="input w-full" min="0" max="100" value="50">
<div class="max-w-sm">
<input
type="range"
class="input w-full"
min="0"
max="27"
value="12"
x-data="{
updatePercent(el) {
const min = parseFloat(el.min || 0);
const max = parseFloat(el.max || 100);
const value = parseFloat(el.value);
const percent = (max === min) ? 0 : ((value - min) / (max - min)) * 100;
el.style.setProperty('--slider-value', `${percent}%`);
}
}"
x-init="updatePercent($el)"
@input="updatePercent($el)"
>
</div>
</div>
</section>
+41 -89
View File
@@ -6,113 +6,65 @@
<div class="relative w-full overflow-x-auto">
<table class="table">
<caption>List of users</caption>
<caption>A list of your recent invoices.</caption>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">Role</th>
<th scope="col">Actions</th>
<th>Invoice</th>
<th>Status</th>
<th>Method</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class="font-medium flex items-center gap-x-2">
<img src="https://avatar.vercel.sh/sarah_chen" alt="Avatar" class="w-5 h-5 rounded-full">
Sarah Chen
</span>
</td>
<td>sarah.chen@example.com</td>
<td>Admin</td>
<td>
<a href="#" class="badge-outline">
View profile
{% lucide "arrow-up-right" %}
</a>
</td>
<td class="font-medium">INV001</td>
<td>Paid</td>
<td>Credit Card</td>
<td class="text-right">$250.00</td>
</tr>
<tr>
<td>
<span class="font-medium flex items-center gap-x-2">
<img src="https://avatar.vercel.sh/marcus_rodriguez" alt="Avatar" class="w-5 h-5 rounded-full">
Marcus Rodriguez
</span>
</td>
<td>m.rodriguez@example.com</td>
<td>Member</td>
<td>
<a href="#" class="badge-outline">
View profile
{% lucide "arrow-up-right" %}
</a>
</td>
<td class="font-medium">INV002</td>
<td>Pending</td>
<td>PayPal</td>
<td class="text-right">$150.00</td>
</tr>
<tr>
<td>
<span class="font-medium flex items-center gap-x-2">
<img src="https://avatar.vercel.sh/emma_thompson" alt="Avatar" class="w-5 h-5 rounded-full">
Emma Thompson
</span>
</td>
<td>emma.t@example.com</td>
<td>Member</td>
<td>
<a href="#" class="badge-outline">
View profile
{% lucide "arrow-up-right" %}
</a>
</td>
<td class="font-medium">INV003</td>
<td>Unpaid</td>
<td>Bank Transfer</td>
<td class="text-right">$350.00</td>
</tr>
<tr>
<td>
<span class="font-medium flex items-center gap-x-2">
<img src="https://avatar.vercel.sh/aiden_patel" alt="Avatar" class="w-5 h-5 rounded-full">
Aiden Patel
</span>
</td>
<td>a.patel@example.com</td>
<td>Admin</td>
<td>
<a href="#" class="badge-outline">
View profile
{% lucide "arrow-up-right" %}
</a>
</td>
<td class="font-medium">INV004</td>
<td>Paid</td>
<td>Paypal</td>
<td class="text-right">$450.00</td>
</tr>
<tr>
<td>
<span class="font-medium flex items-center gap-x-2">
<img src="https://avatar.vercel.sh/lisa_kim" alt="Avatar" class="w-5 h-5 rounded-full">
Lisa Kim
</span>
</td>
<td>l.kim@example.com</td>
<td>Guest</td>
<td>
<a href="#" class="badge-outline">
View profile
{% lucide "arrow-up-right" %}
</a>
</td>
<td class="font-medium">INV005</td>
<td>Paid</td>
<td>Credit Card</td>
<td class="text-right">$550.00</td>
</tr>
<tr>
<td>
<span class="font-medium flex items-center gap-x-2">
<img src="https://avatar.vercel.sh/james_wilson" alt="Avatar" class="w-5 h-5 rounded-full">
James Wilson
</span>
</td>
<td>jwilson@example.com</td>
<td>Member</td>
<td>
<a href="#" class="badge-outline">
View profile
{% lucide "arrow-up-right" %}
</a>
</td>
<td class="font-medium">INV006</td>
<td>Pending</td>
<td>Bank Transfer</td>
<td class="text-right">$200.00</td>
</tr>
<tr>
<td class="font-medium">INV007</td>
<td>Unpaid</td>
<td>Credit Card</td>
<td class="text-right">$300.00</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3">Total</td>
<td class="text-right">$2,500.00</td>
</tr>
</tfoot>
</table>
</div>
</div>
+167
View File
@@ -0,0 +1,167 @@
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('tabs', () => ({
activeTabIndex: 0,
tabs: [],
panels: [],
init() {
this.$nextTick(() => {
this.tabs = Array.from(this.$el.querySelectorAll('[role=tab]:not([disabled])'));
this.panels = Array.from(this.$el.querySelectorAll('[role=tabpanel]'));
if (this.tabs.length > 0) {
this.selectTab(this.tabs[0], false);
}
});
},
nextTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex + 1) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
},
prevTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex - 1 + this.tabs.length) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
},
selectTab(tab, focus = true) {
if (!tab || this.tabs.length === 0) return;
this.tabs.forEach((t, index) => {
const isSelected = t === tab;
t.setAttribute('aria-selected', isSelected);
t.setAttribute('tabindex', isSelected ? '0' : '-1');
if (isSelected) {
this.activeTabIndex = index;
this.activeTab = t;
if (focus) {
t.focus();
}
}
});
const panelId = tab.getAttribute('aria-controls');
if (!panelId) return;
this.panels.forEach(panel => {
panel.hidden = (panel.getAttribute('id') !== panelId);
});
},
$tablist: {
['@click'](event) {
const clickedTab = event.target.closest('[role=tab]');
if (clickedTab) {
this.selectTab(clickedTab);
}
},
['@keydown.arrow-right.prevent']() { this.nextTab() },
['@keydown.arrow-left.prevent']() { this.prevTab() },
},
}));
});
</script>
<section id="tabs" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Tabs</h2>
</header>
<div class="p-4">
<div class="flex flex-col gap-6">
<div class="tabs flex flex-col gap-2 max-w-[300px]" x-data="tabs">
<div role="tablist" aria-orientation="horizontal" aria-label="Feature Sections" class="w-full" x-bind="$tablist">
<button role="tab" id="demo-tab-account" aria-controls="demo-panel-account" aria-selected="true" tabindex="0">
Account
</button>
<button role="tab" id="demo-tab-password" aria-controls="demo-panel-password" aria-selected="false" tabindex="-1">
Password
</button>
</div>
<div role="tabpanel" id="demo-panel-account" aria-labelledby="demo-tab-account" tabindex="-1">
<div class="card">
<header>
<h2>Account</h2>
<p>Make changes to your account here. Click save when you're done.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-account-name">Name</label>
<input type="text" id="demo-tabs-account-name" value="Pedro Duarte" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-account-username">Username</label>
<input type="text" id="demo-tabs-account-username" value="@peduarte" />
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save changes</button>
</footer>
</div>
</div>
<div role="tabpanel" id="demo-panel-password" aria-labelledby="demo-tab-password" tabindex="-1" hidden>
<div class="card">
<header>
<h2>Password</h2>
<p>Change your password here. After saving, you'll be logged out.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-password-current">Current password</label>
<input type="password" id="demo-tabs-password-current" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-password-new">New password</label>
<input type="password" id="demo-tabs-password-new"/>
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save Password</button>
</footer>
</div>
</div>
</div>
<div class="tabs" x-data="tabs">
<div role="tablist" aria-orientation="horizontal" aria-label="Feature Sections" x-bind="$tablist">
<button role="tab" aria-selected="true" tabindex="0">
Home
</button>
<button role="tab" aria-selected="false" tabindex="-1">
Settings
</button>
</div>
</div>
<div class="tabs" x-data="tabs">
<div role="tablist" aria-orientation="horizontal" aria-label="Feature Sections" x-bind="$tablist">
<button role="tab" aria-selected="true" tabindex="0">
Home
</button>
<button role="tab" aria-selected="false" tabindex="-1" disabled>
Disabled
</button>
</div>
</div>
<div class="tabs" x-data="tabs">
<div role="tablist" aria-orientation="horizontal" aria-label="Feature Sections" x-bind="$tablist">
<button role="tab" aria-selected="true" tabindex="0">
{% lucide 'app-window' %}
Preview
</button>
<button role="tab" aria-selected="false" tabindex="-1">
{% lucide 'code' %}
Code
</button>
</div>
</div>
</div>
</div>
</section>
+71 -151
View File
@@ -9,11 +9,11 @@
</div>
{% lucide "chevrons-up-down", { "class": "ml-auto" } %}
{% endset %}
{% call dropdown(
id="dropdown-checkboxes",
{% call popover(
id="demo-sidebar-dropdown-org",
trigger=trigger,
trigger_attrs={"class": "btn-ghost p-2 h-12 w-full justify-start"},
menu_attrs={"data-side": "right"}
content_attrs={"data-side": "right"}
) %}
Something
{% endcall %}
@@ -26,7 +26,7 @@
</form>
</div>
</header>
<section class="flex flex-1 flex-col gap-2 overflow-y-auto">
<section class="flex flex-1 flex-col gap-2 overflow-y-auto scrollbar">
<nav aria-label="Pages navigation" class="flex flex-col p-2">
<h2 class="text-sidebar-foreground/70 flex h-8 items-center px-2 text-xs font-medium">Pages</h2>
<ul class="flex flex-col gap-1">
@@ -63,126 +63,44 @@
<nav aria-label="Components navigation" class="flex flex-col p-2">
<h2 class="text-sidebar-foreground/70 flex h-8 items-center px-2 text-xs font-medium">Components</h2>
<ul class="flex flex-col gap-1">
<li>
<a
href="#accordion"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Accordion</a>
</li>
<li>
<a
href="#alert"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Alert</a>
</li>
<li>
<a
href="#alert-dialog"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Alert Dialog</a>
</li>
<li>
<a
href="#badge"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Badge</a>
</li>
<li>
<a
href="#breadcrumb"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Breadcrumb</a>
</li>
<li>
<a
href="#button"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Button</a>
</li>
<li>
<a
href="#checkbox"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Checkbox</a>
</li>
<li>
<a
href="#dialog"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Dialog</a>
</li>
<li>
<a
href="#dropdown"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Dropdown</a>
</li>
<li>
<a
href="#form"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Form</a>
</li>
<li>
<a
href="#input"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Input</a>
</li>
<li>
<a
href="#label"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Label</a>
</li>
<li>
<a
href="#pagination"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Pagination</a>
</li>
<li>
<a
href="#radio-group"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Radio Group</a>
</li>
<li>
<a
href="#select"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Select</a>
</li>
<li>
<a
href="#slider"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Slider</a>
</li>
<li>
<a
href="#switch"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Switch</a>
</li>
<li>
<a
href="#table"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Table</a>
</li>
<li>
<a
href="#textarea"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Textarea</a>
</li>
<li>
<a
href="#tooltip"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>Tooltip</a>
</li>
{% set components = [
{ "id": "accordion", "name": "Accordion" },
{ "id": "alert", "name": "Alert" },
{ "id": "alert-dialog", "name": "Alert Dialog" },
{ "id": "avatar", "name": "Avatar" },
{ "id": "badge", "name": "Badge" },
{ "id": "breadcrumb", "name": "Breadcrumb" },
{ "id": "button", "name": "Button" },
{ "id": "card", "name": "Card" },
{ "id": "checkbox", "name": "Checkbox" },
{ "id": "combobox", "name": "Combobox" },
{ "id": "dialog", "name": "Dialog" },
{ "id": "dropdown-menu", "name": "Dropdown Menu" },
{ "id": "form", "name": "Form" },
{ "id": "input", "name": "Input" },
{ "id": "label", "name": "Label" },
{ "id": "pagination", "name": "Pagination" },
{ "id": "popover", "name": "Popover" },
{ "id": "radio-group", "name": "Radio Group" },
{ "id": "select", "name": "Select" },
{ "id": "skeleton", "name": "Skeleton" },
{ "id": "slider", "name": "Slider" },
{ "id": "switch", "name": "Switch" },
{ "id": "table", "name": "Table" },
{ "id": "tabs", "name": "Tabs" },
{ "id": "textarea", "name": "Textarea" },
{ "id": "tooltip", "name": "Tooltip" }
] %}
{% for component in components %}
<li>
<a
href="#{{ component.id }}"
class="h-8 flex items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0"
>
{{ component.name }}
</a>
</li>
{% endfor %}
</ul>
</nav>
</section>
@@ -191,49 +109,51 @@
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<img src="https://avatar.vercel.sh/ronan_berder" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">hunvreus</span>
<span class="truncate font-medium">hunvreus</span>
<span class="text-muted-foreground truncate text-xs">hunvreus@example.com</span>
</div>
</div>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "star" %}
Upgrade to Pro
</button>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "user" %}
Account
</button>
<button type="button" role="menuitem">
{% lucide "credit-card" %}
Billing
</button>
<button type="button" role="menuitem">
{% lucide "bell" %}
Notifications
</button>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "log-out" %}
Signout
</button>
<nav role="menu">
<button type="button" role="menuitem">
{% lucide "star" %}
Upgrade to Pro
</button>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "user" %}
Account
</button>
<button type="button" role="menuitem">
{% lucide "credit-card" %}
Billing
</button>
<button type="button" role="menuitem">
{% lucide "bell" %}
Notifications
</button>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "log-out" %}
Signout
</button>
</nav>
{% endset %}
{% set trigger %}
<img src="https://avatar.vercel.sh/ronan_berder" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">hunvreus</span>
<span class="truncate font-medium">hunvreus</span>
<span class="text-muted-foreground truncate text-xs">hunvreus@example.com</span>
</div>
{% lucide "chevrons-up-down", { "class": "text-muted-foreground ml-auto" } %}
{% endset %}
{% call dropdown(
id="dropdown-checkboxes",
{% call popover(
id="demo-sidebar-dropdown-account",
trigger=trigger,
trigger_attrs={"class": "btn-ghost h-12 justify-start p-2 w-full"},
menu_attrs={"class": "min-w-56", "data-side": "right", "data-align": "end"}
content_attrs={"class": "min-w-56", "data-side": "right", "data-align": "end"}
) %}
{{ profile_menu | safe }}
{% endcall %}
+13 -8
View File
@@ -48,14 +48,14 @@
</head>
<body>
{% from "./_components/dialog.njk" import dialog %}
{% from "./_components/dropdown.njk" import dropdown %}
{% from "./_components/popover.njk" import popover %}
{% from "./_components/select.njk" import select %}
<div
class="fixed inset-y-0 left-0 z-50 h-svh border-r bg-sidebar text-sidebar-foreground flex flex-col w-[var(--sidebar-width)] transition-transform ease-in-out peer aria-[hidden=true]:-translate-x-full"
aria-hidden="false"
:aria-hidden="open ? 'false' : 'true'"
@dialog:sidebar.window="toggle()"
@sidebar:toggle.window="toggle()"
x-data="{
open: true,
toggle() { this.open = !this.open }
@@ -79,13 +79,12 @@
x-data="{ sidebarOpen: true }"
@click="
sidebarOpen = !sidebarOpen;
$dispatch('dialog:sidebar', { action: 'toggle' });
$dispatch('sidebar:toggle');
"
>
<span x-show="sidebarOpen">{% lucide "panel-left-close" %}</span>
<span x-show="!sidebarOpen">{% lucide "panel-left-open" %}</span>
{% lucide "panel-left" %}
</button>
<select class="select h-8">
<select class="select h-8 leading-none">
<option value="light">Default</option>
<option value="light">Other</option>
</select>
@@ -109,21 +108,27 @@
{% include "./_includes/components/accordion.njk" %}
{% include "./_includes/components/alert.njk" %}
{% include "./_includes/components/alert-dialog.njk" %}
{% include "./_includes/components/avatar.njk" %}
{% include "./_includes/components/badge.njk" %}
{% include "./_includes/components/breadcrumb.njk" %}
{% include "./_includes/components/button.njk" %}
{% include "./_includes/components/card.njk" %}
{% include "./_includes/components/checkbox.njk" %}
{% include "./_includes/components/combobox.njk" %}
{% include "./_includes/components/dialog.njk" %}
{% include "./_includes/components/dropdown.njk" %}
{% include "./_includes/components/dropdown-menu.njk" %}
{% include "./_includes/components/form.njk" %}
{% include "./_includes/components/input.njk" %}
{% include "./_includes/components/label.njk" %}
{% include "./_includes/components/pagination.njk" %}
{% include "./_includes/components/popover.njk" %}
{% include "./_includes/components/radio-group.njk" %}
{% include "./_includes/components/select.njk" %}
{# {% include "./_includes/components/slider.njk" %} #}
{% include "./_includes/components/skeleton.njk" %}
{% include "./_includes/components/slider.njk" %}
{% include "./_includes/components/switch.njk" %}
{% include "./_includes/components/table.njk" %}
{% include "./_includes/components/tabs.njk" %}
{% include "./_includes/components/textarea.njk" %}
{% include "./_includes/components/tooltip.njk" %}
</div>