mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-11 04:38:18 +00:00
Working on docs and polishing macros
This commit is contained in:
Vendored
+28
-3
@@ -336,9 +336,6 @@
|
||||
.mx-auto {
|
||||
margin-inline: auto;
|
||||
}
|
||||
.my-4 {
|
||||
margin-block: calc(var(--spacing) * 4);
|
||||
}
|
||||
.my-6 {
|
||||
margin-block: calc(var(--spacing) * 6);
|
||||
}
|
||||
@@ -1381,6 +1378,32 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.\[\&_a\:not\(\[class\]\)\]\:text-muted-foreground {
|
||||
& a:not([class]) {
|
||||
color: var(--color-muted-foreground);
|
||||
}
|
||||
}
|
||||
.\[\&_a\:not\(\[class\]\)\]\:no-underline {
|
||||
& a:not([class]) {
|
||||
text-decoration-line: none;
|
||||
}
|
||||
}
|
||||
.\[\&_a\:not\(\[class\]\)\]\:transition-colors {
|
||||
& a:not([class]) {
|
||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
}
|
||||
}
|
||||
.\[\&_a\:not\(\[class\]\)\]\:hover\:text-foreground {
|
||||
& a:not([class]) {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.\[\&_img\]\:size-8 {
|
||||
& img {
|
||||
width: calc(var(--spacing) * 8);
|
||||
@@ -2848,6 +2871,8 @@
|
||||
display: flex;
|
||||
height: calc(var(--spacing) * 10);
|
||||
width: 100%;
|
||||
min-width: calc(var(--spacing) * 0);
|
||||
flex: 1;
|
||||
border-radius: var(--radius-md);
|
||||
background-color: transparent;
|
||||
padding-block: calc(var(--spacing) * 3);
|
||||
|
||||
Vendored
+1
-1
@@ -660,7 +660,7 @@
|
||||
@apply size-4 shrink-0 opacity-50;
|
||||
}
|
||||
input[role='combobox'] {
|
||||
@apply placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50;
|
||||
@apply placeholder:text-muted-foreground flex h-10 flex-1 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50 min-w-0;
|
||||
}
|
||||
}
|
||||
[role='listbox']:not(:has([data-value]:not([aria-hidden='true'])))::before {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -68,8 +68,4 @@ toc:
|
||||
|
||||
<h2 id="usage"><a href="#usage">Usage</a></h2>
|
||||
|
||||
<section class="prose my-6">
|
||||
<p>Basecoat animates <code><details></code> elements by default. Using Alpine.js to handle the open/close state and Tailwind to style the the <code><details></code> elements, you can get an accordion component:</p>
|
||||
</section>
|
||||
|
||||
{{ code_block(code) }}
|
||||
<p class="prose my-6">Basecoat animates <code><details></code> elements by default. The example above uses Alpine.js to handle the open/close state along with some Tailwind CSS classes.</p>
|
||||
@@ -6,8 +6,6 @@ toc:
|
||||
- label: Usage
|
||||
id: usage
|
||||
children:
|
||||
- label: Default
|
||||
id: usage-default
|
||||
- label: Jinja or Nunjucks
|
||||
id: usage-jinja
|
||||
---
|
||||
@@ -35,27 +33,38 @@ toc:
|
||||
|
||||
<h2 id="usage"><a href="#usage">Usage</a></h2>
|
||||
|
||||
<h3 id="usage-default"><a href="#usage-default">Default</a></h3>
|
||||
<p class="prose my-6">The Alert Dialog component uses the same Alpine.js code as the <a href="/components/dialog" class="font-medium underline underline-offset-4">Dialog</a> component with one small difference: we set <code>initialCloseOnOverlayClick</code> to <code>false</code> to prevent the dialog from closing when the overlay is clicked.</p>
|
||||
|
||||
<section class="prose my-6">
|
||||
<p>The Alert Dialog component uses the same Alpine.js code as the <a href="/components/dialog" class="font-medium underline underline-offset-4">Dialog</a> component with one small difference: we set <code>initialCloseOnOverlayClick</code> to <code>false</code> to prevent the dialog from closing when the overlay is clicked.</p>
|
||||
<h4 class="badge-secondary">HTML</h4>
|
||||
|
||||
<section class="my-6">
|
||||
{% set code_html %}{% set footer %}
|
||||
<button class="btn-outline" @click="open = false">Cancel</button>
|
||||
<button class="btn-primary" @click="open = false">Continue</button>
|
||||
{% endset %}
|
||||
{{ dialog(
|
||||
id="alert-dialog",
|
||||
title="Are you absolutely sure?",
|
||||
description="This action cannot be undone. This will permanently delete your account and remove your data from our servers.",
|
||||
trigger="Open alert dialog",
|
||||
trigger_attrs={"class": "btn-outline"},
|
||||
footer=footer,
|
||||
close_button=false,
|
||||
close_on_overlay_click=false
|
||||
) }}{% endset %}
|
||||
{{ code_block(code_html | prettyHtml) }}
|
||||
</section>
|
||||
|
||||
{{ code_block(code | prettyHtml) }}
|
||||
<h4 class="badge-secondary">Javascript (Alpine.js)</h4>
|
||||
|
||||
<section class="my-6">
|
||||
{% set code_script %}{% include "components/dialog-script.njk" %}{% endset %}
|
||||
{{ code_block(code_script | prettyHtml) }}
|
||||
</section>
|
||||
|
||||
<h3 id="usage-jinja"><a href="#usage-jinja">Jinja or Nunjucks</a></h3>
|
||||
|
||||
<section class="prose my-6">
|
||||
<p>If you use Jinja (or Nunjucks), you can use the <code>dialog()</code> macro directly, making sure to set <code>close_on_overlay_click</code> to <code>False</code> (or <code>false</code> for Nunjucks).</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/jinja-and-nunjucks">Read more about using Basecoat with Jinja or Nunjucks</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/components/dialog">Read more about the Dialog component</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<p class="my-6 prose">If you use Jinja (or Nunjucks), you can use the <code>dialog()</code> macro directly, making sure to set <code>close_on_overlay_click</code> and <code>close_button</code> to <code>False</code> (or <code>false</code> for Nunjucks). <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
|
||||
|
||||
{% set raw_code %}{% raw %}{% set footer %}
|
||||
<button class="btn-outline" @click="open = false">Cancel</button>
|
||||
|
||||
@@ -31,11 +31,10 @@ toc:
|
||||
<section class="prose my-6">
|
||||
<p>You can use one of the two classes: <code>alert</code> or <code>alert-dialog</code> with any of the following children:</p>
|
||||
<ul>
|
||||
<li>A SVG icon,</li>
|
||||
<li>A svg icon,</li>
|
||||
<li>A <code><h2></code> for the title.</li>
|
||||
<li>A <code><section></code> for the description.</li>
|
||||
<li>A <code><section></code> for the description. This can include paragraphs, lists, etc.</li>
|
||||
</ul>
|
||||
<p><a href="/kitchen-sink#alert">See the kitchen sink for more examples.</a></p>
|
||||
</section>
|
||||
|
||||
{{ code_block(code) }}
|
||||
|
||||
@@ -20,8 +20,4 @@ toc:
|
||||
|
||||
<h2 id="usage"><a href="#usage">Usage</a></h2>
|
||||
|
||||
<section class="prose my-6">
|
||||
<p>Avatars are fairly simple to implement with Tailwind. The example above shows how to implement one with Tailwind and an <code><img></code> element. <a href="/kitchen-sink#avatar">You can find more examples in the kitchen sink</a>.</p>
|
||||
</section>
|
||||
|
||||
{{ code_block(code) }}
|
||||
<p class="my-6 prose">Avatars are fairly simple to implement with Tailwind. The example above shows how to implement one with Tailwind and an <code><img></code> element. More examples can be found in the <a href="/kitchen-sink#avatar">kitchen sink</a>.</p>
|
||||
@@ -31,7 +31,7 @@ toc:
|
||||
<h2 id="usage"><a href="#usage">Usage</a></h2>
|
||||
|
||||
<section class="prose my-6">
|
||||
<p>Simply use one of the badge classes: <code>badge</code>, <code>badge-primary</code>, <code>badge-secondary</code>, <code>badge-destructive</code> or <code>badge-outline</code>. <a href="/kitchen-sink#badge">You can find more examples in the kitchen sink</a>.</p>
|
||||
<p>Use one of the badge classes: <code>badge</code>, <code>badge-primary</code>, <code>badge-secondary</code>, <code>badge-destructive</code> or <code>badge-outline</code>.
|
||||
</section>
|
||||
|
||||
{{ code_block(code) }}
|
||||
|
||||
@@ -16,7 +16,8 @@ toc:
|
||||
<h2>There is no dedicated breadcrumb component in Basecoat.</h2>
|
||||
</div>
|
||||
|
||||
{% set code %}<ol class="text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5">
|
||||
{% set code_script %}{% include "components/dropdown-menu-script.njk" %}{% endset %}
|
||||
{% set code_html %}<ol class="text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5">
|
||||
<li class="inline-flex items-center gap-1.5">
|
||||
<a href="#" class="hover:text-foreground transition-colors">Home</a>
|
||||
</li>
|
||||
@@ -47,12 +48,22 @@ toc:
|
||||
<span class="text-foreground font-normal">Breadcrumb</span>
|
||||
</li>
|
||||
</ol>{% endset %}
|
||||
{{ code_preview("dropdown-menu", code | prettyHtml) }}
|
||||
{{ code_preview("dropdown-menu", code_script ~ code_html | prettyHtml) }}
|
||||
|
||||
<h2 id="usage"><a href="#usage">Usage</a></h2>
|
||||
|
||||
<section class="prose my-6">
|
||||
<p>Using Tailind and the <a href="/components/dropdown-menu" class="font-medium underline underline-offset-4">Dropdown Menu</a> component you can get a breadcrumb component:</p>
|
||||
<p>The example above uses some simple Tailwind CSS along with the <a href="/components/dropdown-menu">Dropdown Menu</a>.</p>
|
||||
</section>
|
||||
|
||||
{{ code_block(code | prettyHtml) }}
|
||||
<h4 class="badge-secondary">HTML</h3>
|
||||
|
||||
<section class="my-6">
|
||||
{{ code_block(code_html | prettyHtml) }}
|
||||
</section>
|
||||
|
||||
<h4 class="badge-secondary">Javascript (Alpine.js) for the Dropdown Menu</h3>
|
||||
|
||||
<section class="my-6">
|
||||
{{ code_block(code_script | prettyHtml) }}
|
||||
</section>
|
||||
@@ -68,7 +68,9 @@ toc:
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{{ code_block(code) }}
|
||||
<section class="my-6">
|
||||
{{ code_block(code) }}
|
||||
</section>
|
||||
|
||||
<h2 id="examples"><a href="#examples">Examples</a></h2>
|
||||
|
||||
|
||||
@@ -48,13 +48,12 @@ toc:
|
||||
<h2 id="usage">Usage</h2>
|
||||
|
||||
<section class="prose my-6">
|
||||
<p>Simply add the <code>card</code> class to any element and then define any of the children elements:</p>
|
||||
<p>Add the <code>card</code> class to any element and then define any of the children elements:</p>
|
||||
<ul>
|
||||
<li>A <code><header></code> for the card header, which can contain a <code><h2></code> for the title and a <code><p></code> for the description.</li>
|
||||
<li>A <code><section></code> for the card content.</li>
|
||||
<li>A <code><footer></code> for the card footer.</li>
|
||||
</ul>
|
||||
<p><a href="/kitchen-sink#alert">See the kitchen sink for more examples.</a></p>
|
||||
</section>
|
||||
|
||||
{% set code %}<div class="card">
|
||||
|
||||
@@ -28,10 +28,13 @@ toc:
|
||||
<h2 id="usage">Usage</h2>
|
||||
|
||||
<section class="prose my-6">
|
||||
<p>Simply add the <code>input</code> class to your <code><input type="radio"></code> elements or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). You can also set the <code>aria-invalid</code> attribute to <code>true</code> to make the input invalid.</p>
|
||||
<p>Add the <code>input</code> class to your <code><input type="radio"></code> elements or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). You can also set the <code>aria-invalid</code> attribute to <code>true</code> to make the input invalid.</p>
|
||||
</section>
|
||||
|
||||
{{ code_block(code) }}
|
||||
<section class="my-6">
|
||||
{% set code %}<input type="checkbox" class="input">{% endset %}
|
||||
{{ code_block(code) }}
|
||||
</section>
|
||||
|
||||
<h2 id="examples"><a href="#examples">Examples</a></h2>
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@ toc:
|
||||
- label: Usage
|
||||
id: usage
|
||||
children:
|
||||
- label: Default
|
||||
id: usage-default
|
||||
- label: Jinja or Nunjucks
|
||||
id: usage-jinja
|
||||
- label: Examples
|
||||
id: examples
|
||||
children:
|
||||
- label: With groups
|
||||
id: example-with-groups
|
||||
- label: Scrollable
|
||||
id: example-scrollable
|
||||
- label: Top side
|
||||
id: example-top
|
||||
---
|
||||
|
||||
{% from "macros/code_preview.njk" import code_preview %}
|
||||
@@ -27,6 +27,7 @@ toc:
|
||||
trigger_attrs={"class": "w-[200px]"},
|
||||
listbox_attrs={"data-empty": "No framework found."},
|
||||
is_combobox=true,
|
||||
search_placeholder="Search framework...",
|
||||
register_on=["alpine:init", "htmx:afterSwap"]
|
||||
) %}
|
||||
{% for framework in options_frameworks %}
|
||||
@@ -37,26 +38,45 @@ toc:
|
||||
|
||||
<h2 id="usage"><a href="#usage">Usage</a></h2>
|
||||
|
||||
<h3 id="usage-default"><a href="#usage-default">Default</a></h3>
|
||||
|
||||
<section class="prose my-6">
|
||||
<p>The Combobox component uses the same Alpine.js code as the <a href="/components/select" class="font-medium underline underline-offset-4">Select</a> component, and almost identical markup, the only difference being the search box.</p>
|
||||
<p>Combobox uses the same Alpine.js code and markup as the <a href="/components/select">Select</a> component, the only difference being the search box at the top of the listbox.</p>
|
||||
<p>You can set up a few additional options on the <code><div data-popover></code> element:</p>
|
||||
<ul>
|
||||
<li><code>data-side</code> can be set to <code>top</code>, <code>bottom</code>, <code>left</code>, or <code>right</code> to change the side of the popover.</li>
|
||||
<li><code>data-align</code> can be set to <code>start</code>, <code>center</code>, or <code>end</code> to change the alignment of the popover.</li>
|
||||
<li><code>data-empty</code> to set up the message when the search is empty.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{{ code_block(code | prettyHtml) }}
|
||||
<h4 class="badge-secondary">HTML</h4>
|
||||
|
||||
<section class="my-6">
|
||||
{% set code_html %}
|
||||
{% call select(
|
||||
trigger_attrs={"class": "w-[200px]"},
|
||||
listbox_attrs={"data-empty": "No framework found."},
|
||||
is_combobox=true,
|
||||
search_placeholder="Search framework...",
|
||||
register_on=["alpine:init", "htmx:afterSwap"]
|
||||
) %}
|
||||
{% for framework in options_frameworks %}
|
||||
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
|
||||
{% endfor %}
|
||||
{% endcall %}{% endset %}
|
||||
{{ code_block(code_html | prettyHtml) }}
|
||||
</section>
|
||||
|
||||
<h4 class="badge-secondary">Javascript (Alpine.js)</h4>
|
||||
|
||||
<section class="my-6">
|
||||
{% set code_script %}{% include "components/select-script.njk" %}{% endset %}
|
||||
{{ code_block(code_script | prettyHtml) }}
|
||||
</section>
|
||||
|
||||
<h3 id="usage-jinja"><a href="#usage-jinja">Jinja or Nunjucks</a></h3>
|
||||
|
||||
<section class="prose my-6">
|
||||
<p>If you use Jinja (or Nunjucks), you can use the <code>select()</code> macro directly, making sure to set <code>is_combobox</code> to <code>True</code> (or <code>true</code> for Nunjucks).</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/jinja-and-nunjucks">Read more about using Basecoat with Jinja or Nunjucks</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/components/dialog">Read more about the Dialog component</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="my-6 prose">If you use Jinja (or Nunjucks), you can use the <code>select()</code> macro directly, making sure to set <code>is_combobox</code> to <code>True</code> (or <code>true</code> for Nunjucks). <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
|
||||
</section>
|
||||
|
||||
{% set raw_code %}{% raw %}{% call select(
|
||||
@@ -73,7 +93,7 @@ toc:
|
||||
|
||||
<h2 id="examples"><a href="#examples">Examples</a></h2>
|
||||
|
||||
<h3 id="example-with-groups"><a href="#example-with-groups">With groups</a></h3>
|
||||
<h3 id="example-scrollable"><a href="#example-scrollable">Scrollable</a></h3>
|
||||
|
||||
{% set options_timezones = [
|
||||
{
|
||||
@@ -116,6 +136,7 @@ toc:
|
||||
content_attrs={"class": "w-72"},
|
||||
listbox_attrs={"data-empty": "No timezone found."},
|
||||
is_combobox=true,
|
||||
search_placeholder="Search timezone...",
|
||||
register_on=["alpine:init", "htmx:afterSwap"]
|
||||
) %}
|
||||
<div class="max-h-64 overflow-y-auto scrollbar">
|
||||
@@ -138,3 +159,21 @@ toc:
|
||||
{% endcall %}
|
||||
{% endset %}
|
||||
{{ code_preview("combobox-with-groups", code | prettyHtml) }}
|
||||
|
||||
<h3 id="example-top"><a href="#example-top">Top side</a></h3>
|
||||
|
||||
{% set options_frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] %}
|
||||
{% set code %}
|
||||
{% call select(
|
||||
trigger_attrs={"class": "w-[200px]"},
|
||||
listbox_attrs={"data-empty": "No framework found."},
|
||||
is_combobox=true,
|
||||
search_placeholder="Search framework...",
|
||||
register_on=["alpine:init", "htmx:afterSwap"],
|
||||
content_attrs={"data-side": "top"}
|
||||
) %}
|
||||
{% for framework in options_frameworks %}
|
||||
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
|
||||
{% endfor %}
|
||||
{% endcall %}{% endset %}
|
||||
{{ code_preview("combobox", code | prettyHtml) }}
|
||||
@@ -32,7 +32,13 @@ toc:
|
||||
<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"/>
|
||||
<input type="text" id="demo-popover-width" value="100%" class="col-span-2 h-8" x-init="$watch('open', (value) => {
|
||||
if (value) {
|
||||
setTimeout(() => $el.focus(), 50)
|
||||
} else {
|
||||
$el.blur()
|
||||
}
|
||||
})"/>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center gap-4">
|
||||
<label for="demo-popover-max-width">Max. width</label>
|
||||
|
||||
@@ -51,7 +51,7 @@ toc:
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endset %}
|
||||
{{ code_preview("select-alpine", code_alpine | prettyHtml) }}
|
||||
{{ code_preview("select", code_alpine | prettyHtml) }}
|
||||
|
||||
<h2 id="usage"><a href="#usage">Usage</a></h2>
|
||||
|
||||
@@ -69,7 +69,35 @@ toc:
|
||||
<p>If you need to do more than what <code><select></code> allows (e.g. HTML options), you can use the code below which uses the <a href="/components/combobox">Popover</a> component. Make sure to include the hidden input with the name you want to use for that field.</p>
|
||||
</section>
|
||||
|
||||
{{ code_block(code_alpine | prettyHtml) }}
|
||||
<h4 class="badge-secondary">HTML</h4>
|
||||
|
||||
<section class="prose my-6">
|
||||
{% set code_html %}
|
||||
{% call select(
|
||||
name="select-js",
|
||||
trigger_attrs={"class": "w-[180px]"},
|
||||
register_on=["alpine:init", "htmx:afterSwap"],
|
||||
include_script=false
|
||||
) %}
|
||||
<div role="group" aria-labelledby="fruit-options-label">
|
||||
<span id="fruit-options-label" role="heading">Fruits</span>
|
||||
<div role="option" data-value="apple">Apple</div>
|
||||
<div role="option" data-value="banana">Banana</div>
|
||||
<div role="option" data-value="blueberry">Blueberry</div>
|
||||
<div role="option" data-value="pineapple">Grapes</div>
|
||||
<div role="option" data-value="pineapple">Pineapple</div>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endset %}
|
||||
{{ code_block(code_html | prettyHtml) }}
|
||||
</section>
|
||||
|
||||
<h4 class="badge-secondary">Javascript</h4>
|
||||
|
||||
<section class="prose my-6">
|
||||
{% set code_script %}{% include "components/select-script.njk" %}{% endset %}
|
||||
{{ code_block(code_script | prettyHtml, "html") }}
|
||||
</section>
|
||||
|
||||
<h3 id="usage-jinja"><a href="#usage-jinja">Jinja or Nunjucks</a></h3>
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
layout: layouts/page.njk
|
||||
title: Installation
|
||||
description: How to install Basecoat in your app.
|
||||
---
|
||||
|
||||
<h2 id="jinja-and-nunjucks"><a href="#jinja-and-nunjucks">CSS vs. Tailwind</a></h2>
|
||||
|
||||
<h2 id="htmx"><a href="#htmx">HTML & Javascript</a></h2>
|
||||
|
||||
<h2 id="jinja-and-nunjucks"><a href="#jinja-and-nunjucks">Jinja and Nunjucks</a></h2>
|
||||
Reference in New Issue
Block a user