mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-13 13:48:28 +00:00
PoC for new components, including select
This commit is contained in:
+54
-30
@@ -446,37 +446,56 @@
|
||||
/* Dialog */
|
||||
@layer components {
|
||||
.dialog {
|
||||
[role='dialog'] {
|
||||
@apply fixed inset-0 z-50 bg-black/50 opacity-0 invisible transition-all;
|
||||
@apply inset-y-0 opacity-0 transition-all transition-discrete;
|
||||
|
||||
&:not([inert]),
|
||||
&[inert='false'] {
|
||||
@apply opacity-100 visible;
|
||||
> article {
|
||||
@apply opacity-100 scale-100 visible;
|
||||
}
|
||||
&:is([open],:popover-open) {
|
||||
@apply opacity-100;
|
||||
|
||||
&::backdrop {
|
||||
@apply opacity-100;
|
||||
}
|
||||
> article {
|
||||
@apply bg-background fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg sm:max-w-lg opacity-0 scale-95 invisible transition-all max-h-[calc(100%-2rem)];
|
||||
@apply scale-100;
|
||||
}
|
||||
|
||||
> header {
|
||||
@apply flex flex-col gap-2 text-center sm:text-left;
|
||||
@starting-style {
|
||||
@apply opacity-0;
|
||||
|
||||
> h2 {
|
||||
@apply text-lg leading-none font-semibold;
|
||||
}
|
||||
> p {
|
||||
@apply text-muted-foreground text-sm;
|
||||
}
|
||||
&::backdrop {
|
||||
@apply opacity-0;
|
||||
}
|
||||
> section {
|
||||
@apply flex-1 -mx-6 px-6;
|
||||
> article {
|
||||
@apply scale-95;
|
||||
}
|
||||
> footer {
|
||||
@apply flex flex-col-reverse gap-2 sm:flex-row sm:justify-end;
|
||||
}
|
||||
}
|
||||
&::backdrop {
|
||||
@apply bg-black/50 opacity-0 transition-all transition-discrete;
|
||||
}
|
||||
> article {
|
||||
@apply bg-background fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border p-6 shadow-lg sm:max-w-lg max-h-[calc(100%-2rem)];
|
||||
@apply transition-all scale-95;
|
||||
|
||||
> header {
|
||||
@apply flex flex-col gap-2 text-center sm:text-left;
|
||||
|
||||
> h2 {
|
||||
@apply text-lg leading-none font-semibold;
|
||||
}
|
||||
> p {
|
||||
@apply text-muted-foreground text-sm;
|
||||
}
|
||||
}
|
||||
> section {
|
||||
@apply flex-1 -mx-6 px-6;
|
||||
}
|
||||
> footer {
|
||||
@apply flex flex-col-reverse gap-2 sm:flex-row sm:justify-end;
|
||||
}
|
||||
> form[method='dialog'] {
|
||||
@apply absolute top-4 right-4;
|
||||
> button {
|
||||
@apply ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4;
|
||||
@apply ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -531,7 +550,9 @@
|
||||
}
|
||||
|
||||
/* Popover */
|
||||
@layer components {
|
||||
@import "./components/popover.css";
|
||||
|
||||
/* @layer components {
|
||||
.popover {
|
||||
@apply relative inline-flex;
|
||||
|
||||
@@ -625,7 +646,7 @@
|
||||
[role='separator'] {
|
||||
@apply border-border -mx-1 my-1;
|
||||
}
|
||||
/* Combobox */
|
||||
// Combobox
|
||||
> header {
|
||||
@apply flex h-9 items-center gap-2 border-b px-3 -mx-1 -mt-1 mb-1;
|
||||
|
||||
@@ -647,7 +668,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
/* Radio */
|
||||
@layer components {
|
||||
@@ -713,7 +734,7 @@
|
||||
/* Select */
|
||||
@layer components {
|
||||
.form select,
|
||||
.select {
|
||||
select.select {
|
||||
@apply appearance-none border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent pl-3 pr-9 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 h-9;
|
||||
@apply bg-[image:var(--chevron-down-icon-50)] bg-no-repeat bg-position-[center_right_0.75rem] bg-size-[1rem];
|
||||
|
||||
@@ -726,7 +747,7 @@
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
&[data-uninitialized] {
|
||||
&:not([data-sidebar-initialized]) {
|
||||
@apply max-md:hidden;
|
||||
}
|
||||
&:not([aria-hidden]),
|
||||
@@ -734,7 +755,10 @@
|
||||
@apply max-md:bg-black/50 max-md:fixed max-md:inset-0 max-md:z-40;
|
||||
}
|
||||
nav {
|
||||
@apply bg-sidebar text-sidebar-foreground flex flex-col w-(--sidebar-mobile-width) md:w-(--sidebar-width) fixed inset-y-0 transition-transform ease-in-out duration-300 z-50;
|
||||
@apply bg-sidebar text-sidebar-foreground flex flex-col w-(--sidebar-mobile-width) md:w-(--sidebar-width) fixed inset-y-0 z-50 transition-transform ease-in-out duration-300;
|
||||
}
|
||||
& + * {
|
||||
@apply transition-[margin] ease-in-out duration-300;
|
||||
}
|
||||
&:not([data-side]),
|
||||
&[data-side=left] {
|
||||
@@ -742,7 +766,7 @@
|
||||
@apply left-0 border-r;
|
||||
}
|
||||
& + * {
|
||||
@apply relative md:ml-(--sidebar-width) transition-[margin] ease-in-out duration-300;
|
||||
@apply relative md:ml-(--sidebar-width);
|
||||
}
|
||||
&[aria-hidden=true] {
|
||||
nav {
|
||||
@@ -758,7 +782,7 @@
|
||||
@apply right-0 border-l;
|
||||
}
|
||||
& + * {
|
||||
@apply relative md:mr-(--sidebar-width) transition-[margin] ease-in-out duration-300;
|
||||
@apply relative md:mr-(--sidebar-width);
|
||||
}
|
||||
&[aria-hidden=true] {
|
||||
nav {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
@layer components {
|
||||
.dialog {
|
||||
@apply inset-y-0 opacity-0 transition-all transition-discrete;
|
||||
|
||||
&:is([open],:popover-open) {
|
||||
@apply opacity-100;
|
||||
|
||||
&::backdrop {
|
||||
@apply opacity-100;
|
||||
}
|
||||
> article {
|
||||
@apply scale-100;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
@apply opacity-0;
|
||||
|
||||
&::backdrop {
|
||||
@apply opacity-0;
|
||||
}
|
||||
> article {
|
||||
@apply scale-95;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::backdrop {
|
||||
@apply bg-black/50 opacity-0 transition-all transition-discrete;
|
||||
}
|
||||
|
||||
> article {
|
||||
@apply bg-background fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border p-6 shadow-lg sm:max-w-lg max-h-[calc(100%-2rem)];
|
||||
@apply transition-all scale-95;
|
||||
|
||||
> header {
|
||||
@apply flex flex-col gap-2 text-center sm:text-left;
|
||||
|
||||
> h2 {
|
||||
@apply text-lg leading-none font-semibold;
|
||||
}
|
||||
> p {
|
||||
@apply text-muted-foreground text-sm;
|
||||
}
|
||||
}
|
||||
> section {
|
||||
@apply flex-1 -mx-6 px-6;
|
||||
}
|
||||
> footer {
|
||||
@apply flex flex-col-reverse gap-2 sm:flex-row sm:justify-end;
|
||||
}
|
||||
> form[method='dialog'] {
|
||||
@apply absolute top-4 right-4;
|
||||
> button {
|
||||
@apply ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
@layer components {
|
||||
div.select {
|
||||
[popover].popover {
|
||||
@apply p-1 min-w-[anchor-size(width)];
|
||||
|
||||
|
||||
|
||||
[role='option'] {
|
||||
@apply aria-hidden:hidden [&_svg]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none [&_svg]:shrink-0 [&_svg]:size-4 aria-disabled:opacity-50 aria-disabled:pointer-events-none disabled:opacity-50 disabled:pointer-events-none w-full truncate;
|
||||
|
||||
&[aria-selected='true'] {
|
||||
@apply bg-[image:var(--check-icon)] bg-no-repeat bg-position-[center_right_0.5rem] bg-size-[0.875rem];
|
||||
}
|
||||
}
|
||||
[role='listbox'] [role='heading'] {
|
||||
@apply flex text-muted-foreground px-2 py-1.5 text-xs;
|
||||
}
|
||||
[role='listbox'] [role='group']:not(:has([role='option']:not([aria-hidden='true']))) {
|
||||
@apply hidden;
|
||||
}
|
||||
[role='separator'] {
|
||||
@apply border-border -mx-1 my-1;
|
||||
}
|
||||
> header {
|
||||
@apply flex h-9 items-center gap-2 border-b px-3 -mx-1 -mt-1 mb-1;
|
||||
|
||||
svg {
|
||||
@apply size-4 shrink-0 opacity-50;
|
||||
}
|
||||
input[role='combobox'] {
|
||||
@apply placeholder:text-muted-foreground flex h-10 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 {
|
||||
@apply flex items-center justify-center p-6 text-sm truncate;
|
||||
}
|
||||
[role='listbox'][data-empty]:not(:has([data-value]:not([aria-hidden='true'])))::before {
|
||||
@apply content-[attr(data-empty)];
|
||||
}
|
||||
[role='listbox']:not([data-empty]):not(:has([data-value]:not([aria-hidden='true'])))::before {
|
||||
@apply content-['No_results_found'];
|
||||
}
|
||||
}
|
||||
|
||||
&:not([data-select-initialized]) [role='option']:hover,
|
||||
&[data-select-initialized] [role='option'].active,
|
||||
&[data-select-initialized] [role='option']:focus-visible {
|
||||
@apply bg-accent text-accent-foreground;
|
||||
}
|
||||
}
|
||||
.dropdown-menu {
|
||||
[popover].popover {
|
||||
@apply p-1;
|
||||
min-width: anchor-size(width);
|
||||
|
||||
[role='menuitem'],
|
||||
[role='menuitemcheckbox'],
|
||||
[role='menuitemradio'] {
|
||||
@apply aria-hidden:hidden [&_svg]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none [&_svg]:shrink-0 [&_svg]:size-4 aria-disabled:opacity-50 aria-disabled:pointer-events-none disabled:opacity-50 disabled:pointer-events-none w-full truncate;
|
||||
@apply focus-visible:bg-accent focus-visible:text-accent-foreground hover:bg-accent hover:text-accent-foreground;
|
||||
&.active {
|
||||
@apply bg-accent text-accent-foreground;
|
||||
}
|
||||
}
|
||||
[role='menu'] [role='heading'] {
|
||||
@apply flex px-2 py-1.5 text-sm font-medium;
|
||||
}
|
||||
[role='separator'] {
|
||||
@apply border-border -mx-1 my-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
[popover].popover {
|
||||
@apply absolute inset-auto bg-popover text-popover-foreground overflow-x-hidden overflow-y-auto p-4 rounded-md border shadow-md z-50 visible opacity-0 scale-95 w-max transition-all transition-discrete;
|
||||
|
||||
&:not([data-side]),
|
||||
&[data-side='bottom'] {
|
||||
@apply -translate-y-2;
|
||||
}
|
||||
&[data-side='top'] {
|
||||
@apply translate-y-2;
|
||||
}
|
||||
&[data-side='left'] {
|
||||
@apply translate-x-2;
|
||||
}
|
||||
&[data-side='right'] {
|
||||
@apply -translate-x-2;
|
||||
}
|
||||
&:not([data-side]),
|
||||
&[data-side='bottom'] {
|
||||
@apply mt-1 top-[anchor(bottom)];
|
||||
}
|
||||
&[data-side='top'] {
|
||||
@apply mb-1 bottom-[anchor(top)];
|
||||
}
|
||||
&[data-side='left'] {
|
||||
@apply mr-1 right-[anchor(left)];
|
||||
}
|
||||
&[data-side='right'] {
|
||||
@apply ml-1 left-[anchor(right)];
|
||||
}
|
||||
&:not([data-side]),
|
||||
&[data-side='bottom'],
|
||||
&[data-side='top'] {
|
||||
&:not([data-align]),
|
||||
&[data-align='start'] {
|
||||
@apply left-[anchor(left)];
|
||||
}
|
||||
&[data-align='end'] {
|
||||
@apply right-[anchor(right)];
|
||||
}
|
||||
&[data-align='center'] {
|
||||
@apply left-[anchor(center)] -translate-x-1/2;
|
||||
}
|
||||
}
|
||||
&[data-side='left'],
|
||||
&[data-side='right'] {
|
||||
&:not([data-align]),
|
||||
&[data-align='start'] {
|
||||
@apply top-[anchor(top)];
|
||||
}
|
||||
&[data-align='end'] {
|
||||
@apply bottom-[anchor(bottom)];
|
||||
}
|
||||
&[data-align='center'] {
|
||||
@apply top-[anchor(center)] -translate-y-1/2;
|
||||
}
|
||||
}
|
||||
&:is(:popover-open) {
|
||||
@apply opacity-100 scale-100 translate-x-0 translate-y-0;
|
||||
|
||||
@starting-style {
|
||||
@apply opacity-0 scale-95;
|
||||
|
||||
&:not([data-side]),
|
||||
&[data-side='bottom'] {
|
||||
@apply -translate-y-2;
|
||||
}
|
||||
&[data-side='top'] {
|
||||
@apply translate-y-2;
|
||||
}
|
||||
&[data-side='left'] {
|
||||
@apply translate-x-2;
|
||||
}
|
||||
&[data-side='right'] {
|
||||
@apply -translate-x-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
window.basecoat = window.basecoat || {};
|
||||
window.basecoat.registerDropdownMenu = function(Alpine) {
|
||||
if (Alpine.components && Alpine.components.dropdownMenu) return;
|
||||
|
||||
Alpine.data('dropdownMenu', () => ({
|
||||
open: false,
|
||||
focusedIndex: null,
|
||||
menuItems: [],
|
||||
|
||||
init() {
|
||||
this.$nextTick(() => {
|
||||
this.menuItems = Array.from(this.$el.querySelectorAll('[role=menuitem]:not([disabled]),[role=menuitemcheckbox]:not([disabled]),[role=menuitemradio]: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],[role=menuitemcheckbox],[role=menuitemradio]');
|
||||
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],[role=menuitemcheckbox],[role=menuitemradio]');
|
||||
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) },
|
||||
'@mouseover'(e) { this.handleMenuitemMousemove(e) },
|
||||
':aria-hidden'() { return !this.open },
|
||||
'x-cloak': ''
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
window.basecoat = window.basecoat || {};
|
||||
window.basecoat.registerSelect = function(Alpine) {
|
||||
if (Alpine.components && Alpine.components.select) return;
|
||||
|
||||
Alpine.data('select', (initialValue = null) => ({
|
||||
open: false,
|
||||
options: [],
|
||||
disabledOptions: [],
|
||||
focusedIndex: null,
|
||||
selectedLabel: null,
|
||||
selectedValue: null,
|
||||
query: '',
|
||||
|
||||
init() {
|
||||
this.$nextTick(() => {
|
||||
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);
|
||||
} 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;
|
||||
},
|
||||
selectValue(value, dispatch = true) {
|
||||
if (this.options.length === 0 || value == null) return;
|
||||
|
||||
const option = this.options.find(opt => opt.getAttribute('data-value') === String(value));
|
||||
if (option) {
|
||||
this.selectOption(option, dispatch);
|
||||
}
|
||||
},
|
||||
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) },
|
||||
'@mouseover'(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() },
|
||||
}
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
window.basecoat = window.basecoat || {};
|
||||
window.basecoat.registerSidebar= function(Alpine) {
|
||||
if (Alpine.components && Alpine.components.sidebar) return;
|
||||
|
||||
Alpine.data('sidebar', (initialOpen = true, initialMobileOpen = false) => ({
|
||||
open: window.innerWidth >= 768
|
||||
? initialOpen
|
||||
: initialMobileOpen,
|
||||
|
||||
init() {
|
||||
this.$nextTick(() => {
|
||||
this.$el.removeAttribute('data-uninitialized');
|
||||
});
|
||||
},
|
||||
|
||||
$main: {
|
||||
'@sidebar:open.window'(e) { this.open = true },
|
||||
'@sidebar:close.window'(e) { this.open = false },
|
||||
'@sidebar:toggle.window'(e) { this.open = !this.open },
|
||||
'@click'(e) { if (e.target === this.$el) this.open = false },
|
||||
':aria-hidden'() { return !this.open },
|
||||
':inert'() { return !this.open }
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
window.basecoat = window.basecoat || {};
|
||||
window.basecoat.registerTabs = function(Alpine) {
|
||||
if (Alpine.components && Alpine.components.tabs) return;
|
||||
|
||||
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() },
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
window.basecoat = window.basecoat || {};
|
||||
window.basecoat.registerToast = function(Alpine) {
|
||||
if (Alpine.components && Alpine.components.toast) return;
|
||||
|
||||
Alpine.store('toaster', { isPaused: false });
|
||||
Alpine.data('toast', (config={}) => ({
|
||||
config: config,
|
||||
open: false,
|
||||
timeoutDuration: null,
|
||||
timeoutId: null,
|
||||
|
||||
init() {
|
||||
if (config.duration !== -1) {
|
||||
this.timeoutDuration = config.duration || (config.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();
|
||||
},
|
||||
executeAction(actionString) {
|
||||
if (actionString) {
|
||||
Alpine.evaluate(this.$el, actionString);
|
||||
}
|
||||
},
|
||||
|
||||
$toastBindings: {
|
||||
['@mouseenter']() { this.$store.toaster.isPaused = true },
|
||||
['@mouseleave']() { this.$store.toaster.isPaused = false },
|
||||
['@keydown.escape.prevent']() { this.close() },
|
||||
[':aria-hidden']() { return !this.open }
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.magic('toast', (el) => (config, toasterId='toaster') => {
|
||||
const toaster = document.getElementById(toasterId);
|
||||
const template = document.getElementById('toast-template');
|
||||
|
||||
if (!toaster) {
|
||||
console.error(`Toaster container with id #${toasterId} not found.`);
|
||||
return;
|
||||
}
|
||||
if (!template) {
|
||||
console.error('Toast template with id #toast-template not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const clone = template.content.firstElementChild.cloneNode(true);
|
||||
|
||||
clone.setAttribute('x-data', `toast(${JSON.stringify(config)})`);
|
||||
clone.removeAttribute('id');
|
||||
|
||||
toaster.appendChild(clone);
|
||||
});
|
||||
};
|
||||
+85
-146
@@ -1,156 +1,95 @@
|
||||
window.basecoat = window.basecoat || {};
|
||||
window.basecoat.registerSelect = function(Alpine) {
|
||||
if (Alpine.components && Alpine.components.select) return;
|
||||
|
||||
Alpine.data('select', (initialValue = null) => ({
|
||||
open: false,
|
||||
options: [],
|
||||
disabledOptions: [],
|
||||
focusedIndex: null,
|
||||
selectedLabel: null,
|
||||
selectedValue: null,
|
||||
query: '',
|
||||
(() => {
|
||||
const initSelect = (select) => {
|
||||
const trigger = select.querySelector(':scope > [popovertarget]');
|
||||
const selectedValue = trigger.querySelector(':scope > span');
|
||||
const popover = select.querySelector(':scope > [popover]');
|
||||
const listbox = popover.querySelector('[role="listbox"]');
|
||||
const input = select.querySelector(':scope > input[type="hidden"]');
|
||||
if (!trigger || !popover || !listbox || !input) return;
|
||||
|
||||
init() {
|
||||
this.$nextTick(() => {
|
||||
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);
|
||||
} else {
|
||||
this.selectOption(this.options[0], false);
|
||||
}
|
||||
});
|
||||
},
|
||||
focusOption() {
|
||||
if (this.options.length === 0) return;
|
||||
const options = Array.from(listbox.querySelectorAll('[role="option"]'));
|
||||
let activeIndex = -1;
|
||||
|
||||
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;
|
||||
},
|
||||
selectValue(value, dispatch = true) {
|
||||
if (this.options.length === 0 || value == null) return;
|
||||
|
||||
const option = this.options.find(opt => opt.getAttribute('data-value') === String(value));
|
||||
const updateValue = (option) => {
|
||||
if (option) {
|
||||
this.selectOption(option, dispatch);
|
||||
}
|
||||
},
|
||||
selectOption(option, dispatch = true) {
|
||||
if (this.options.length === 0 || !option || option.disabled || option.getAttribute('data-value') == null) {
|
||||
return;
|
||||
selectedValue.innerHTML = option.dataset.label || option.innerHTML;
|
||||
input.value = option.dataset.value;
|
||||
listbox.querySelector('[role="option"][aria-selected="true"]')?.removeAttribute('aria-selected');
|
||||
option.setAttribute('aria-selected', 'true');
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
const selectOption = (option) => {
|
||||
if (!option) return;
|
||||
|
||||
updateValue(option);
|
||||
|
||||
trigger.removeAttribute('aria-activedescendant');
|
||||
options.forEach(opt => opt.classList.remove('active'));
|
||||
activeIndex = -1;
|
||||
popover.hidePopover();
|
||||
};
|
||||
|
||||
let initialOption = options.find(opt => input.value && opt.dataset.value === input.value);
|
||||
if (!initialOption && options.length > 0) initialOption = options[0];
|
||||
|
||||
updateValue(initialOption);
|
||||
|
||||
trigger.addEventListener('keydown', (e) => {
|
||||
if (!popover.matches(':popover-open')) return;
|
||||
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
|
||||
|
||||
if (e.key === 'ArrowDown') activeIndex = (activeIndex + 1) % options.length;
|
||||
else activeIndex = (activeIndex - 1 + options.length) % options.length;
|
||||
|
||||
const activeOption = options[activeIndex];
|
||||
if (activeOption) {
|
||||
activeOption.classList.add('active');
|
||||
if (activeOption.id) {
|
||||
trigger.setAttribute('aria-activedescendant', activeOption.id);
|
||||
}
|
||||
activeOption.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
selectOption(options[activeIndex]);
|
||||
}
|
||||
},
|
||||
filterOptions(query) {
|
||||
if (query.length > 0) {
|
||||
this.disabledOptions.forEach(opt => { opt.setAttribute('aria-hidden', 'true'); });
|
||||
} else {
|
||||
this.disabledOptions.forEach(opt => { opt.removeAttribute('aria-hidden'); });
|
||||
});
|
||||
|
||||
listbox.addEventListener('click', (e) => {
|
||||
const clickedOption = e.target.closest('[role="option"]');
|
||||
if (clickedOption) selectOption(clickedOption);
|
||||
});
|
||||
|
||||
popover.addEventListener('toggle', (e) => {
|
||||
if (e.newState === 'closed') {
|
||||
trigger.removeAttribute('aria-activedescendant');
|
||||
if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
|
||||
activeIndex = -1;
|
||||
}
|
||||
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');
|
||||
});
|
||||
|
||||
select.dataset.selectInitialized = true;
|
||||
};
|
||||
|
||||
document.querySelectorAll('div.select:not([data-select-initialized])').forEach(initSelect);
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
if (node.matches('div.select:not([data-select-initialized])')) {
|
||||
initSelect(node);
|
||||
}
|
||||
node.querySelectorAll('div.select:not([data-select-initialized])').forEach(initSelect);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$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) },
|
||||
'@mouseover'(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() },
|
||||
}
|
||||
}));
|
||||
};
|
||||
// Start observing the whole document
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
})();
|
||||
+64
-21
@@ -1,25 +1,68 @@
|
||||
window.basecoat = window.basecoat || {};
|
||||
window.basecoat.registerSidebar= function(Alpine) {
|
||||
if (Alpine.components && Alpine.components.sidebar) return;
|
||||
(() => {
|
||||
const initSidebar = (sidebarComponent) => {
|
||||
const initialOpen = sidebarComponent.dataset.initialOpen !== 'false';
|
||||
const initialMobileOpen = sidebarComponent.dataset.initialMobileOpen === 'true';
|
||||
const breakpoint = parseInt(sidebarComponent.dataset.breakpoint) || 768;
|
||||
|
||||
let open = breakpoint > 0
|
||||
? (window.innerWidth >= breakpoint ? initialOpen : initialMobileOpen)
|
||||
: initialOpen;
|
||||
|
||||
const updateState = () => {
|
||||
sidebarComponent.setAttribute('aria-hidden', !open);
|
||||
if (open) {
|
||||
sidebarComponent.removeAttribute('inert');
|
||||
} else {
|
||||
sidebarComponent.setAttribute('inert', '');
|
||||
}
|
||||
};
|
||||
|
||||
Alpine.data('sidebar', (initialOpen = true, initialMobileOpen = false) => ({
|
||||
open: window.innerWidth >= 768
|
||||
? initialOpen
|
||||
: initialMobileOpen,
|
||||
const setState = (state) => {
|
||||
open = state;
|
||||
updateState();
|
||||
};
|
||||
|
||||
init() {
|
||||
this.$nextTick(() => {
|
||||
this.$el.removeAttribute('data-uninitialized');
|
||||
const sidebarId = sidebarComponent.id;
|
||||
|
||||
window.addEventListener('sidebar:open', (e) => {
|
||||
if (!e.detail?.id || e.detail.id === sidebarId) setState(true);
|
||||
});
|
||||
window.addEventListener('sidebar:close', (e) => {
|
||||
if (!e.detail?.id || e.detail.id === sidebarId) setState(false);
|
||||
});
|
||||
window.addEventListener('sidebar:toggle', (e) => {
|
||||
if (!e.detail?.id || e.detail.id === sidebarId) setState(!open);
|
||||
});
|
||||
|
||||
sidebarComponent.addEventListener('click', (e) => {
|
||||
const nav = sidebarComponent.querySelector('nav');
|
||||
const closeTarget = e.target.closest('[data-sidebar-close-on-mobile]');
|
||||
|
||||
if (closeTarget && window.innerWidth < breakpoint) {
|
||||
setState(false);
|
||||
} else if (e.target === sidebarComponent || (nav && !nav.contains(e.target))) {
|
||||
setState(false);
|
||||
}
|
||||
});
|
||||
|
||||
updateState();
|
||||
sidebarComponent.dataset.sidebarInitialized = true;
|
||||
};
|
||||
|
||||
document.querySelectorAll('.sidebar:not([data-sidebar-initialized])').forEach(initSidebar);
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
if (node.matches('.sidebar:not([data-sidebar-initialized])')) {
|
||||
initSidebar(node);
|
||||
}
|
||||
node.querySelectorAll('.sidebar:not([data-sidebar-initialized])').forEach(initSidebar);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$main: {
|
||||
'@sidebar:open.window'(e) { this.open = true },
|
||||
'@sidebar:close.window'(e) { this.open = false },
|
||||
'@sidebar:toggle.window'(e) { this.open = !this.open },
|
||||
'@click'(e) { if (e.target === this.$el) this.open = false },
|
||||
':aria-hidden'() { return !this.open },
|
||||
':inert'() { return !this.open }
|
||||
},
|
||||
}));
|
||||
};
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
})();
|
||||
+71
-60
@@ -1,64 +1,75 @@
|
||||
window.basecoat = window.basecoat || {};
|
||||
window.basecoat.registerTabs = function(Alpine) {
|
||||
if (Alpine.components && Alpine.components.tabs) return;
|
||||
|
||||
Alpine.data('tabs', (initialTabIndex = 0) => ({
|
||||
activeTabIndex: initialTabIndex,
|
||||
tabs: [],
|
||||
panels: [],
|
||||
(() => {
|
||||
const initTabs = (tabsComponent) => {
|
||||
const tablist = tabsComponent.querySelector('[role="tablist"]');
|
||||
if (!tablist) return;
|
||||
|
||||
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);
|
||||
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
|
||||
const panels = tabs.map(tab => document.getElementById(tab.getAttribute('aria-controls'))).filter(Boolean);
|
||||
|
||||
const selectTab = (tabToSelect) => {
|
||||
tabs.forEach((tab, index) => {
|
||||
tab.setAttribute('aria-selected', 'false');
|
||||
tab.setAttribute('tabindex', '-1');
|
||||
if (panels[index]) panels[index].hidden = true;
|
||||
});
|
||||
|
||||
tabToSelect.setAttribute('aria-selected', 'true');
|
||||
tabToSelect.setAttribute('tabindex', '0');
|
||||
const activePanel = document.getElementById(tabToSelect.getAttribute('aria-controls'));
|
||||
if (activePanel) activePanel.hidden = false;
|
||||
};
|
||||
|
||||
tablist.addEventListener('click', (e) => {
|
||||
const clickedTab = e.target.closest('[role="tab"]');
|
||||
if (clickedTab) selectTab(clickedTab);
|
||||
});
|
||||
|
||||
tablist.addEventListener('keydown', (e) => {
|
||||
const currentTab = e.target;
|
||||
if (!tabs.includes(currentTab)) return;
|
||||
|
||||
let nextTab;
|
||||
const currentIndex = tabs.indexOf(currentTab);
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
nextTab = tabs[(currentIndex + 1) % tabs.length];
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
nextTab = tabs[(currentIndex - 1 + tabs.length) % tabs.length];
|
||||
break;
|
||||
case 'Home':
|
||||
nextTab = tabs[0];
|
||||
break;
|
||||
case 'End':
|
||||
nextTab = tabs[tabs.length - 1];
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
selectTab(nextTab);
|
||||
nextTab.focus();
|
||||
});
|
||||
|
||||
tabsComponent.dataset.tabsInitialized = true;
|
||||
};
|
||||
|
||||
document.querySelectorAll('.tabs:not([data-tabs-initialized])').forEach(initTabs);
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
if (node.matches('.tabs:not([data-tabs-initialized])')) {
|
||||
initTabs(node);
|
||||
}
|
||||
node.querySelectorAll('.tabs:not([data-tabs-initialized])').forEach(initTabs);
|
||||
}
|
||||
});
|
||||
},
|
||||
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() },
|
||||
},
|
||||
}));
|
||||
};
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
})();
|
||||
+51
-68
@@ -17,7 +17,7 @@
|
||||
@param close_on_overlay_click {boolean} [optional] [default=true] - Whether clicking the overlay closes the dialog.
|
||||
#}
|
||||
{% macro dialog(
|
||||
id,
|
||||
id=None,
|
||||
trigger=None,
|
||||
title=None,
|
||||
description=None,
|
||||
@@ -32,79 +32,62 @@
|
||||
close_button=true,
|
||||
close_on_overlay_click=true
|
||||
) %}
|
||||
<div
|
||||
{% set id = id or ("dialog-" + (range(100000, 999999) | random | string)) %}
|
||||
{% if trigger %}
|
||||
<button
|
||||
type="button"
|
||||
onClick="document.getElementById('{{ id }}').showModal()"
|
||||
{% for key, value in trigger_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ trigger }}
|
||||
</button>
|
||||
{% endif %}
|
||||
<dialog
|
||||
id="{{ id }}"
|
||||
x-data="dialog({{ 'true' if open else 'false' }}, {{ 'true' if close_on_overlay_click else 'false' }})"
|
||||
x-bind="$main"
|
||||
class="dialog"
|
||||
onClick="this.close()"
|
||||
{% for key, value in main_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{% if trigger %}
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded="false"
|
||||
aria-controls="{{ id }}-dialog"
|
||||
x-bind="$trigger"
|
||||
{% for key, value in trigger_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ trigger }}
|
||||
</button>
|
||||
{% endif %}
|
||||
<div
|
||||
role="dialog"
|
||||
id="{{ id }}-dialog"
|
||||
tabindex="-1"
|
||||
aria-modal="true"
|
||||
aria-labelledby="{{ id }}-title"
|
||||
inert
|
||||
x-bind="$content"
|
||||
>
|
||||
<article
|
||||
{% for key, value in content_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{% if title or description %}
|
||||
<header
|
||||
{% for key, value in content_header_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
<h2 id="{{ id }}-title">{{ title | safe }}</h2>
|
||||
{% if description %}<p>{{ description | safe }}</p>{% endif %}
|
||||
</header>
|
||||
{% endif %}
|
||||
{% if caller %}
|
||||
<section
|
||||
{% for key, value in content_body_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ caller() }}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if footer %}
|
||||
<footer
|
||||
{% for key, value in content_footer_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ footer | safe }}
|
||||
</footer>
|
||||
{% endif %}
|
||||
{% if close_button %}
|
||||
<button
|
||||
@click="hide()"
|
||||
aria-label="Close dialog"
|
||||
>
|
||||
<article onClick="event.stopPropagation()">
|
||||
{% if title or description %}
|
||||
<header
|
||||
{% for key, value in content_header_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
<h2 id="{{ id }}-title">{{ title | safe }}</h2>
|
||||
{% if description %}<p>{{ description | safe }}</p>{% endif %}
|
||||
</header>
|
||||
{% endif %}
|
||||
{% if caller %}
|
||||
<section
|
||||
{% for key, value in content_body_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ caller() }}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if footer %}
|
||||
<footer
|
||||
{% for key, value in content_footer_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ footer | safe }}
|
||||
</footer>
|
||||
{% endif %}
|
||||
{% if close_button %}
|
||||
<form method="dialog">
|
||||
<button aria-label="Close dialog">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</article>
|
||||
</dialog>
|
||||
{% endmacro %}
|
||||
@@ -10,50 +10,33 @@
|
||||
#}
|
||||
{% macro dropdown_menu(
|
||||
id=None,
|
||||
trigger=None,
|
||||
menu=None,
|
||||
main_attrs={},
|
||||
trigger,
|
||||
items=None,
|
||||
trigger_attrs={},
|
||||
content_attrs={}
|
||||
popover_attrs={}
|
||||
) %}
|
||||
<div
|
||||
class="popover {{ main_attrs.class }}"
|
||||
x-data="dropdownMenu"
|
||||
@click.away="open = false"
|
||||
{% if id %}id="{{ id }}"{% endif %}
|
||||
{% for key, value in main_attrs %}
|
||||
{% if key != "class" %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% if trigger %}
|
||||
{% set id = id or ("dropdown-menu-" + (range(100000, 999999) | random | string)) %}
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
type="button"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded="false"
|
||||
x-bind="$trigger"
|
||||
{% if id %}
|
||||
id="{{ id }}-trigger"
|
||||
aria-controls="{{ id }}-menu"
|
||||
{% endif %}
|
||||
popovertarget="{{ id }}"
|
||||
{% for key, value in trigger_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ trigger | safe }}
|
||||
</button>
|
||||
{% endif %}
|
||||
<div
|
||||
data-popover
|
||||
aria-hidden="true"
|
||||
x-bind="$content"
|
||||
{% if id %}id="{{ id }}-menu"{% endif %}
|
||||
{% for key, value in content_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
popover
|
||||
class="popover p-1 {% if popover_attrs.class %} {{ popover_attrs.class }}{% endif %}"
|
||||
id="{{ id }}"
|
||||
{% for key, value in popover_attrs %}
|
||||
{% if key != "class" %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<nav role="menu">
|
||||
{% if menu %}
|
||||
{{ render_dropdown_items(menu, id ~ "-items" if id else "items") }}
|
||||
{% if items %}
|
||||
{{ render_dropdown_items(items, id ~ "-items" if id else "items") }}
|
||||
{% else %}
|
||||
{{ caller() if caller }}
|
||||
{% endif %}
|
||||
|
||||
+20
-38
@@ -9,47 +9,29 @@
|
||||
#}
|
||||
{% macro popover(
|
||||
id=None,
|
||||
trigger=None,
|
||||
main_attrs={},
|
||||
trigger,
|
||||
trigger_attrs={},
|
||||
content_attrs={}
|
||||
popover_attrs={}
|
||||
) %}
|
||||
<div
|
||||
class="popover {{ main_attrs.class }}"
|
||||
x-data="popover"
|
||||
@click.away="open = false"
|
||||
{% if id %}id="{{ id }}"{% endif %}
|
||||
{% for key, value in main_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% set id = id or ("popover-" + (range(100000, 999999) | random | string)) %}
|
||||
<button
|
||||
type="button"
|
||||
popovertarget="{{ id }}"
|
||||
id="{{ id }}-trigger"
|
||||
{% for key, value in trigger_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{% if trigger %}
|
||||
<button
|
||||
type="button"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded="false"
|
||||
x-bind="$trigger"
|
||||
{% if id %}
|
||||
id="{{ id }}-trigger"
|
||||
aria-controls="{{ id }}-menu"
|
||||
{% endif %}
|
||||
{% for key, value in trigger_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ trigger | safe }}
|
||||
</button>
|
||||
{% endif %}
|
||||
<div
|
||||
data-popover
|
||||
aria-hidden="true"
|
||||
x-bind="$content"
|
||||
{% if id %}id="{{ id }}-menu"{% endif %}
|
||||
{% for key, value in content_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ caller() if caller }}
|
||||
</div>
|
||||
{{ trigger | safe }}
|
||||
</button>
|
||||
<div
|
||||
popover
|
||||
class="popover {{ popover_attrs.class }}"
|
||||
id="{{ id }}"
|
||||
{% for key, value in popover_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{{ caller() if caller }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
+55
-43
@@ -17,39 +17,53 @@
|
||||
selected=None,
|
||||
name=None,
|
||||
items=None,
|
||||
main_attrs={},
|
||||
trigger_attrs={},
|
||||
content_attrs={},
|
||||
popover_attrs={},
|
||||
listbox_attrs={},
|
||||
input_attrs={},
|
||||
search_placeholder="Search entries...",
|
||||
is_combobox=false
|
||||
) %}
|
||||
<div
|
||||
class="popover {{ main_attrs.class }}"
|
||||
x-data="select('{{ name }}', '{{ selected or '' }}')"
|
||||
@click.away="open = false"
|
||||
{% if id %}id="{{ id }}"{% endif %}
|
||||
{% for key, value in main_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% set id = id or ("select-" + (range(100000, 999999) | random | string)) %}
|
||||
|
||||
{% set first_option = [] %}
|
||||
{% set selected_option = [] %}
|
||||
|
||||
{% for item in items %}
|
||||
{% if item.type == "group" %}
|
||||
{% for sub_item in item.items %}
|
||||
{% if not first_option[0] %}
|
||||
{% set first_option = (first_option.push(sub_item), first_option) %}
|
||||
{% endif %}
|
||||
{% if selected and sub_item.value == selected and not selected_option[0] %}
|
||||
{% set selected_option = (selected_option.push(sub_item), selected_option) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% if not first_option[0] %}
|
||||
{% set first_option = (first_option.push(item), first_option) %}
|
||||
{% endif %}
|
||||
{% if selected and item.value == selected and not selected_option[0] %}
|
||||
{% set selected_option = (selected_option.push(item), selected_option) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% set default_option = selected_option[0] or first_option[0] or None %}
|
||||
|
||||
<div class="select">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline justify-between font-normal {{ trigger_attrs.class }}"
|
||||
id="{{ id }}-trigger"
|
||||
popovertarget="{{ id }}"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded="false"
|
||||
x-bind="$trigger"
|
||||
{% if id %}
|
||||
id="{{ id }}-trigger"
|
||||
aria-controls="{{ id }}-content"
|
||||
{% endif %}
|
||||
class="btn-outline justify-between font-normal {{ trigger_attrs.class }}"
|
||||
aria-controls="{{ id }}-listbox"
|
||||
{% for key, value in trigger_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<div x-html="selectedLabel" class="flex items-center gap-x-2"
|
||||
></div>
|
||||
<span class="truncate">{{ default_option.label }}</span>
|
||||
{% if is_combobox %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevrons-up-down-icon lucide-chevrons-up-down text-muted-foreground opacity-50 shrink-0"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
||||
{% else %}
|
||||
@@ -57,12 +71,11 @@
|
||||
{% endif %}
|
||||
</button>
|
||||
<div
|
||||
data-popover
|
||||
aria-hidden="true"
|
||||
x-bind="$content"
|
||||
{% if id %}id="{{ id }}-content"{% endif %}
|
||||
{% for key, value in content_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
popover
|
||||
id="{{ id }}"
|
||||
class="popover {{ popover_attrs.class }}"
|
||||
{% for key, value in popover_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% if is_combobox %}
|
||||
@@ -80,35 +93,33 @@
|
||||
aria-expanded="true"
|
||||
aria-controls="{{ id }}-content"
|
||||
aria-labelledby="{{ id }}-trigger"
|
||||
x-model="query"
|
||||
x-bind="$filter"
|
||||
>
|
||||
</header>
|
||||
{% endif %}
|
||||
<div
|
||||
role="listbox"
|
||||
id="{{ id }}-listbox"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="{{ id }}-trigger"
|
||||
{% for key, value in listbox_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{% if items %}
|
||||
{{ render_select_items(items, id ~ "-items" if id else "items") }}
|
||||
{% if items.length > 0 %}
|
||||
{{ render_select_items(items, default_option.value, id ~ "-items" if id else "items") }}
|
||||
{% else %}
|
||||
{{ caller() if caller }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
<input
|
||||
type="hidden"
|
||||
name="{{ name or id ~ '-value' }}"
|
||||
value="{{ selected or '' }}"
|
||||
{% for key, value in input_attrs %}
|
||||
{% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -118,10 +129,9 @@
|
||||
@param items {array} - The array of items to render.
|
||||
@param parent_id_prefix {string} [optional] - The prefix for the item id.
|
||||
#}
|
||||
{% macro render_select_items(items, parent_id_prefix="items") %}
|
||||
{% macro render_select_items(items, selected, parent_id_prefix="items") %}
|
||||
{% for item in items %}
|
||||
{% set item_id = parent_id_prefix ~ "-" ~ loop.index %}
|
||||
|
||||
{% if item.type == "group" %}
|
||||
{% set group_label_id = item.id if item.id else "group-label-" + item_id %}
|
||||
<div
|
||||
@@ -132,14 +142,16 @@
|
||||
{% endfor %}
|
||||
>
|
||||
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
|
||||
{{ render_select_items(item.items, item_id) if item.items }}
|
||||
{{ render_select_items(item.items, selected, item_id) if item.items }}
|
||||
</div>
|
||||
{% elif item.type == "separator" %}
|
||||
<hr role="separator" />
|
||||
{% elif item.type == "item" or not item.type %}
|
||||
<div
|
||||
id="{{ item_id }}"
|
||||
role="option"
|
||||
data-value="{{ item.value }}"
|
||||
{% if selected == item.value %}aria-selected="true"{% endif %}
|
||||
{% for key, value in item.attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
|
||||
@@ -23,18 +23,15 @@
|
||||
footer_attrs={},
|
||||
content_wrapper_attrs=None
|
||||
) %}
|
||||
<div
|
||||
<aside
|
||||
{% if id %}id="{{ id }}"{% endif %}
|
||||
class="sidebar {{ main_attrs.class }}"
|
||||
data-uninitialized
|
||||
data-side="{{ side if side else "left" }}"
|
||||
aria-hidden="{{ "true" if not open else "false" }}"
|
||||
{{ "inert" if not open }}
|
||||
{% for key, value in main_attrs %}
|
||||
{% if key != "class" %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
x-data="sidebar({{ "true" if open else "false" }})"
|
||||
x-bind="$main"
|
||||
>
|
||||
<nav
|
||||
aria-label="{{ label }}"
|
||||
@@ -71,7 +68,7 @@
|
||||
</footer>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
{% endmacro %}
|
||||
|
||||
{#
|
||||
|
||||
@@ -14,17 +14,17 @@
|
||||
@param default_tab_index {number} [optional] [default=1] - The 1-based index of the tab to be active initially.
|
||||
#}
|
||||
{% macro tabs(
|
||||
id,
|
||||
id=None,
|
||||
tabsets=[],
|
||||
main_attrs=None,
|
||||
tablist_attrs=None,
|
||||
default_tab_index=1
|
||||
)
|
||||
%}
|
||||
{% set id = id or ("tabs-" + (range(100000, 999999) | random | string)) %}
|
||||
<div
|
||||
class="tabs {{ main_attrs.class }}"
|
||||
x-data="tabs({{ default_tab_index - 1 }})"
|
||||
{% if id %}id="{{ id }}"{% endif %}
|
||||
id="{{ id }}"
|
||||
{% for key, value in main_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
@@ -32,7 +32,6 @@
|
||||
<nav
|
||||
role="tablist"
|
||||
aria-orientation="horizontal"
|
||||
x-bind="$tablist"
|
||||
{% for key, value in tablist_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
@@ -41,8 +40,8 @@
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="{{ tabset.id }}-tab-{{ loop.index }}"
|
||||
aria-controls="{{ tabset.id }}-panel-{{ loop.index }}"
|
||||
id="{{ id }}-tab-{{ loop.index }}"
|
||||
aria-controls="{{ id }}-panel-{{ loop.index }}"
|
||||
aria-selected="{{ 'true' if loop.index == default_tab_index else 'false' }}"
|
||||
tabindex="0"
|
||||
{% for key, value in tabset.tab_attrs %}
|
||||
@@ -58,8 +57,8 @@
|
||||
{% if tabset.panel %}
|
||||
<div
|
||||
role="tabpanel"
|
||||
id="{{ tabset.id }}-panel-{{ loop.index }}"
|
||||
aria-labelledby="{{ tabset.id }}-tab-{{ loop.index }}"
|
||||
id="{{ id }}-panel-{{ loop.index }}"
|
||||
aria-labelledby="{{ id }}-tab-{{ loop.index }}"
|
||||
tabindex="-1"
|
||||
aria-selected="{{ 'true' if loop.index == default_tab_index else 'false' }}"
|
||||
{% if loop.index != default_tab_index %}hidden{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user