mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-12 13:18:22 +00:00
Cleaning up folders
This commit is contained in:
Vendored
-1
@@ -1,5 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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,4 +1,4 @@
|
||||
{% from "../../_components/toast.njk" import toaster %}
|
||||
{% from "../../_includes/components/toast.njk" import toaster %}
|
||||
{% set toasts = [{
|
||||
category: "error",
|
||||
message: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% from "../../_components/toast.njk" import toaster %}
|
||||
{% from "../../_includes/components/toast.njk" import toaster %}
|
||||
{% set toasts = [{
|
||||
category: "info",
|
||||
message: {
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
{% from "../../_components/toast.njk" import toaster %}
|
||||
{% from "../../_includes/components/toast.njk" import toaster %}
|
||||
{% set toasts = [{
|
||||
category: "warning",
|
||||
message: {
|
||||
|
||||
Generated
-12
@@ -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",
|
||||
|
||||
@@ -13,8 +13,5 @@
|
||||
"@tailwindcss/cli": "^4.1.3",
|
||||
"concurrently": "^9.1.2",
|
||||
"tailwindcss": "^4.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tw-animate-css": "^1.2.5"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user