mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-11 04:38:18 +00:00
PoC for new components, including select
This commit is contained in:
@@ -5,6 +5,7 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default async function(eleventyConfig) {
|
||||
eleventyConfig.addWatchTarget("src/nunjucks/");
|
||||
eleventyConfig.setInputDirectory("./docs/src");
|
||||
eleventyConfig.addPassthroughCopy({"docs/src/assets": "assets"});
|
||||
eleventyConfig.addPassthroughCopy({ "src/js": "assets/js" });
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Geist+Mono&display=swap");
|
||||
|
||||
@import "tailwindcss";
|
||||
@import "../../src/css/basecoat.css";
|
||||
@import "./custom.css";
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Geist';
|
||||
--font-mono: 'Geist Mono';
|
||||
}
|
||||
|
||||
@import "./themes/claude.css";
|
||||
@import "./themes/doom-64.css";
|
||||
@import "./themes/supabase.css";
|
||||
@@ -78,24 +78,12 @@
|
||||
<link rel="stylesheet" href="/assets/styles.css">
|
||||
|
||||
<!-- JS -->
|
||||
<script src="/assets/js/dialog.js" defer></script>
|
||||
<script src="/assets/js/dropdown-menu.js" defer></script>
|
||||
<script src="/assets/js/popover.js" defer></script>
|
||||
<script src="/assets/js/toast.js" defer></script>
|
||||
|
||||
<script src="/assets/js/select.js" defer></script>
|
||||
<script src="/assets/js/sidebar.js" defer></script>
|
||||
<script src="/assets/js/tabs.js" defer></script>
|
||||
<script src="/assets/js/toast.js" defer></script>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
window.basecoat.registerDialog(Alpine);
|
||||
window.basecoat.registerDropdownMenu(Alpine);
|
||||
window.basecoat.registerPopover(Alpine);
|
||||
window.basecoat.registerSelect(Alpine);
|
||||
window.basecoat.registerSidebar(Alpine);
|
||||
window.basecoat.registerTabs(Alpine);
|
||||
window.basecoat.registerToast(Alpine);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
] %}
|
||||
|
||||
{{ tabs(
|
||||
id="code-block-{{ id }}",
|
||||
tabsets=tabsets,
|
||||
default_tab_index=(2 if default_code else 1),
|
||||
register_on=["alpine:init", "htmx:afterSwap"]
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
<div class="flex h-14 w-full items-center gap-2 px-4">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Hide sidebar"
|
||||
data-tooltip="Hide sidebar"
|
||||
:data-tooltip="sidebarOpen ? 'Hide sidebar' : 'Show sidebar'"
|
||||
onclick="window.dispatchEvent(new CustomEvent('sidebar:toggle'))"
|
||||
aria-label="Toggle sidebar"
|
||||
data-tooltip="Toggle sidebar"
|
||||
data-side="bottom"
|
||||
data-align="start"
|
||||
class="btn-sm-icon-ghost mr-auto size-7 -ml-1.5"
|
||||
x-init
|
||||
x-data="{ sidebarOpen: true }"
|
||||
@click="
|
||||
sidebarOpen = !sidebarOpen;
|
||||
$dispatch('sidebar:toggle');
|
||||
"
|
||||
>
|
||||
{% lucide "panel-left" %}
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% from "popover.njk" import popover %}
|
||||
{% from "dropdown-menu.njk" import dropdown_menu %}
|
||||
|
||||
<section id="breadcrumb" class="w-full rounded-lg border scroll-mt-14">
|
||||
<header class="border-b px-4 py-3 flex items-center justify-between">
|
||||
@@ -17,17 +17,17 @@
|
||||
{% set trigger %}
|
||||
{% lucide "ellipsis" %}
|
||||
{% endset %}
|
||||
{% call popover(
|
||||
{{ dropdown_menu(
|
||||
id="demo-breadcrumb-menu",
|
||||
trigger=trigger,
|
||||
trigger_attrs={"class": "flex size-9 items-center justify-center h-4 w-4 hover:text-foreground cursor-pointer"}
|
||||
) %}
|
||||
<nav role="menu">
|
||||
<button type="button" role="menuitem">Documentation</button>
|
||||
<button type="button" role="menuitem">Themes</button>
|
||||
<button type="button" role="menuitem">GitHub</button>
|
||||
</nav>
|
||||
{% endcall %}
|
||||
trigger_attrs={"class": "flex size-9 items-center justify-center h-4 w-4 hover:text-foreground cursor-pointer"},
|
||||
popover_attrs={"class": "p-1"},
|
||||
items=[
|
||||
{ label: "Documentation" },
|
||||
{ label: "Themes" },
|
||||
{ label: "GitHub" }
|
||||
]
|
||||
) }}
|
||||
</li>
|
||||
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
|
||||
<li class="inline-flex items-center gap-1.5">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="p-4">
|
||||
<div class="flex flex-wrap items-start gap-4">
|
||||
{% call dropdown_menu(
|
||||
id="dropdown-menu",
|
||||
id="dropdown-menu-default",
|
||||
trigger="Open",
|
||||
trigger_attrs={"class": "btn-outline"},
|
||||
content_attrs={"class": "min-w-56"}
|
||||
@@ -134,8 +134,6 @@
|
||||
</button>
|
||||
{% endcall %}
|
||||
|
||||
|
||||
|
||||
{% set profile_menu %}
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<img alt="@hunvreus" src="https://github.com/hunvreus.png" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
|
||||
@@ -170,7 +168,6 @@
|
||||
{% endset %}
|
||||
|
||||
{% call dropdown_menu(
|
||||
id="dropdown-menu-radio-group",
|
||||
trigger="Radio Group",
|
||||
trigger_attrs={"class": "btn-outline"},
|
||||
content_attrs={"class": "min-w-56"},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
id="demo-popover",
|
||||
trigger="Open popover",
|
||||
trigger_attrs={"class": "btn-outline"},
|
||||
content_attrs={"class": "w-80 p-4"}
|
||||
popover_attrs={"class": "w-80"}
|
||||
) %}
|
||||
<div class="grid gap-4">
|
||||
<header class="grid gap-1.5">
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
</a>
|
||||
</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]">
|
||||
@@ -28,46 +27,70 @@
|
||||
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div class="flex flex-wrap items-center gap-2 md:flex-row">
|
||||
{% call select(trigger_attrs={"class": "w-[180px]"}) %}
|
||||
<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>
|
||||
<div role="group" aria-labelledby="vegetable-options-label">
|
||||
<span id="vegetable-options-label" role="heading">Grapes</span>
|
||||
<div role="option" data-value="pineapple">Pineapple</div>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{{ select(
|
||||
id="select-default",
|
||||
trigger_attrs={"class": "w-[180px]"},
|
||||
selected="blueberry",
|
||||
items=[
|
||||
{
|
||||
type: "group",
|
||||
label: "Fruits",
|
||||
items: [
|
||||
{ type: "item", label: "Apple", value: "apple" },
|
||||
{ type: "item", label: "Banana", value: "banana" },
|
||||
{ type: "item", label: "Blueberry Blueberry Blueberry Blueberry Blueberry", value: "blueberry" }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "group",
|
||||
label: "Grapes",
|
||||
items: [
|
||||
{ type: "item", label: "Pineapple", value: "pineapple" }
|
||||
]
|
||||
}
|
||||
]
|
||||
) }}
|
||||
{% call select(
|
||||
id="select-scrollbar",
|
||||
trigger_attrs={"class": "w-[180px]"},
|
||||
listbox_attrs={"class": "scrollbar overflow-y-auto max-h-64"}
|
||||
) %}
|
||||
{% for i in range(0, 99) %}
|
||||
<div role="option" data-value="item-{{ i }}">Item {{ i }}</div>
|
||||
{% endfor %}
|
||||
{% for i in range(0, 99) %}
|
||||
<div role="option" data-value="item-{{ i }}">Item {{ i }}</div>
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
|
||||
{% call select(trigger_attrs={"disabled": "disabled", "class": "w-[180px]"}) %}
|
||||
{% call select(
|
||||
id="select-disabled",
|
||||
trigger_attrs={"disabled": "disabled", "class": "w-[180px]"}
|
||||
) %}
|
||||
<div role="option" data-value="disabled">Disabled</div>
|
||||
{% endcall %}
|
||||
|
||||
|
||||
{% 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
|
||||
</div>
|
||||
<div type="button" role="option" data-value="line">
|
||||
{% lucide "chart-line", {"class": "text-muted-foreground"} %}
|
||||
Line
|
||||
</div>
|
||||
<div type="button" role="option" data-value="pie">
|
||||
{% lucide "chart-pie", {"class": "text-muted-foreground"} %}
|
||||
Pie
|
||||
</div>
|
||||
{% call select(
|
||||
id="select-with-icon",
|
||||
name="chart-type",
|
||||
trigger_attrs={'class': 'w-[180px]'}
|
||||
) %}
|
||||
<div type="button" role="option" data-value="bar">
|
||||
<span class="flex items-center gap-2">
|
||||
{% lucide "chart-bar", {"class": "text-muted-foreground"} %}
|
||||
Bar
|
||||
</span>
|
||||
</div>
|
||||
<div type="button" role="option" data-value="line">
|
||||
<span class="flex items-center gap-2">
|
||||
{% lucide "chart-line", {"class": "text-muted-foreground"} %}
|
||||
Line
|
||||
</span>
|
||||
</div>
|
||||
<div type="button" role="option" data-value="pie">
|
||||
<span class="flex items-center gap-2">
|
||||
{% lucide "chart-pie", {"class": "text-muted-foreground"} %}
|
||||
Pie
|
||||
</span>
|
||||
</div>
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,18 +13,24 @@
|
||||
min="0"
|
||||
max="27"
|
||||
value="12"
|
||||
x-data="{
|
||||
updatePercent(el) {
|
||||
const min = parseFloat(el.min || 0);
|
||||
const max = parseFloat(el.max || 100);
|
||||
const value = parseFloat(el.value);
|
||||
const percent = (max === min) ? 0 : ((value - min) / (max - min)) * 100;
|
||||
el.style.setProperty('--slider-value', `${percent}%`);
|
||||
}
|
||||
}"
|
||||
x-init="updatePercent($el)"
|
||||
@input="updatePercent($el)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<script>
|
||||
(() => {
|
||||
const sliderEl = document.querySelector('input[type="range"].input');
|
||||
if (!sliderEl) return;
|
||||
|
||||
const updateSlider = (el) => {
|
||||
const min = parseFloat(el.min || 0);
|
||||
const max = parseFloat(el.max || 100);
|
||||
const value = parseFloat(el.value);
|
||||
const percent = (max === min) ? 0 : ((value - min) / (max - min)) * 100;
|
||||
el.style.setProperty('--slider-value', `${percent}%`);
|
||||
};
|
||||
|
||||
updateSlider(sliderEl);
|
||||
sliderEl.addEventListener('input', (e) => updateSlider(e.target));
|
||||
})();
|
||||
</script>
|
||||
@@ -63,7 +63,7 @@
|
||||
"hx-select": "#content",
|
||||
"hx-target": "#content",
|
||||
"hx-swap": "outerHTML",
|
||||
"@click": "if (window.innerWidth < 768) { open = false }"
|
||||
"data-sidebar-close-on-mobile": ""
|
||||
} %}
|
||||
|
||||
{% set icon_home %}{% lucide "house" %}{% endset %}
|
||||
@@ -79,7 +79,7 @@
|
||||
{ label: "Kitchen sink", url: "/kitchen-sink", icon: icon_layout_dashboard, attrs: link_attrs },
|
||||
{ label: "GitHub", url: "https://github.com/hunvreus/basecoat", icon: icon_github, attrs: {
|
||||
"target": "_blank",
|
||||
"@click": "if (window.innerWidth < 768) { open = false }"
|
||||
"data-sidebar-close-on-mobile": ""
|
||||
}
|
||||
}
|
||||
]},
|
||||
@@ -117,6 +117,7 @@
|
||||
|
||||
|
||||
{{ sidebar(
|
||||
id="sidebar",
|
||||
label="Sidebar navigation",
|
||||
header=header,
|
||||
footer=footer,
|
||||
|
||||
+341
-448
File diff suppressed because it is too large
Load Diff
@@ -18,8 +18,8 @@ toc:
|
||||
|
||||
{% set code_html %}
|
||||
{% set footer %}
|
||||
<button class="btn-outline" @click="open = false">Cancel</button>
|
||||
<button class="btn-primary" @click="open = false">Continue</button>
|
||||
<button class="btn-outline" onClick="document.getElementById('alert-dialog').close()">Cancel</button>
|
||||
<button class="btn-primary" onClick="document.getElementById('alert-dialog').close()">Continue</button>
|
||||
{% endset %}
|
||||
{{ dialog(
|
||||
id="alert-dialog",
|
||||
@@ -105,8 +105,8 @@ document.addEventListener('alpine:init', () => {
|
||||
</div>
|
||||
|
||||
{% set raw_code %}{% raw %}{% set footer %}
|
||||
<button class="btn-outline" @click="open = false">Cancel</button>
|
||||
<button class="btn-primary" @click="open = false">Continue</button>
|
||||
<button class="btn-outline" onClick="document.getElementById('demo-alert-dialog').close()">Cancel</button>
|
||||
<button class="btn-primary" onClick="document.getElementById('demo-alert-dialog').close()">Continue</button>
|
||||
{% endset %}
|
||||
{{ dialog(
|
||||
id="demo-alert-dialog",
|
||||
|
||||
@@ -23,8 +23,8 @@ toc:
|
||||
|
||||
{% set code_html %}
|
||||
{% set footer %}
|
||||
<button class="btn-outline" @click="open = false">Cancel</button>
|
||||
<button class="btn" @click="open = false">Save changes</button>
|
||||
<button class="btn-outline" onClick="this.closest('dialog').close()">Cancel</button>
|
||||
<button class="btn" onClick="this.closest('dialog').close()">Save changes</button>
|
||||
{% endset %}
|
||||
{% call dialog(
|
||||
id="demo-dialog-edit-profile",
|
||||
@@ -175,7 +175,7 @@ document.addEventListener('alpine:init', () => {
|
||||
|
||||
{% set code %}
|
||||
{% set footer %}
|
||||
<button class="btn-outline" @click="open = false">Close</button>
|
||||
<button class="btn-outline" onClick="this.closest('dialog').close()">Close</button>
|
||||
{% endset %}
|
||||
{% call dialog(
|
||||
id="dialog-example",
|
||||
|
||||
@@ -21,8 +21,7 @@ toc:
|
||||
id="demo-popover",
|
||||
trigger="Open popover",
|
||||
trigger_attrs={"class": "btn-outline"},
|
||||
content_attrs={"class": "w-80 p-4"},
|
||||
register_on=["alpine:init", "htmx:afterSwap"]
|
||||
popover_attrs={"class": "w-80"}
|
||||
) %}
|
||||
<div class="grid gap-4">
|
||||
<header class="grid gap-1.5">
|
||||
@@ -52,13 +51,13 @@ toc:
|
||||
{% endset %}
|
||||
|
||||
{% set code_script %}
|
||||
<script>
|
||||
{# <script>
|
||||
{% fetchCode "src/js/popover.js" %}
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
window.basecoat.registerPopover(Alpine);
|
||||
});
|
||||
</script>
|
||||
</script> #}
|
||||
{% endset %}
|
||||
|
||||
{{ code_preview("popover", code_script | prettyHtml ~ code_html | prettyHtml) }}
|
||||
|
||||
@@ -34,34 +34,30 @@ toc:
|
||||
<option>Blueberry</option>
|
||||
<option>Grapes</option>
|
||||
<option>Pineapple</option>
|
||||
</optgroup>
|
||||
</select>{% endset %}
|
||||
{{ code_preview("select-native", code_native) }}
|
||||
|
||||
{% set code_html %}
|
||||
{% call select(
|
||||
name="select-js",
|
||||
{{ select(
|
||||
trigger_attrs={"class": "w-[180px]"},
|
||||
register_on=["alpine:init", "htmx:afterSwap"]
|
||||
) %}
|
||||
<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 %}
|
||||
items=[
|
||||
{
|
||||
type: "group",
|
||||
label: "Fruits",
|
||||
items: [
|
||||
{ type: "item", value: "apple", label: "Apple" },
|
||||
{ type: "item", value: "banana", label: "Banana" },
|
||||
{ type: "item", value: "blueberry", label: "Blueberry" },
|
||||
{ type: "item", value: "pineapple", label: "Grapes" },
|
||||
{ type: "item", value: "pineapple", label: "Pineapple" }
|
||||
]
|
||||
}
|
||||
]
|
||||
) }}
|
||||
{% endset %}
|
||||
|
||||
{% set code_script %}
|
||||
<script>
|
||||
{% fetchCode "src/js/select.js" %}
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
window.basecoat.registerSelect(Alpine);
|
||||
});
|
||||
</script>
|
||||
{% endset %}
|
||||
|
||||
{{ code_preview("select", code_script | prettyHtml ~ code_html | prettyHtml) }}
|
||||
@@ -153,16 +149,21 @@ document.addEventListener('alpine:init', () => {
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% set raw_code %}{% raw %}{% call select(name="select-js") %}
|
||||
<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 %}{% endraw %}{% endset %}
|
||||
{% set raw_code %}{% raw %}{{ select(
|
||||
items=[
|
||||
{
|
||||
type: "group",
|
||||
label: "Fruits",
|
||||
items: [
|
||||
{ type: "item", value: "apple", label: "Apple" },
|
||||
{ type: "item", value: "banana", label: "Banana" },
|
||||
{ type: "item", value: "blueberry", label: "Blueberry" },
|
||||
{ type: "item", value: "pineapple", label: "Grapes" },
|
||||
{ type: "item", value: "pineapple", label: "Pineapple" }
|
||||
]
|
||||
}
|
||||
]
|
||||
) }}{% endraw %}{% endset %}
|
||||
{{ code_block(raw_code, "jinja") }}
|
||||
|
||||
<h2 id="examples"><a href="#examples">Examples</a></h2>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
layout: layouts/base.njk
|
||||
---
|
||||
|
||||
|
||||
|
||||
<div id="my-popover" open popover="manual">
|
||||
SOMETHING
|
||||
SOMETHING
|
||||
SOMETHING
|
||||
SOMETHING
|
||||
SOMETHING
|
||||
</div>
|
||||
|
||||
<button popovertarget="my-popover">
|
||||
TOGGLE SIDEBAR
|
||||
</button>
|
||||
@@ -84,13 +84,13 @@ toc:
|
||||
|
||||
|
||||
{% set code_script %}
|
||||
<script>
|
||||
{# <script>
|
||||
{% fetchCode "src/js/popover.js" %}
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
window.basecoat.registerPopover(Alpine);
|
||||
});
|
||||
</script>
|
||||
</script> #}
|
||||
{% endset %}
|
||||
|
||||
{% set code_html_toaster %}{{ toaster() }}{% endset %}
|
||||
|
||||
+9
-8
@@ -32,16 +32,17 @@ layout: layouts/layout.njk
|
||||
<p class="text-sm text-muted-foreground">m@example.com</p>
|
||||
</div>
|
||||
|
||||
{% call select(
|
||||
{{ select(
|
||||
is_combobox=true,
|
||||
selected="owner",
|
||||
content_attrs={"data-align": "end"}
|
||||
) %}
|
||||
<div role="option" data-value="viewer">Viewer</div>
|
||||
<div role="option" data-value="developer">Developer</div>
|
||||
<div role="option" data-value="billing">Billing</div>
|
||||
<div role="option" data-value="owner">Owner</div>
|
||||
{% endcall %}
|
||||
popover_attrs={"data-align": "end"},
|
||||
items=[
|
||||
{ type: "item", value: "viewer", label: "Viewer" },
|
||||
{ type: "item", value: "developer", label: "Developer" },
|
||||
{ type: "item", value: "billing", label: "Billing" },
|
||||
{ type: "item", value: "owner", label: "Owner" }
|
||||
]
|
||||
) }}
|
||||
</li>
|
||||
<li class="flex items-center gap-4">
|
||||
<img src="/assets/images/avatar-2.png" alt="Jackson Lee" class="w-10 h-10 rounded-full" />
|
||||
|
||||
+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