mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-11 04:38:18 +00:00
Removing Alpine.js entirely
This commit is contained in:
@@ -75,8 +75,6 @@
|
||||
<script src="/assets/js/sidebar.js" defer></script>
|
||||
<script src="/assets/js/tabs.js" defer></script>
|
||||
<script src="/assets/js/toast.js" defer></script>
|
||||
{# Alpine.js #}
|
||||
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
{# HTMX #}
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
{# Highlight.js #}
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
{% macro code_block(code, language="html", class="my-4") %}
|
||||
<div
|
||||
class="relative {{ class }}"
|
||||
x-data="{
|
||||
copied: false,
|
||||
copy() {
|
||||
const codeElement = $refs.code;
|
||||
if (!codeElement) return;
|
||||
navigator.clipboard.writeText(codeElement.textContent || '').then(() => {
|
||||
this.copied = true;
|
||||
setTimeout(() => { this.copied = false }, 2000);
|
||||
<div class="relative {{ class }}">
|
||||
<pre class="grid text-sm max-h-[650px] overflow-y-auto rounded-xl scrollbar"><code class="language-{{ language }} !bg-muted !p-3.5">{{- code | escape -}}</code></pre>
|
||||
|
||||
<button
|
||||
onclick="
|
||||
const button = this;
|
||||
const code = this.parentElement.querySelector('pre code');
|
||||
if (!code) return;
|
||||
navigator.clipboard.writeText(code.textContent || '').then(() => {
|
||||
button.classList.add('copied');
|
||||
setTimeout(() => { button.classList.remove('copied') }, 2000);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy text: ', err);
|
||||
});
|
||||
}
|
||||
}"
|
||||
>
|
||||
<pre x-ref="code" class="grid text-sm max-h-[650px] overflow-y-auto rounded-xl scrollbar"><code class="language-{{ language }} !bg-muted !p-3.5">{{- code | escape -}}</code></pre>
|
||||
|
||||
<button
|
||||
@click="copy()"
|
||||
class="btn-icon-ghost size-8 absolute right-2.5 top-2 text-muted-foreground hover:text-foreground"
|
||||
"
|
||||
class="btn-icon-ghost size-8 absolute right-2.5 top-2 text-muted-foreground hover:text-foreground group"
|
||||
>
|
||||
{% lucide "clipboard", {"x-show": "!copied" } %}
|
||||
{% lucide "check", {"x-show": "copied" } %}
|
||||
{% lucide "clipboard", {"class": "group-[.copied]:hidden" } %}
|
||||
{% lucide "check", {"class": "hidden group-[.copied]:block" } %}
|
||||
</button>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -7,20 +7,7 @@
|
||||
</header>
|
||||
<div class="p-4">
|
||||
<div class="group grid w-full max-w-xl gap-4">
|
||||
<section
|
||||
x-init
|
||||
@click="
|
||||
const clickedSummary = $event.target.closest('summary');
|
||||
if (!clickedSummary) return;
|
||||
const clickedDetails = clickedSummary.closest('details');
|
||||
if (!clickedDetails) return;
|
||||
$el.querySelectorAll('details').forEach(detailsEl => {
|
||||
if (detailsEl !== clickedDetails) {
|
||||
detailsEl.removeAttribute('open');
|
||||
}
|
||||
});
|
||||
"
|
||||
>
|
||||
<section class="accordion">
|
||||
<details class="group border-b last:border-b-0">
|
||||
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
|
||||
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
|
||||
@@ -55,20 +42,7 @@
|
||||
</section>
|
||||
</details>
|
||||
</section>
|
||||
<section
|
||||
x-init
|
||||
@click="
|
||||
const clickedSummary = $event.target.closest('summary');
|
||||
if (!clickedSummary) return;
|
||||
const clickedDetails = clickedSummary.closest('details');
|
||||
if (!clickedDetails) return;
|
||||
$el.querySelectorAll('details').forEach(detailsEl => {
|
||||
if (detailsEl !== clickedDetails) {
|
||||
detailsEl.removeAttribute('open');
|
||||
}
|
||||
});
|
||||
"
|
||||
>
|
||||
<section class="accordion">
|
||||
<details class="group border-b last:border-b-0">
|
||||
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
|
||||
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
|
||||
@@ -92,6 +66,24 @@
|
||||
</section>
|
||||
</details>
|
||||
</section>
|
||||
<script>
|
||||
(() => {
|
||||
const accordions = document.querySelectorAll('.accordion');
|
||||
accordions.forEach(accordion => {
|
||||
accordion.addEventListener('click', (event) => {
|
||||
const summary = event.target.closest('summary');
|
||||
if (!summary) return;
|
||||
const details = summary.closest('details');
|
||||
if (!details) return;
|
||||
accordion.querySelectorAll('details').forEach(detailsEl => {
|
||||
if (detailsEl !== details) {
|
||||
detailsEl.removeAttribute('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -9,8 +9,8 @@
|
||||
</header>
|
||||
<div class="p-4">
|
||||
{% 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-demo').close()">Cancel</button>
|
||||
<button class="btn-primary" onclick="document.getElementById('alert-dialog-demo').close()">Continue</button>
|
||||
{% endset %}
|
||||
{{ dialog(
|
||||
id="alert-dialog-demo",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<div class="p-4">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
{% set footer %}
|
||||
<button class="btn-outline" @click="open = false">Cancel</button>
|
||||
<button class="btn" @click="open = false">Save changes</button>
|
||||
<button class="btn-outline" onclick="document.getElementById('demo-dialog-edit-profile').close()">Cancel</button>
|
||||
<button class="btn" onclick="document.getElementById('demo-dialog-edit-profile').close()">Save changes</button>
|
||||
{% endset %}
|
||||
{% call dialog(
|
||||
id="demo-dialog-edit-profile",
|
||||
|
||||
@@ -76,9 +76,6 @@
|
||||
role="menuitemcheckbox"
|
||||
aria-checked="true"
|
||||
class="group"
|
||||
x-data="{ checked: true }"
|
||||
@click="checked = !checked"
|
||||
:aria-checked="checked"
|
||||
>
|
||||
{% lucide "check", {
|
||||
"class": "invisible group-aria-checked:visible",
|
||||
@@ -93,9 +90,6 @@
|
||||
aria-checked="false"
|
||||
class="group"
|
||||
aria-disabled="true"
|
||||
x-data="{ checked: false }"
|
||||
@click="checked = !checked"
|
||||
:aria-checked="checked"
|
||||
>
|
||||
{% lucide "check", {
|
||||
"class": "invisible group-aria-checked:visible",
|
||||
@@ -109,9 +103,6 @@
|
||||
role="menuitemcheckbox"
|
||||
aria-checked="false"
|
||||
class="group"
|
||||
x-data="{ checked: false }"
|
||||
@click="checked = !checked"
|
||||
:aria-checked="checked"
|
||||
>
|
||||
{% lucide "check", {
|
||||
"class": "invisible group-aria-checked:visible",
|
||||
@@ -130,6 +121,19 @@
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const dropdownMenu = document.querySelector('#dropdown-menu-checkboxes');
|
||||
const checkboxes = dropdownMenu.querySelectorAll('div[role="menuitemcheckbox"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('click', () => {
|
||||
const isChecked = checkbox.getAttribute('aria-checked') === 'true';
|
||||
checkbox.setAttribute('aria-checked', !isChecked);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% 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"/>
|
||||
@@ -164,19 +168,18 @@
|
||||
{% endset %}
|
||||
|
||||
{% call dropdown_menu(
|
||||
id="dropdown-menu-radio-group",
|
||||
trigger="Radio Group",
|
||||
trigger_attrs={"class": "btn-outline"},
|
||||
popover_attrs={"class": "min-w-56"}
|
||||
) %}
|
||||
<div role="group" aria-labelledby="position-options" x-data="{ checked: 'bottom' }">
|
||||
<div role="group" aria-labelledby="position-options">
|
||||
<span id="position-options" role="heading">Panel Position</span>
|
||||
<hr role="separator">
|
||||
<div
|
||||
role="menuitemradio"
|
||||
aria-checked="false"
|
||||
class="group"
|
||||
@click="checked = 'top'"
|
||||
:aria-checked="checked == 'top'"
|
||||
>
|
||||
<div class="size-4 flex items-center justify-center">
|
||||
<div
|
||||
@@ -190,10 +193,8 @@
|
||||
</div>
|
||||
<div
|
||||
role="menuitemradio"
|
||||
aria-checked="false"
|
||||
aria-checked="true"
|
||||
class="group"
|
||||
@click="checked = 'bottom'"
|
||||
:aria-checked="checked == 'bottom'"
|
||||
>
|
||||
<div class="size-4 flex items-center justify-center">
|
||||
<div
|
||||
@@ -209,8 +210,6 @@
|
||||
role="menuitemradio"
|
||||
aria-checked="false"
|
||||
class="group"
|
||||
@click="checked = 'right'"
|
||||
:aria-checked="checked == 'right'"
|
||||
>
|
||||
<div class="size-4 flex items-center justify-center">
|
||||
<div
|
||||
@@ -224,6 +223,20 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endcall %}
|
||||
<script>
|
||||
(() => {
|
||||
const dropdownMenu = document.querySelector('#dropdown-menu-radio-group');
|
||||
const radioButtons = dropdownMenu.querySelectorAll('div[role="menuitemradio"]');
|
||||
radioButtons.forEach(radioButton => {
|
||||
radioButton.addEventListener('click', () => {
|
||||
radioButtons.forEach(radioButton => {
|
||||
radioButton.setAttribute('aria-checked', 'false');
|
||||
});
|
||||
radioButton.setAttribute('aria-checked', 'true');
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% set trigger %}
|
||||
<img alt="@hunvreus" src="https://github.com/hunvreus.png" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
</section>
|
||||
<script>
|
||||
(() => {
|
||||
const sliderEl = document.querySelector('input[type="range"].input');
|
||||
if (!sliderEl) return;
|
||||
const sliders = document.querySelectorAll('input[type="range"].input');
|
||||
if (!sliders) return;
|
||||
|
||||
const updateSlider = (el) => {
|
||||
const min = parseFloat(el.min || 0);
|
||||
@@ -30,7 +30,9 @@
|
||||
el.style.setProperty('--slider-value', `${percent}%`);
|
||||
};
|
||||
|
||||
updateSlider(sliderEl);
|
||||
sliderEl.addEventListener('input', (event) => updateSlider(event.target));
|
||||
sliders.forEach(slider => {
|
||||
updateSlider(slider);
|
||||
slider.addEventListener('input', (event) => updateSlider(event.target));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -1062,6 +1062,16 @@
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
.group-\[\.copied\]\:block {
|
||||
&:is(:where(.group):is(.copied) *) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.group-\[\.copied\]\:hidden {
|
||||
&:is(:where(.group):is(.copied) *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.peer-checked\:border-primary {
|
||||
&:is(:where(.peer):checked ~ *) {
|
||||
border-color: var(--color-primary);
|
||||
|
||||
@@ -14,20 +14,7 @@ toc:
|
||||
<h2>There is no dedicated accordion component in Basecoat.</h2>
|
||||
</div>
|
||||
|
||||
{% set code %}<section
|
||||
x-init
|
||||
@click="
|
||||
const clickedSummary = $event.target.closest('summary');
|
||||
if (!clickedSummary) return;
|
||||
const clickedDetails = clickedSummary.closest('details');
|
||||
if (!clickedDetails) return;
|
||||
$el.querySelectorAll('details').forEach(detailsEl => {
|
||||
if (detailsEl !== clickedDetails) {
|
||||
detailsEl.removeAttribute('open');
|
||||
}
|
||||
});
|
||||
"
|
||||
>
|
||||
{% set code %}<section class="accordion">
|
||||
<details class="group border-b last:border-b-0 ">
|
||||
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
|
||||
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
|
||||
@@ -62,6 +49,24 @@ toc:
|
||||
</section>
|
||||
</details>
|
||||
</section>
|
||||
<script>
|
||||
(() => {
|
||||
const accordions = document.querySelectorAll('.accordion');
|
||||
accordions.forEach(accordion => {
|
||||
accordion.addEventListener('click', (event) => {
|
||||
const summary = event.target.closest('summary');
|
||||
if (!summary) return;
|
||||
const details = summary.closest('details');
|
||||
if (!details) return;
|
||||
accordion.querySelectorAll('details').forEach(detailsEl => {
|
||||
if (detailsEl !== details) {
|
||||
detailsEl.removeAttribute('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endset %}
|
||||
{{ code_preview("accordion", code | prettyHtml, "w-full max-w-md") }}
|
||||
|
||||
|
||||
@@ -267,9 +267,6 @@ toc:
|
||||
role="menuitemcheckbox"
|
||||
aria-checked="true"
|
||||
class="group"
|
||||
x-data="{ checked: true }"
|
||||
@click="checked = !checked"
|
||||
:aria-checked="checked"
|
||||
>
|
||||
{% lucide "check", {
|
||||
"class": "invisible group-aria-checked:visible",
|
||||
@@ -283,10 +280,7 @@ toc:
|
||||
role="menuitemcheckbox"
|
||||
aria-checked="false"
|
||||
class="group"
|
||||
disabled
|
||||
x-data="{ checked: false }"
|
||||
@click="checked = !checked"
|
||||
:aria-checked="checked"
|
||||
aria-disabled="true"
|
||||
>
|
||||
{% lucide "check", {
|
||||
"class": "invisible group-aria-checked:visible",
|
||||
@@ -300,9 +294,6 @@ toc:
|
||||
role="menuitemcheckbox"
|
||||
aria-checked="false"
|
||||
class="group"
|
||||
x-data="{ checked: false }"
|
||||
@click="checked = !checked"
|
||||
:aria-checked="checked"
|
||||
>
|
||||
{% lucide "check", {
|
||||
"class": "invisible group-aria-checked:visible",
|
||||
@@ -319,7 +310,20 @@ toc:
|
||||
Logout
|
||||
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
|
||||
</div>
|
||||
{% endcall %}{% endset %}
|
||||
{% endcall %}
|
||||
<script>
|
||||
(() => {
|
||||
const dropdownMenu = document.querySelector('#dropdown-menu-checkboxes');
|
||||
const checkboxes = dropdownMenu.querySelectorAll('div[role="menuitemcheckbox"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('click', () => {
|
||||
const isChecked = checkbox.getAttribute('aria-checked') === 'true';
|
||||
checkbox.setAttribute('aria-checked', !isChecked);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endset %}
|
||||
{{ code_preview("dropdown-menu-checkboxes", code | prettyHtml) }}
|
||||
|
||||
<h3 id="example-radio-group"><a href="#example-radio-group">Radio Group</a></h3>
|
||||
@@ -330,15 +334,13 @@ toc:
|
||||
trigger_attrs={"class": "btn-outline"},
|
||||
popover_attrs={"class": "min-w-56"}
|
||||
) %}
|
||||
<div role="group" aria-labelledby="position-options" x-data="{ checked: 'bottom' }">
|
||||
<div role="group" aria-labelledby="position-options">
|
||||
<span id="position-options" role="heading">Panel Position</span>
|
||||
<hr role="separator">
|
||||
<div
|
||||
role="menuitemradio"
|
||||
aria-checked="false"
|
||||
class="group"
|
||||
@click="checked = 'top'"
|
||||
:aria-checked="checked == 'top'"
|
||||
>
|
||||
<div class="size-4 flex items-center justify-center">
|
||||
<div
|
||||
@@ -352,10 +354,8 @@ toc:
|
||||
</div>
|
||||
<div
|
||||
role="menuitemradio"
|
||||
aria-checked="false"
|
||||
aria-checked="true"
|
||||
class="group"
|
||||
@click="checked = 'bottom'"
|
||||
:aria-checked="checked == 'bottom'"
|
||||
>
|
||||
<div class="size-4 flex items-center justify-center">
|
||||
<div
|
||||
@@ -371,8 +371,6 @@ toc:
|
||||
role="menuitemradio"
|
||||
aria-checked="false"
|
||||
class="group"
|
||||
@click="checked = 'right'"
|
||||
:aria-checked="checked == 'right'"
|
||||
>
|
||||
<div class="size-4 flex items-center justify-center">
|
||||
<div
|
||||
@@ -385,5 +383,20 @@ toc:
|
||||
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endcall %}{% endset %}
|
||||
{% endcall %}
|
||||
<script>
|
||||
(() => {
|
||||
const dropdownMenu = document.querySelector('#dropdown-menu-radio-group');
|
||||
const radioButtons = dropdownMenu.querySelectorAll('div[role="menuitemradio"]');
|
||||
radioButtons.forEach(radioButton => {
|
||||
radioButton.addEventListener('click', () => {
|
||||
radioButtons.forEach(radioButton => {
|
||||
radioButton.setAttribute('aria-checked', 'false');
|
||||
});
|
||||
radioButton.setAttribute('aria-checked', 'true');
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endset %}
|
||||
{{ code_preview("dropdown-menu", code | prettyHtml) }}
|
||||
@@ -16,18 +16,26 @@ toc:
|
||||
min="0"
|
||||
max="100"
|
||||
value="50"
|
||||
x-data="{
|
||||
updatePercent(el) {
|
||||
>
|
||||
<script>
|
||||
(() => {
|
||||
const sliders = document.querySelectorAll('input[type="range"].input');
|
||||
if (!sliders) 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}%`);
|
||||
}
|
||||
}"
|
||||
x-init="updatePercent($el)"
|
||||
@input="updatePercent($el)"
|
||||
>{% endset %}
|
||||
};
|
||||
|
||||
sliders.forEach(slider => {
|
||||
updateSlider(slider);
|
||||
slider.addEventListener('input', (event) => updateSlider(event.target));
|
||||
});
|
||||
})();
|
||||
</script>{% endset %}
|
||||
{{ code_preview("slider", code, "w-full max-w-sm") }}
|
||||
|
||||
<h2 id="usage"><a href="#usage">Usage</a></h2>
|
||||
|
||||
+9
-3
@@ -232,9 +232,15 @@ layout: layouts/layout.njk
|
||||
I can't log in.
|
||||
</div>
|
||||
</section>
|
||||
<footer class="flex items-center gap-2" x-data="{ message: '' }">
|
||||
<input type="text" class="input w-full" placeholder="Type your message here..." x-model="message" />
|
||||
<button type="button" class="btn-icon" :disabled="!message">
|
||||
<footer class="flex items-center gap-2">
|
||||
<input type="text" class="input w-full" placeholder="Type your message here..." oninput="
|
||||
if (this.value) {
|
||||
this.parentElement.querySelector('button').removeAttribute('disabled');
|
||||
} else {
|
||||
this.parentElement.querySelector('button').setAttribute('disabled', 'true');
|
||||
}
|
||||
"/>
|
||||
<button type="button" class="btn-icon" disabled>
|
||||
{% lucide "send"%}
|
||||
</button>
|
||||
</footer>
|
||||
|
||||
@@ -38,11 +38,11 @@
|
||||
@param description {string} [optional] - The secondary description text.
|
||||
@param action {object} [optional] - Defines an action button.
|
||||
- label {string}: Button text.
|
||||
- click {string}: JavaScript code to execute on click (e.g., '$dispatch(\'custom-event\')').
|
||||
- onclick {string}: JavaScript code to execute on click.
|
||||
- url {string}: URL for an anchor link button.
|
||||
@param cancel {object} [optional] - Defines a cancel/dismiss button (similar structure to action).
|
||||
- label {string}: Button text.
|
||||
- click {string}: JavaScript code to execute on click (e.g., '$dispatch(\'custom-event\')').
|
||||
- onclick {string}: JavaScript code to execute on click.
|
||||
- url {string}: URL for an anchor link button.
|
||||
#}
|
||||
{% macro toast(
|
||||
|
||||
@@ -38,11 +38,11 @@
|
||||
@param description {string} [optional] - The secondary description text.
|
||||
@param action {object} [optional] - Defines an action button.
|
||||
- label {string}: Button text.
|
||||
- click {string}: JavaScript code to execute on click (e.g., '$dispatch(\'custom-event\')').
|
||||
- onclick {string}: JavaScript code to execute on click.
|
||||
- url {string}: URL for an anchor link button.
|
||||
@param cancel {object} [optional] - Defines a cancel/dismiss button (similar structure to action).
|
||||
- label {string}: Button text.
|
||||
- click {string}: JavaScript code to execute on click (e.g., '$dispatch(\'custom-event\')').
|
||||
- onclick {string}: JavaScript code to execute on click.
|
||||
- url {string}: URL for an anchor link button.
|
||||
#}
|
||||
{% macro toast(
|
||||
|
||||
Reference in New Issue
Block a user