Working on docs and polishing macros

This commit is contained in:
Ronan Berder
2025-04-27 23:19:52 +08:00
parent a82964545c
commit dee28f1ee5
33 changed files with 765 additions and 561 deletions
@@ -0,0 +1,55 @@
<script>
if (!window._dialogComponentLoaded) {
window._dialogComponentLoaded = true;
const registerDialogComponent = () => {
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 }));
setTimeout(() => this.$refs.dialog.focus(), 50);
});
}
},
hide() {
if (this.open) {
this.open = false;
this.$el.dispatchEvent(new CustomEvent('dialog:closed', { bubbles: true }));
}
},
$main: {
'@dialog:open.window'(e) {
if (event.detail && event.detail.id === this.id) this.show();
},
},
$trigger: {
'@click'() { this.show() },
':aria-expanded'() { return this.open }
},
$content: {
'@keydown.escape'() { this.open && this.hide() },
':inert'() { return !this.open },
'@click'(e) { e.target === this.$el && this.closeOnOverlayClick && this.hide() },
'x-cloak': ''
}
}));
}
{% for event in (register_on or ["alpine:init"]) %}
document.addEventListener('{{ event }}', registerDialogComponent);
{% endfor %}
}
</script>
+24 -63
View File
@@ -1,3 +1,24 @@
{#
Renders a dialog component with optional trigger, header, body, and footer.
@param id {string} - Unique identifier for the dialog component.
@param trigger {string} [optional] - Text or HTML for the button that triggers the dialog.
@param title {string} [optional] - Title text displayed in the dialog header.
@param description {string} [optional] - Description text displayed below the title.
@param footer {string} [optional] - HTML content for the dialog footer.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
@param content_attrs {object} [optional] - Additional HTML attributes for the dialog content article.
@param content_header_attrs {object} [optional] - Additional HTML attributes for the dialog header.
@param content_body_attrs {object} [optional] - Additional HTML attributes for the dialog body section.
@param content_footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer.
@param open {boolean} [optional] [default=false] - Whether the dialog should be open initially.
@param close_button {boolean} [optional] [default=true] - Whether to include a close button.
@param close_on_overlay_click {boolean} [optional] [default=true] - Whether clicking the overlay closes the dialog.
@param include_html {boolean} [optional] [default=true] - Whether to render the HTML structure.
@param include_script {boolean} [optional] [default=true] - Whether to include the Alpine.js component script.
@param register_on {array} [optional] [default=["alpine:init"]] - Alpine.js events to register the component on.
#}
{% macro dialog(
id,
trigger=None,
@@ -14,72 +35,11 @@
close_button=true,
close_on_overlay_click=true,
include_html=true,
include_script=true,
include_script=false,
register_on=["alpine:init"]
) %}
{% if include_script %}
<script>
if (!window._dialogComponentLoaded) {
window._dialogComponentLoaded = true;
const registerDialogComponent = () => {
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 }));
setTimeout(() => this.$refs.dialog.focus(), 50);
});
}
},
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: {
'@keydown.escape'() { this.open && this.hide() },
':inert'() { return !this.open },
'@click'(e) { e.target === this.$el && this.closeOnOverlayClick && this.hide() },
'x-cloak': ''
}
}));
}
{% for event in register_on %}
document.addEventListener('{{ event }}', registerDialogComponent);
{% endfor %}
}
</script>
{% include "components/dialog-script.njk" %}
{% endif %}
{% if include_html %}
@@ -94,6 +54,7 @@
>
{% if trigger %}
<button
type="button"
{% if id %}aria-controls="{{ id }}-dialog"{% endif %}
aria-expanded="false"
aria-controls="{{ id }}-dialog"
@@ -0,0 +1,98 @@
<script>
if (!window._dropdownMenuComponentLoaded) {
window._dropdownMenuComponentLoaded = true;
const registerDropdownMenuComponent = () => {
Alpine.data('dropdownMenu', () => ({
open: false,
focusedIndex: null,
menuItems: [],
init() {
this.$nextTick(() => {
this.menuItems = Array.from(this.$el.querySelectorAll('[role=menuitem]:not([disabled])'));
});
},
focusMenuitem() {
if (this.menuItems.length === 0) return;
if (this.focusedIndex >= this.menuItems.length) {
this.focusedIndex = this.menuItems.length - 1;
} else if (this.focusedIndex < 0 || this.focusedIndex === null) {
this.focusedIndex = 0;
}
this.menuItems.forEach(item => item.blur());
this.menuItems[this.focusedIndex].focus();
},
moveMenuitemFocus(delta) {
if (this.menuItems.length === 0) return;
let wasOpen = this.open;
if (!this.open) {
this.open = true;
this.focusedIndex = 0;
} else {
this.focusedIndex = this.focusedIndex === null
? 0
: this.focusedIndex + delta;
}
if (wasOpen) {
this.focusMenuitem();
} else {
setTimeout(() => this.focusMenuitem(), 50);
}
},
handleMenuitemClick(event) {
const menuitem = event.target.closest('[role=menuitem]');
if (menuitem && menuitem.getAttribute('aria-disabled') !== 'true' && !menuitem.disabled) {
this.$nextTick(() => {
this.$refs.trigger.focus();
this.open = false;
});
}
},
handleMenuitemMousemove(event) {
const menuitem = event.target.closest('[role=menuitem]');
if (menuitem && menuitem.getAttribute('aria-disabled') !== 'true' && !menuitem.disabled) {
this.focusedIndex = this.menuItems.indexOf(menuitem);
this.focusMenuitem();
}
},
$trigger: {
'@click'() { this.open = !this.open },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveMenuitemFocus(+1); },
'@keydown.up.prevent'() { this.moveMenuitemFocus(-1); },
'@keydown.home.prevent'() { this.focusMenuitem(0) },
'@keydown.end.prevent'() { this.focusMenuitem(this.menuItems.length - 1) },
'@keydown.enter.prevent'() { this.open = !this.open },
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@click'(e) { this.handleMenuitemClick(e) },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveMenuitemFocus(+1); },
'@keydown.up.prevent'() { this.moveMenuitemFocus(-1); },
'@keydown.home.prevent'() { this.focusMenuitem(0) },
'@keydown.end.prevent'() { this.focusMenuitem(this.menuItems.length - 1) },
'@mousemove'(e) { this.handleMenuitemMousemove(e) },
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
}));
}
{% for event in (register_on or ["alpine:init"]) %}
document.addEventListener('{{ event }}', registerDropdownMenuComponent);
{% endfor %}
}
</script>
+5 -102
View File
@@ -1,112 +1,15 @@
{% macro dropdown_menu(
id=None,
trigger=None,
main_attrs=None,
trigger_attrs=None,
content_attrs=None,
main_attrs={},
trigger_attrs={},
content_attrs={},
include_html=true,
include_script=true,
include_script=false,
register_on=["alpine:init"]
) %}
{% if include_script %}
<script>
if (!window._dropdownMenuComponentLoaded) {
window._dropdownMenuComponentLoaded = true;
const registerDropdownMenuComponent = () => {
Alpine.data('dropdownMenu', () => ({
open: false,
focusedIndex: null,
menuItems: [],
init() {
this.$nextTick(() => {
this.menuItems = Array.from(this.$el.querySelectorAll('[role=menuitem]:not([disabled])'));
});
},
focusMenuitem() {
if (this.menuItems.length === 0) return;
if (this.focusedIndex >= this.menuItems.length) {
this.focusedIndex = this.menuItems.length - 1;
} else if (this.focusedIndex < 0 || this.focusedIndex === null) {
this.focusedIndex = 0;
}
this.menuItems.forEach(item => item.blur());
this.menuItems[this.focusedIndex].focus();
},
moveMenuitemFocus(delta) {
if (this.menuItems.length === 0) return;
let wasOpen = this.open;
if (!this.open) {
this.open = true;
this.focusedIndex = 0;
} else {
this.focusedIndex = this.focusedIndex === null
? 0
: this.focusedIndex + delta;
}
if (wasOpen) {
this.focusMenuitem();
} else {
setTimeout(() => this.focusMenuitem(), 50);
}
},
handleMenuitemClick(event) {
const menuitem = event.target.closest('[role=menuitem]');
if (menuitem && menuitem.getAttribute('aria-disabled') !== 'true' && !menuitem.disabled) {
this.$nextTick(() => {
this.$refs.trigger.focus();
this.open = false;
});
}
},
handleMenuitemMousemove(event) {
const menuitem = event.target.closest('[role=menuitem]');
if (menuitem && menuitem.getAttribute('aria-disabled') !== 'true' && !menuitem.disabled) {
this.focusedIndex = this.menuItems.indexOf(menuitem);
this.focusMenuitem();
}
},
$trigger: {
'@click'() { this.open = !this.open },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveMenuitemFocus(+1); },
'@keydown.up.prevent'() { this.moveMenuitemFocus(-1); },
'@keydown.home.prevent'() { this.focusMenuitem(0) },
'@keydown.end.prevent'() { this.focusMenuitem(this.menuItems.length - 1) },
'@keydown.enter.prevent'() { this.open = !this.open },
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@click'(e) { this.handleMenuitemClick(e) },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveMenuitemFocus(+1); },
'@keydown.up.prevent'() { this.moveMenuitemFocus(-1); },
'@keydown.home.prevent'() { this.focusMenuitem(0) },
'@keydown.end.prevent'() { this.focusMenuitem(this.menuItems.length - 1) },
'@mousemove'(e) { this.handleMenuitemMousemove(e) },
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
}));
}
{% for event in register_on %}
document.addEventListener('{{ event }}', registerDropdownMenuComponent);
{% endfor %}
}
</script>
{% include "components/dropdown-menu-script.njk" %}
{% endif %}
{% if include_html %}
@@ -0,0 +1,33 @@
<script>
if (!window._popoverComponentLoaded) {
window._popoverComponentLoaded = true;
const registerPopoverComponent = () => {
Alpine.data('popover', () => ({
open: false,
$trigger: {
'@click'() { this.open = !this.open },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
}));
}
{% for event in (register_on or ["alpine:init"]) %}
document.addEventListener('{{ event }}', registerPopoverComponent);
{% endfor %}
}
</script>
+2 -34
View File
@@ -5,43 +5,11 @@
trigger_attrs=None,
content_attrs=None,
include_html=true,
include_script=true,
include_script=false,
register_on=["alpine:init"]
) %}
{% if include_script %}
<script>
if (!window._popoverComponentLoaded) {
window._popoverComponentLoaded = true;
const registerPopoverComponent = () => {
Alpine.data('popover', () => ({
open: false,
$trigger: {
'@click'() { this.open = !this.open },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
}));
}
{% for event in register_on %}
document.addEventListener('{{ event }}', registerPopoverComponent);
{% endfor %}
}
</script>
{% include "components/popover-script.njk" %}
{% endif %}
{% if include_html %}
+158
View File
@@ -0,0 +1,158 @@
<script>
if (!window._selectComponentLoaded) {
window._selectComponentLoaded = true;
const registerSelectComponent = () => {
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, false);
this.focusedIndex = this.options.indexOf(option);
this.focusOption();
} else {
this.selectOption(this.options[0], false);
}
});
},
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'));
const option = this.options[this.focusedIndex];
option.setAttribute('data-focus', '');
option.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
},
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) {
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, dispatch = true) {
if (this.options.length === 0 || !option || option.disabled || option.getAttribute('data-value') == null) {
return;
}
this.options.forEach(opt => {
opt.setAttribute('aria-selected', opt === option);
});
this.selectedLabel = option.innerHTML;
this.selectedValue = option.getAttribute('data-value');
if (dispatch) {
this.$dispatch('select:change', {
value: this.selectedValue,
label: this.selectedLabel
});
this.$dispatch('change');
}
},
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.prevent'() {
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'() {
if (this.open) this.handleOptionEnter();
else this.open = true;
},
':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.escape.prevent'() {
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() },
}
}));
}
{% for event in (register_on or ["alpine:init"]) %}
document.addEventListener('{{ event }}', registerSelectComponent);
{% endfor %}
}
</script>
+21 -165
View File
@@ -1,177 +1,26 @@
{% macro select(
id=None,
name=None,
selected=None,
main_attrs=None,
trigger_attrs=None,
content_attrs=None,
listbox_attrs=None,
is_combobox=False,
name=None,
main_attrs={},
trigger_attrs={},
content_attrs={},
listbox_attrs={},
input_attrs={},
search_placeholder="Search entries...",
is_combobox=false,
include_html=true,
include_script=true,
include_script=false,
register_on=["alpine:init"]
) %}
{% if include_script %}
<script>
if (!window._selectComponentLoaded) {
window._selectComponentLoaded = true;
const registerSelectComponent = () => {
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'));
const option = this.options[this.focusedIndex];
option.setAttribute('data-focus', '');
option.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
},
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) {
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.prevent'() {
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'() {
if (this.open) this.handleOptionEnter();
else this.open = true;
},
':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.escape.prevent'() {
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() },
}
}));
}
{% for event in register_on %}
document.addEventListener('{{ event }}', registerSelectComponent);
{% endfor %}
}
</script>
{% include "components/select-script.njk" %}
{% endif %}
{% if include_html %}
<div
class="popover {{ main_attrs.class }}"
x-data="select('{{ name }}', '{{ selected }}')"
x-data="select('{{ name }}', '{{ selected or '' }}')"
@click.away="open = false"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in main_attrs %}
@@ -215,7 +64,7 @@
<input
type="text"
value=""
placeholder="Search framework..."
placeholder="{{ search_placeholder }}"
autocomplete="off"
autocorrect="off"
spellcheck="false"
@@ -239,8 +88,15 @@
{% if caller %}{{ caller() }}{% endif %}
</div>
</div>
{% if name %}
<input type="hidden" name="{{ name }}" x-model="selectedValue">
{% if name is defined %}
<input
type="hidden"
name="{{ name }}"
x-model="selectedValue"
{% for key, value in input_attrs %}
{% if key != 'name' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% endif %}
</div>
{% endif %}
+71
View File
@@ -0,0 +1,71 @@
<script>
if (!window._tabsComponentLoaded) {
window._tabsComponentLoaded = true;
const registerTabsComponent = () => {
Alpine.data('tabs', (initialTabIndex = 0) => ({
activeTabIndex: initialTabIndex,
tabs: [],
panels: [],
init() {
this.$nextTick(() => {
this.tabs = Array.from(this.$el.querySelectorAll(':scope > [role=tablist] [role=tab]:not([disabled])'));
this.panels = Array.from(this.$el.querySelectorAll(':scope > [role=tabpanel]'));
if (this.tabs.length > 0) {
this.selectTab(this.tabs[initialTabIndex], 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() },
},
}));
}
{% for event in (register_on or ["alpine:init"]) %}
document.addEventListener('{{ event }}', registerTabsComponent);
{% endfor %}
}
</script>
+3 -72
View File
@@ -5,82 +5,12 @@
tablist_attrs=None,
default_tab_index=1,
include_html=true,
include_script=true,
include_script=false,
register_on=["alpine:init"]
)
%}
{% if include_script %}
<script>
if (!window._tabsComponentLoaded) {
window._tabsComponentLoaded = true;
const registerTabsComponent = () => {
Alpine.data('tabs', (initialTabIndex = 0) => ({
activeTabIndex: initialTabIndex,
tabs: [],
panels: [],
init() {
this.$nextTick(() => {
this.tabs = Array.from(this.$el.querySelectorAll(':scope > [role=tablist] [role=tab]:not([disabled])'));
this.panels = Array.from(this.$el.querySelectorAll(':scope > [role=tabpanel]'));
if (this.tabs.length > 0) {
this.selectTab(this.tabs[initialTabIndex], 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() },
},
}));
}
{% for event in register_on %}
document.addEventListener('{{ event }}', registerTabsComponent);
{% endfor %}
}
</script>
{% include "components/tabs-script.njk" %}
{% endif %}
{% if include_html %}
@@ -102,6 +32,7 @@
>
{% for tabset in tabsets %}
<button
type="button"
role="tab"
id="{{ tabset.id }}-tab-{{ loop.index }}"
aria-controls="{{ tabset.id }}-panel-{{ loop.index }}"
@@ -0,0 +1,52 @@
<script>
if (!window._toastComponentLoaded) {
window._toastComponentLoaded = true;
const registerToastComponent = () => {
Alpine.store('toaster', { isPaused: false });
Alpine.data('toast', (category = '', timeoutDuration = null) => ({
open: false,
timeoutDuration: null,
timeoutId: null,
init() {
this.timeoutDuration = 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.prevent']() { this.close() },
},
}));
}
{% for event in (register_on or ["alpine:init"]) %}
document.addEventListener('{{ event }}', registerToastComponent);
{% endfor %}
}
</script>
+2 -53
View File
@@ -4,62 +4,11 @@
main_attrs=None,
is_fragment=false,
include_html=true,
include_script=true,
include_script=false,
register_on=["alpine:init"]
) %}
{% if not is_fragment and include_script %}
<script>
if (!window._toastComponentLoaded) {
window._toastComponentLoaded = true;
const registerToastComponent = () => {
Alpine.store('toaster', { isPaused: false });
Alpine.data('toast', (category = '', timeoutDuration = null) => ({
open: false,
timeoutDuration: null,
timeoutId: null,
init() {
this.timeoutDuration = 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.prevent']() { this.close() },
},
}));
}
{% for event in register_on %}
document.addEventListener('{{ event }}', registerToastComponent);
{% endfor %}
}
</script>
{% include "components/toast-script.njk" %}
{% endif %}
{% if include_html %}
+8 -3
View File
@@ -47,9 +47,14 @@
<!-- JS -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
{% include "components/dialog-script.njk" %}
{% include "components/dropdown-menu-script.njk" %}
{% include "components/popover-script.njk" %}
{% include "components/select-script.njk" %}
{% include "components/tabs-script.njk" %}
{% include "components/toast-script.njk" %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/html.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/django.min.js"></script>
<script>
const applyHighlighting = () => { if (window.hljs) hljs.highlightAll(); };
@@ -60,7 +65,7 @@
document.addEventListener('htmx:afterSwap', applyHighlighting);
}
</script>
{% if head %}{{ head | safe }}{% endif %}
</head>
<body>
@@ -118,7 +123,7 @@
</div>
</header>
<div class="container grid gap-4 p-4">
<div class="grid gap-4 p-4">
{{ content | safe }}
</div>
</main>
+1 -1
View File
@@ -8,7 +8,7 @@ layout: layouts/base.njk
<h1 class="text-3xl font-semibold tracking-tight">{{ title }}</h1>
<p class="text-muted-foreground">{{ description }}</p>
</header>
<article class="pb-12 pt-8 [&>h2]:mt-16 [&>h2]:scroll-m-22 [&>h2]:border-b [&>h2]:pb-4 [&>h2]:text-xl [&>h2]:font-semibold [&>h2]:tracking-tight [&>h2]:first:mt-0 [&>h3]:font-heading [&>h3]:mt-8 [&>h3]:scroll-m-22 [&>h3]:text-lg [&>h3]:font-semibold [&>h3]:tracking-tight content">
<article class="pb-12 pt-8 [&>h2]:mt-16 [&>h2]:scroll-m-22 [&>h2]:border-b [&>h2]:pb-4 [&>h2]:text-xl [&>h2]:font-semibold [&>h2]:tracking-tight [&>h2]:first:mt-0 [&>h3]:font-heading [&>h3]:mt-8 [&>h3]:scroll-m-22 [&>h3]:text-lg [&>h3]:font-semibold [&>h3]:tracking-tight">
{{ content | safe }}
</article>
</div>
+39
View File
@@ -0,0 +1,39 @@
---
layout: layouts/base.njk
---
<main class="mx-auto relative py-6 lg:gap-10 lg:py-8 flex w-full max-w-screen-lg">
<div class="mx-auto w-full min-w-0 max-w-screen-md">
<header class="space-y-2">
<h1 class="text-3xl font-semibold tracking-tight">{{ title }}</h1>
<p class="text-muted-foreground">{{ description }}</p>
</header>
<article class="pb-12 pt-8 [&>h2]:mt-16 [&>h2]:scroll-m-22 [&>h2]:border-b [&>h2]:pb-4 [&>h2]:text-xl [&>h2]:font-semibold [&>h2]:tracking-tight [&>h2]:first:mt-0 [&>h3]:font-heading [&>h3]:mt-8 [&>h3]:scroll-m-22 [&>h3]:text-lg [&>h3]:font-semibold [&>h3]:tracking-tight">
{{ content | safe }}
</article>
</div>
<div class="hidden text-sm xl:block w-full max-w-[300px]">
<nav class="sticky top-22 space-y-2 [&_ul]:m-0 [&_ul]:list-none [&_ul_ul]:pl-4 [&_li]:mt-0 [&_li]:pt-2 [&_a]:inline-block [&_a]:no-underline [&_a]:transition-colors [&_a]:hover:text-foreground [&_a]:text-muted-foreground">
<h4 class="font-medium">On This Page</h4>
{% if toc %}
{% macro renderToc(items) %}
{% if items and items.length %}
<ul>
{% for item in items %}
<li>
<a href="#{{ item.id }}">{{ item.label }}</a>
{% if item.children and item.children.length %}
{{ renderToc(item.children) }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% endmacro %}
{{ renderToc(toc) }}
{% else %}
<p class="text-muted-foreground text-center bg-muted rounded-md p-2">No table of contents.</p>
{% endif %}
</nav>
</div>
</main>
+1 -1
View File
@@ -11,7 +11,7 @@
</div>
{% endset %}
<div class="relative my-4">
<div class="relative my-6">
{% from "components/tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "Preview", panel: preview_panel },
@@ -62,7 +62,7 @@
<button
x-init
@click="$dispatch('dialog:control', { targetId: 'dialog-example', action: 'open' })"
@click="$dispatch('dialog:open', { id: 'dialog-example' })"
type="button"
class="btn-outline"
>
@@ -5,6 +5,7 @@
<h2 class="text-sm font-medium">Select</h2>
</header>
<div class="p-4 gap">
<div class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<select class="select w-[180px]">
@@ -51,7 +52,7 @@
{% endcall %}
{% call select(trigger_attrs={'class': 'w-[180px]'}) %}
{% call select(name="just-testing", trigger_attrs={'class': 'w-[180px]'}) %}
<div type="button" role="option" data-value="bar">
{% lucide "chart-bar", {"class": "text-muted-foreground"} %}
Bar