Cleaning up folders

This commit is contained in:
Ronan Berder
2025-04-22 16:09:56 +08:00
parent f740ac8f6d
commit 4034b2dcf1
45 changed files with 1122 additions and 1138 deletions
-1
View File
@@ -1,5 +1,4 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
-149
View File
@@ -1,149 +0,0 @@
{% 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
) %}
{% if registerComponent('dialog') %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('dialog', (initialOpen = false, initialCloseOnOverlayClick = true) => ({
id: null,
open: false,
closeOnOverlayClick: true,
init() {
this.id = this.$el.id;
this.open = initialOpen;
this.closeOnOverlayClick = initialCloseOnOverlayClick;
},
show() {
if (!this.open) {
this.open = true;
this.$nextTick(() => this.$el.dispatchEvent(new CustomEvent('dialog:opened', { bubbles: true })));
}
},
hide() {
if (this.open) {
this.open = false;
this.$el.dispatchEvent(new CustomEvent('dialog:closed', { bubbles: true }));
}
},
handleControl(event) {
const detail = event.detail;
if (detail && detail.targetId === this.id) {
const action = detail.action || 'toggle';
if (action === 'open') this.show();
else if (action === 'close') this.hide();
}
},
$main: {
'@dialog:control.window'(e) {
this.handleControl(e)
},
},
$trigger: {
'@click'() { this.show() },
':aria-expanded'() { return this.open }
},
$content: {
':inert'() { return !this.open },
'@click'(e) { e.target === this.$el && this.closeOnOverlayClick && this.hide() },
'@keydown.escape.window'() { this.open && this.hide() },
'x-cloak': ''
}
}));
});
</script>
{% endif %}
<div
id="{{ id }}"
x-data="dialog({{ 'true' if open else 'false' }}, {{ 'true' if close_on_overlay_click else 'false' }})"
x-bind="$main"
class="dialog"
{% for key, value in main_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if trigger %}
<button
{% if id %}aria-controls="{{ id }}-dialog"{% endif %}
aria-expanded="false"
aria-controls="{{ id }}-dialog"
x-bind="$trigger"
{% for key, value in trigger_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ trigger }}
</button>
{% endif %}
<div
role="dialog"
id="{{ id }}-dialog"
aria-modal="true"
aria-labelledby="{{ id }}-title"
inert
x-bind="$content"
>
<article
{% for key, value in content_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if title or description %}
<header
{% for key, value in content_header_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
<h2 id="{{ id }}-title">{{ title | safe }}</h2>
<p>{{ description | safe }}</p>
</header>
{% endif %}
{% if caller %}
<section
{% for key, value in content_body_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ caller() }}
</section>
{% endif %}
{% if footer %}
<footer
{% for key, value in content_footer_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ footer | safe }}
</footer>
{% endif %}
{% if close_button %}
<button
@click="hide()"
aria-label="Close dialog"
>
{% lucide "x" %}
</button>
{% endif %}
</article>
</div>
</div>
{% endmacro %}
-129
View File
@@ -1,129 +0,0 @@
{% 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 %}
-224
View File
@@ -1,224 +0,0 @@
{% macro select(
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', (name = null, initialValue = null) => ({
open: false,
name: null,
options: [],
disabledOptions: [],
focusedIndex: null,
selectedLabel: null,
selectedValue: null,
query: '',
init() {
this.$nextTick(() => {
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]);
}
});
},
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', '');
},
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': ''
},
$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="popover {{ main_attrs.class }}"
x-data="select('{{ name }}', '{{ selected }}')"
@click.away="open = false"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in trigger_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<button
type="button"
aria-haspopup="listbox"
aria-expanded="false"
x-bind="$trigger"
{% if id %}
id="{{ id }}-trigger"
aria-controls="{{ id }}-content"
{% endif %}
class="btn-outline justify-between font-normal {{ trigger_attrs.class }}"
{% for key, value in trigger_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<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
data-popover
aria-hidden="true"
x-bind="$content"
{% if id %}id="{{ id }}-content"{% endif %}
{% for key, value in content_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% 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 name %}
<input type="hidden" name="{{ name }}" x-model="selectedValue">
{% endif %}
</div>
{% endmacro %}
-120
View File
@@ -1,120 +0,0 @@
{% macro tabs(
tabs,
main_attrs=None,
trigger_attrs=None,
content_attrs=None
)
%}
{% if registerComponent('toast') %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('toast', () => ({
open: false,
focusedIndex: -1,
menuItems: [],
init() {
this.$nextTick(() => {
const menuElement = this.$el.querySelector('[role="menu"]');
this.menuItems = Array.from(menuElement.querySelectorAll('[role=menuitem]'));
});
},
$trigger: {
'@click'() { this.handleClick() },
'@keydown.down.prevent'() { this.open && this.nextItem() },
':aria-expanded'() { return this.open },
':aria-pressed'() { return this.open }
},
$menu: {
'@click'(e) { this.handleMenuItemClick(e) },
'@keydown.escape.window'() { this.handleEscape() },
'@keydown.down.prevent'() { this.nextItem() },
'@keydown.up.prevent'() { this.prevItem() },
'@keydown.home.prevent'() { this.focusItem(0) },
'@keydown.end.prevent'() { this.focusItem(this.menuItems.length - 1) },
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
focusItem(index) {
if (index >= 0 && index < this.menuItems.length) {
this.focusedIndex = index;
this.menuItems[index].focus();
}
},
nextItem() {
this.focusItem((this.focusedIndex + 1) % this.menuItems.length);
},
prevItem() {
this.focusItem(this.focusedIndex <= 0 ? this.menuItems.length - 1 : this.focusedIndex - 1);
},
handleClick() {
this.open = !this.open;
this.$nextTick(() => {
if(this.open && this.menuItems.length) this.focusItem(0);
});
},
handleEscape() {
if (this.open) this.open = false;
},
handleMenuItemClick(event) {
const menuItem = event.target.closest('[role=menuitem]');
if (menuItem && !menuItem.disabled) {
menuItem.blur();
this.open = false;
}
},
}));
});
</script>
{% endif %}
<div
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 %}
{{ 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"
aria-hidden="true"
x-bind="$menu"
{% if id %}id="{{ id }}-menu"{% endif %}
{% for key, value in menu_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ caller() }}
</nav>
</div>
{% endmacro %}
-134
View File
@@ -1,134 +0,0 @@
{% macro toaster(
id="toaster",
toasts=[],
main_attrs=None,
is_fragment=false
) %}
{% if not is_fragment and registerComponent('toast') %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('toaster', { isPaused: false });
Alpine.data('toast', (category = []) => ({
open: false,
timeoutDuration: null,
timeoutId: null,
init() {
this.timeoutDuration = category === 'error' ? 5000 : 3000;
this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
this.open = true;
this.$watch('$store.toaster.isPaused', (isPaused) => {
if (!this.open) return;
if (isPaused) {
this.pauseTimeout();
} else {
this.resumeTimeout();
}
});
},
pauseTimeout() {
clearTimeout(this.timeoutId);
this.timeoutId = null;
},
resumeTimeout(index) {
if (this.open && this.timeoutId === null) {
this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
}
},
close() {
this.pauseTimeout();
this.open = false;
this.$el.blur();
},
$toast: {
['@mouseenter']() { this.$store.toaster.isPaused = true },
['@mouseleave']() { this.$store.toaster.isPaused = false },
['@keydown.escape.window']() { this.close() },
},
}));
});
</script>
{% endif %}
{% set iconMapping = {
'error': 'circle-x',
'success': 'circle-check',
'info': 'info',
'warning': 'circle-alert'
} %}
<div
id="{{ id }}"
class="toaster"
{% for key, value in main_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
{% if is_fragment %}hx-swap-oob="beforeend"{% endif %}
>
{% for toast in toasts %}
<div
class="toast"
role="{{ 'alert' if toast.category == 'error' else 'status' }}"
aria-atomic="true"
aria-hidden="false"
:aria-hidden="!open"
{% if toast.category %}data-category="{{ toast.category }}"{% endif %}
x-data="toast('{{ toast.category }}')"
x-bind="$toast"
>
<div class="toast-content">
<div class="flex items-center justify-between gap-x-3 p-4">
{% if toast.category in ["error", "success", "info", "warning"] %}
{% lucide iconMapping[toast.category], class="size-4 shrink-0" %}
{% endif %}
<section class="flex-1 flex flex-col gap-0.5 items-start">
{% if toast.message is string %}
<h2 class="font-medium">{{ toast.message }}</h2>
{% elif toast.message is mapping %}
{% if toast.message.title %}
<h2 class="font-medium">{{ toast.message.title }}</h2>
{% endif %}
{% if toast.message.description %}
<p class="text-muted-foreground">{{ toast.message.description }}</p>
{% endif %}
{% endif %}
</section>
{% if toast.message.action or toast.message.cancel %}
<footer class="flex flex-col gap-1 self-start">
{% if toast.message.action %}
{% if toast.message.action.click %}
<button
type="button"
class="btn h-6 text-xs px-2.5 rounded-sm"
@click="{{ toast.message.action.click }}"
>{{ toast.message.action.label }}</button>
{% elif toast.message.action.url %}
<a
href="{{ toast.message.action.url }}"
class="btn h-6 text-xs px-2.5 rounded-sm"
>{{ toast.message.action.label }}</a>
{% endif %}
{% endif %}
{% if toast.message.cancel %}
{% if toast.message.cancel.click %}
<button
type="button"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
@click="{{ toast.message.cancel.click }}"
>{{ toast.message.cancel.label }}</button>
{% elif toast.message.cancel.url %}
<a
href="{{ toast.message.cancel.url }}"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
>{{ toast.message.cancel.label }}</a>
{% endif %}
{% endif %}
</footer>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endmacro %}
+147 -69
View File
@@ -1,71 +1,149 @@
<section id="dialog" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Dialog</h2>
</header>
<div class="p-4">
<div class="flex flex-wrap items-center gap-4">
{% set footer %}
<button class="btn-outline" @click="open = false">Cancel</button>
<button class="btn" @click="open = false">Save changes</button>
{% endset %}
{% call dialog(
id="demo-dialog-edit-profile",
title="Edit profile",
description="Make changes to your profile here. Click save when you're done.",
main_attrs={"x-ref": "myDialog"},
trigger="Edit Profile",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
footer=footer
) %}
<form class="form grid gap-4">
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-name">Name</label>
<input type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" />
</div>
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-username">Username</label>
<input type="text" value="@peduarte" id="demo-dialog-edit-profile-username" />
</div>
</form>
{% endcall %}
{% set footer %}
<button class="btn-outline" @click="open = false">Close</button>
{% endset %}
{% call dialog(
id="dialog-example",
title="Scrollable Content",
description="This is a dialog with scrollable content.",
main_attrs={"x-ref": "myDialog"},
trigger="Scrollable Content",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
content_body_attrs={"class": "overflow-y-auto scrollbar"},
footer=footer
) %}
<div class="space-y-4 text-sm">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
{% endcall %}
{% 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
) %}
{% if registerComponent('dialog') %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('dialog', (initialOpen = false, initialCloseOnOverlayClick = true) => ({
id: null,
open: false,
closeOnOverlayClick: true,
<button
x-init
@click="$dispatch('dialog:control', { targetId: 'dialog-example', action: 'open' })"
type="button"
class="btn-outline"
>
Trigger dialog:control
</button>
</div>
init() {
this.id = this.$el.id;
this.open = initialOpen;
this.closeOnOverlayClick = initialCloseOnOverlayClick;
},
show() {
if (!this.open) {
this.open = true;
this.$nextTick(() => this.$el.dispatchEvent(new CustomEvent('dialog:opened', { bubbles: true })));
}
},
hide() {
if (this.open) {
this.open = false;
this.$el.dispatchEvent(new CustomEvent('dialog:closed', { bubbles: true }));
}
},
handleControl(event) {
const detail = event.detail;
if (detail && detail.targetId === this.id) {
const action = detail.action || 'toggle';
if (action === 'open') this.show();
else if (action === 'close') this.hide();
}
},
$main: {
'@dialog:control.window'(e) {
this.handleControl(e)
},
},
$trigger: {
'@click'() { this.show() },
':aria-expanded'() { return this.open }
},
$content: {
':inert'() { return !this.open },
'@click'(e) { e.target === this.$el && this.closeOnOverlayClick && this.hide() },
'@keydown.escape.window'() { this.open && this.hide() },
'x-cloak': ''
}
}));
});
</script>
{% endif %}
<div
id="{{ id }}"
x-data="dialog({{ 'true' if open else 'false' }}, {{ 'true' if close_on_overlay_click else 'false' }})"
x-bind="$main"
class="dialog"
{% for key, value in main_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if trigger %}
<button
{% if id %}aria-controls="{{ id }}-dialog"{% endif %}
aria-expanded="false"
aria-controls="{{ id }}-dialog"
x-bind="$trigger"
{% for key, value in trigger_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ trigger }}
</button>
{% endif %}
<div
role="dialog"
id="{{ id }}-dialog"
aria-modal="true"
aria-labelledby="{{ id }}-title"
inert
x-bind="$content"
>
<article
{% for key, value in content_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if title or description %}
<header
{% for key, value in content_header_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
<h2 id="{{ id }}-title">{{ title | safe }}</h2>
<p>{{ description | safe }}</p>
</header>
{% endif %}
{% if caller %}
<section
{% for key, value in content_body_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ caller() }}
</section>
{% endif %}
{% if footer %}
<footer
{% for key, value in content_footer_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ footer | safe }}
</footer>
{% endif %}
{% if close_button %}
<button
@click="hide()"
aria-label="Close dialog"
>
{% lucide "x" %}
</button>
{% endif %}
</article>
</div>
</section>
</div>
{% endmacro %}
+128 -37
View File
@@ -1,38 +1,129 @@
<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 %}
{% 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>
</section>
</div>
{% endmacro %}
+222 -54
View File
@@ -1,56 +1,224 @@
<section id="select" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Select</h2>
</header>
<div class="p-4">
<div class="flex flex-wrap items-center gap-4">
<select class="select">
<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(
selected="cyan"
) %}
{% macro select(
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', (name = null, initialValue = null) => ({
open: false,
name: null,
options: [],
disabledOptions: [],
focusedIndex: null,
selectedLabel: null,
selectedValue: null,
query: '',
<div role="group" aria-labelledby="fruit-options-label">
<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">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 %}
init() {
this.$nextTick(() => {
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]);
}
});
},
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', '');
},
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': ''
},
$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="popover {{ main_attrs.class }}"
x-data="select('{{ name }}', '{{ selected }}')"
@click.away="open = false"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in trigger_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<button
type="button"
aria-haspopup="listbox"
aria-expanded="false"
x-bind="$trigger"
{% if id %}
id="{{ id }}-trigger"
aria-controls="{{ id }}-content"
{% endif %}
class="btn-outline justify-between font-normal {{ trigger_attrs.class }}"
{% for key, value in trigger_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<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
data-popover
aria-hidden="true"
x-bind="$content"
{% if id %}id="{{ id }}-content"{% endif %}
{% for key, value in content_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% 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>
</section>
{% if name %}
<input type="hidden" name="{{ name }}" x-model="selectedValue">
{% endif %}
</div>
{% endmacro %}
+103 -150
View File
@@ -1,167 +1,120 @@
{% macro tabs(
tabs,
main_attrs=None,
trigger_attrs=None,
content_attrs=None
)
%}
{% if registerComponent('toast') %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('tabs', () => ({
activeTabIndex: 0,
tabs: [],
panels: [],
Alpine.data('toast', () => ({
open: false,
focusedIndex: -1,
menuItems: [],
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);
}
const menuElement = this.$el.querySelector('[role="menu"]');
this.menuItems = Array.from(menuElement.querySelectorAll('[role=menuitem]'));
});
},
nextTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex + 1) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
$trigger: {
'@click'() { this.handleClick() },
'@keydown.down.prevent'() { this.open && this.nextItem() },
':aria-expanded'() { return this.open },
':aria-pressed'() { return this.open }
},
prevTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex - 1 + this.tabs.length) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
$menu: {
'@click'(e) { this.handleMenuItemClick(e) },
'@keydown.escape.window'() { this.handleEscape() },
'@keydown.down.prevent'() { this.nextItem() },
'@keydown.up.prevent'() { this.prevItem() },
'@keydown.home.prevent'() { this.focusItem(0) },
'@keydown.end.prevent'() { this.focusItem(this.menuItems.length - 1) },
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
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);
focusItem(index) {
if (index >= 0 && index < this.menuItems.length) {
this.focusedIndex = index;
this.menuItems[index].focus();
}
},
nextItem() {
this.focusItem((this.focusedIndex + 1) % this.menuItems.length);
},
prevItem() {
this.focusItem(this.focusedIndex <= 0 ? this.menuItems.length - 1 : this.focusedIndex - 1);
},
handleClick() {
this.open = !this.open;
this.$nextTick(() => {
if(this.open && this.menuItems.length) this.focusItem(0);
});
},
$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() },
handleEscape() {
if (this.open) this.open = false;
},
handleMenuItemClick(event) {
const menuItem = event.target.closest('[role=menuitem]');
if (menuItem && !menuItem.disabled) {
menuItem.blur();
this.open = false;
}
},
}));
});
</script>
{% endif %}
<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
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 %}
{{ 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"
aria-hidden="true"
x-bind="$menu"
{% if id %}id="{{ id }}-menu"{% endif %}
{% for key, value in menu_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ caller() }}
</nav>
</div>
{% endmacro %}
<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>
+132 -21
View File
@@ -1,23 +1,134 @@
<section id="toast" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Toast</h2>
</header>
<div class="p-4">
<div class="flex flex-wrap items-center gap-2">
<div class="flex flex-wrap items-center gap-2">
<button class="btn-outline" hx-trigger="click" hx-get="/partials/toast/success" hx-target="#toast-htmx">
Success
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/partials/toast/error" hx-target="#toast-htmx">
Error
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/partials/toast/info" hx-target="#toast-htmx">
Info
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/partials/toast/warning" hx-target="#toast-htmx">
Warning
</button>
<div id="toast-htmx"></div>
{% macro toaster(
id="toaster",
toasts=[],
main_attrs=None,
is_fragment=false
) %}
{% if not is_fragment and registerComponent('toast') %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('toaster', { isPaused: false });
Alpine.data('toast', (category = []) => ({
open: false,
timeoutDuration: null,
timeoutId: null,
init() {
this.timeoutDuration = category === 'error' ? 5000 : 3000;
this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
this.open = true;
this.$watch('$store.toaster.isPaused', (isPaused) => {
if (!this.open) return;
if (isPaused) {
this.pauseTimeout();
} else {
this.resumeTimeout();
}
});
},
pauseTimeout() {
clearTimeout(this.timeoutId);
this.timeoutId = null;
},
resumeTimeout(index) {
if (this.open && this.timeoutId === null) {
this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
}
},
close() {
this.pauseTimeout();
this.open = false;
this.$el.blur();
},
$toast: {
['@mouseenter']() { this.$store.toaster.isPaused = true },
['@mouseleave']() { this.$store.toaster.isPaused = false },
['@keydown.escape.window']() { this.close() },
},
}));
});
</script>
{% endif %}
{% set iconMapping = {
'error': 'circle-x',
'success': 'circle-check',
'info': 'info',
'warning': 'circle-alert'
} %}
<div
id="{{ id }}"
class="toaster"
{% for key, value in main_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
{% if is_fragment %}hx-swap-oob="beforeend"{% endif %}
>
{% for toast in toasts %}
<div
class="toast"
role="{{ 'alert' if toast.category == 'error' else 'status' }}"
aria-atomic="true"
aria-hidden="false"
:aria-hidden="!open"
{% if toast.category %}data-category="{{ toast.category }}"{% endif %}
x-data="toast('{{ toast.category }}')"
x-bind="$toast"
>
<div class="toast-content">
<div class="flex items-center justify-between gap-x-3 p-4">
{% if toast.category in ["error", "success", "info", "warning"] %}
{% lucide iconMapping[toast.category], class="size-4 shrink-0" %}
{% endif %}
<section class="flex-1 flex flex-col gap-0.5 items-start">
{% if toast.message is string %}
<h2 class="font-medium">{{ toast.message }}</h2>
{% elif toast.message is mapping %}
{% if toast.message.title %}
<h2 class="font-medium">{{ toast.message.title }}</h2>
{% endif %}
{% if toast.message.description %}
<p class="text-muted-foreground">{{ toast.message.description }}</p>
{% endif %}
{% endif %}
</section>
{% if toast.message.action or toast.message.cancel %}
<footer class="flex flex-col gap-1 self-start">
{% if toast.message.action %}
{% if toast.message.action.click %}
<button
type="button"
class="btn h-6 text-xs px-2.5 rounded-sm"
@click="{{ toast.message.action.click }}"
>{{ toast.message.action.label }}</button>
{% elif toast.message.action.url %}
<a
href="{{ toast.message.action.url }}"
class="btn h-6 text-xs px-2.5 rounded-sm"
>{{ toast.message.action.label }}</a>
{% endif %}
{% endif %}
{% if toast.message.cancel %}
{% if toast.message.cancel.click %}
<button
type="button"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
@click="{{ toast.message.cancel.click }}"
>{{ toast.message.cancel.label }}</button>
{% elif toast.message.cancel.url %}
<a
href="{{ toast.message.cancel.url }}"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
>{{ toast.message.cancel.label }}</a>
{% endif %}
{% endif %}
</footer>
{% endif %}
</div>
</div>
</div>
</section>
{% endfor %}
</div>
{% endmacro %}
+71
View File
@@ -0,0 +1,71 @@
<section id="dialog" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Dialog</h2>
</header>
<div class="p-4">
<div class="flex flex-wrap items-center gap-4">
{% set footer %}
<button class="btn-outline" @click="open = false">Cancel</button>
<button class="btn" @click="open = false">Save changes</button>
{% endset %}
{% call dialog(
id="demo-dialog-edit-profile",
title="Edit profile",
description="Make changes to your profile here. Click save when you're done.",
main_attrs={"x-ref": "myDialog"},
trigger="Edit Profile",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
footer=footer
) %}
<form class="form grid gap-4">
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-name">Name</label>
<input type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" />
</div>
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-username">Username</label>
<input type="text" value="@peduarte" id="demo-dialog-edit-profile-username" />
</div>
</form>
{% endcall %}
{% set footer %}
<button class="btn-outline" @click="open = false">Close</button>
{% endset %}
{% call dialog(
id="dialog-example",
title="Scrollable Content",
description="This is a dialog with scrollable content.",
main_attrs={"x-ref": "myDialog"},
trigger="Scrollable Content",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
content_body_attrs={"class": "overflow-y-auto scrollbar"},
footer=footer
) %}
<div class="space-y-4 text-sm">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
{% endcall %}
<button
x-init
@click="$dispatch('dialog:control', { targetId: 'dialog-example', action: 'open' })"
type="button"
class="btn-outline"
>
Trigger dialog:control
</button>
</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>
+56
View File
@@ -0,0 +1,56 @@
<section id="select" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Select</h2>
</header>
<div class="p-4">
<div class="flex flex-wrap items-center gap-4">
<select class="select">
<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(
selected="cyan"
) %}
<div role="group" aria-labelledby="fruit-options-label">
<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">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>
</section>
+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>
+23
View File
@@ -0,0 +1,23 @@
<section id="toast" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Toast</h2>
</header>
<div class="p-4">
<div class="flex flex-wrap items-center gap-2">
<div class="flex flex-wrap items-center gap-2">
<button class="btn-outline" hx-trigger="click" hx-get="/partials/toast/success" hx-target="#toast-htmx">
Success
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/partials/toast/error" hx-target="#toast-htmx">
Error
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/partials/toast/info" hx-target="#toast-htmx">
Info
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/partials/toast/warning" hx-target="#toast-htmx">
Warning
</button>
<div id="toast-htmx"></div>
</div>
</div>
</section>
+31 -31
View File
@@ -48,10 +48,10 @@
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
</head>
<body>
{% from "./_components/dialog.njk" import dialog %}
{% from "./_components/popover.njk" import popover %}
{% from "./_components/select.njk" import select %}
{% from "./_components/toast.njk" import toaster %}
{% from "./_includes/components/dialog.njk" import dialog %}
{% from "./_includes/components/popover.njk" import popover %}
{% from "./_includes/components/select.njk" import select %}
{% from "./_includes/components/toast.njk" import toaster %}
<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"
@@ -107,33 +107,33 @@
</header>
<div class="grid gap-4 p-4">
{% 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-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/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/toast.njk" %}
{% include "./_includes/components/tooltip.njk" %}
{% include "./_includes/partials/accordion.njk" %}
{% include "./_includes/partials/alert.njk" %}
{% include "./_includes/partials/alert-dialog.njk" %}
{% include "./_includes/partials/avatar.njk" %}
{% include "./_includes/partials/badge.njk" %}
{% include "./_includes/partials/breadcrumb.njk" %}
{% include "./_includes/partials/button.njk" %}
{% include "./_includes/partials/card.njk" %}
{% include "./_includes/partials/checkbox.njk" %}
{% include "./_includes/partials/combobox.njk" %}
{% include "./_includes/partials/dialog.njk" %}
{% include "./_includes/partials/dropdown-menu.njk" %}
{% include "./_includes/partials/form.njk" %}
{% include "./_includes/partials/input.njk" %}
{% include "./_includes/partials/label.njk" %}
{% include "./_includes/partials/pagination.njk" %}
{% include "./_includes/partials/popover.njk" %}
{% include "./_includes/partials/radio-group.njk" %}
{% include "./_includes/partials/select.njk" %}
{% include "./_includes/partials/skeleton.njk" %}
{% include "./_includes/partials/slider.njk" %}
{% include "./_includes/partials/switch.njk" %}
{% include "./_includes/partials/table.njk" %}
{% include "./_includes/partials/tabs.njk" %}
{% include "./_includes/partials/textarea.njk" %}
{% include "./_includes/partials/toast.njk" %}
{% include "./_includes/partials/tooltip.njk" %}
</div>
</main>
{{ toaster() }}
+1 -1
View File
@@ -1,4 +1,4 @@
{% from "../../_components/toast.njk" import toaster %}
{% from "../../_includes/components/toast.njk" import toaster %}
{% set toasts = [{
category: "error",
message: {
+1 -1
View File
@@ -1,4 +1,4 @@
{% from "../../_components/toast.njk" import toaster %}
{% from "../../_includes/components/toast.njk" import toaster %}
{% set toasts = [{
category: "info",
message: {
+1 -1
View File
@@ -1,4 +1,4 @@
{% from "../../_components/toast.njk" import toaster %}
{% from "../../_includes/components/toast.njk" import toaster %}
{% set toasts = [{
message: {
title: "Event has been created",
+1 -1
View File
@@ -1,4 +1,4 @@
{% from "../../_components/toast.njk" import toaster %}
{% from "../../_includes/components/toast.njk" import toaster %}
{% set toasts = [{
category: "warning",
message: {
-12
View File
@@ -4,9 +4,6 @@
"requires": true,
"packages": {
"": {
"dependencies": {
"tw-animate-css": "^1.2.5"
},
"devDependencies": {
"@11ty/eleventy": "^3.0.0",
"@grimlink/eleventy-plugin-lucide-icons": "^2.1.4",
@@ -3372,15 +3369,6 @@
"dev": true,
"license": "0BSD"
},
"node_modules/tw-animate-css": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.5.tgz",
"integrity": "sha512-ABzjfgVo+fDbhRREGL4KQZUqqdPgvc5zVrLyeW9/6mVqvaDepXc7EvedA+pYmMnIOsUAQMwcWzNvom26J2qYvQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Wombosvideo"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
-3
View File
@@ -13,8 +13,5 @@
"@tailwindcss/cli": "^4.1.3",
"concurrently": "^9.1.2",
"tailwindcss": "^4.1.3"
},
"dependencies": {
"tw-animate-css": "^1.2.5"
}
}