Compare commits

..
25 Commits
Author SHA1 Message Date
Ronan Berder 1e1682df21 0.3.2 2025-08-18 17:12:47 +08:00
Ronan Berder d280e21aac Remove testing message 2025-08-18 10:01:17 +08:00
Ronan Berder a03e225bd7 Merge branch 'development' 2025-08-18 09:57:07 +08:00
Ronan Berder bd23745393 Solving merging conflict with #86 2025-08-18 09:57:00 +08:00
Ronan Berder 29600bd3ac Minor improvements to select 2025-08-18 09:52:09 +08:00
Ronan Berder 97a53f1a67 Merge pull request #59 from f15u/federico/fix-kitchen-sink
docs: fix label textarea
2025-08-18 09:39:30 +08:00
Ronan Berder ea4fd16b00 Merge pull request #68 from mtt-artis/patch-1
docs: typo
2025-08-18 09:02:23 +08:00
Ronan Berder c5db76d677 Merge pull request #86 from elanfranconi/elanfranconi/patch_closeToast_blur
Blur only occurs when the active element is inside the toast.
2025-08-18 08:48:50 +08:00
Elias Lanfranconi 246aa65860 Blur only occurs when the active element is inside the toast. 2025-08-10 14:09:05 +01:00
Ronan Berder 150c20fd67 Preventing change event from being triggered when initializing 2025-08-02 02:29:25 -06:00
Ronan Berder 81acdd18cc Fixing docs (typo and navigation) 2025-07-28 18:30:52 +08:00
Ronan Berder 16219ccae6 Improving registration and initialization of components, updating docs 2025-07-28 18:16:41 +08:00
Ronan Berder a2e9775a2c Fixing minor bugs and starting work on carousel 2025-07-13 10:39:13 +08:00
mtt 2a43d10da9 docs: typo 2025-07-12 15:54:10 +02:00
Federico Biccheddu 92ebe490b3 docs: fix label textarea 2025-07-01 23:18:14 +02:00
Ronan Berder 4a44656fd9 Bumping up version to 0.2.8 2025-06-30 19:48:12 +08:00
Ronan Berder 608e3b2197 Preventing change event from being dispatched if value hasn't changed 2025-06-30 19:46:59 +08:00
Ronan Berder a1a74c220f Adding sponsor link 2025-06-29 14:04:49 +08:00
Ronan Berder 55e3c78131 Adding contributing guidelines 2025-06-29 14:03:13 +08:00
Ronan Berder 113d34a842 Bumping version number to 0.2.7 after failed npm publish 2025-06-25 09:53:07 +08:00
Ronan Berder 972719f08f Fixing bug on hover for select 2025-06-25 09:49:01 +08:00
Ronan Berder d125153921 Bumping up package version 2025-06-25 09:40:59 +08:00
Ronan Berder 2389133e12 Handle mouse and keyboard navigation on select and dropdown 2025-06-25 09:27:29 +08:00
Ronan Berder 3f12f91765 Fixing form page 2025-06-24 22:18:37 +08:00
Ronan Berder ae4019a713 Adding theme switcher page to the docs 2025-06-24 21:14:19 +08:00
32 changed files with 610 additions and 358 deletions
+15
View File
@@ -0,0 +1,15 @@
# Contributing to Basecoat
- Support development by sponsoring the author: https://github.com/sponsors/hunvreus
- Submit pull requests (PRs) against the `development` branch, not `main`.
- For branches, we have:
- `main` is our production and default branch. This is what we deploy to https://basecoatui.com and publish to https://www.npmjs.com/package/basecoat-css and https://www.npmjs.com/package/basecoat-cli.
- `development` is for staging. We will deploy it soon at https://dev.basecoatui.com.
- New features are worked in `feature/name-of-the-feature` branches.
- Issues are addressed in `issue/123-main-isse` branches.
- When ready, we PR against `development`, test it and then finally merge to `main`.
- Keep changes focused: one feature or fix per PR.
- Test locally before submitting.
- Follow existing code style.
Thanks for helping!
+7
View File
@@ -69,6 +69,7 @@
{# CSS #}
<link rel="stylesheet" href="/assets/styles.css">
{# JS #}
<script src="/assets/js/basecoat.js" defer></script>
<script src="/assets/js/dropdown-menu.js" defer></script>
<script src="/assets/js/popover.js" defer></script>
<script src="/assets/js/select.js" defer></script>
@@ -94,6 +95,12 @@
document.addEventListener('htmx:afterSwap', highlight);
}
</script>
<script>
document.addEventListener('htmx:historyRestore', () => {
if (window.basecoat) window.basecoat.initAll();
if (window.Alpine) Alpine.initTree(document.body);
});
</script>
{% if head %}{{ head | safe }}{% endif %}
</head>
<body>
+1
View File
@@ -105,6 +105,7 @@
{ label: "Table", url: "/components/table", attrs: link_attrs },
{ label: "Tabs", url: "/components/tabs", attrs: link_attrs },
{ label: "Textarea", url: "/components/textarea", attrs: link_attrs },
{ label: "Theme Switcher", url: "/components/theme-switcher", attrs: link_attrs },
{ label: "Toast", url: "/components/toast", attrs: link_attrs },
{ label: "Tooltip", url: "/components/tooltip", attrs: link_attrs }
]}
+80 -63
View File
@@ -2465,61 +2465,61 @@
justify-content: flex-end;
}
}
> form[method='dialog'] {
> button, > form[method='dialog'] {
position: absolute;
top: calc(var(--spacing) * 4);
right: calc(var(--spacing) * 4);
> button {
border-radius: var(--radius-xs);
opacity: 70%;
--tw-ring-offset-color: var(--color-background);
transition-property: opacity;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
&:hover {
@media (hover: hover) {
opacity: 100%;
}
}
> button, > form[method='dialog'] > button {
border-radius: var(--radius-xs);
opacity: 70%;
--tw-ring-offset-color: var(--color-background);
transition-property: opacity;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
&:hover {
@media (hover: hover) {
opacity: 100%;
}
&:focus {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
&:focus {
--tw-ring-color: var(--color-ring);
}
&:focus {
--tw-ring-offset-width: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
}
&:focus {
--tw-outline-style: none;
outline-style: none;
@media (forced-colors: active) {
outline: 2px solid transparent;
outline-offset: 2px;
}
}
&:disabled {
pointer-events: none;
}
&[data-state="open"] {
background-color: var(--color-accent);
}
&[data-state="open"] {
color: var(--color-muted-foreground);
}
& svg {
pointer-events: none;
}
& svg {
flex-shrink: 0;
}
& svg:not([class*='size-']) {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
&:focus {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
&:focus {
--tw-ring-color: var(--color-ring);
}
&:focus {
--tw-ring-offset-width: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
}
&:focus {
--tw-outline-style: none;
outline-style: none;
@media (forced-colors: active) {
outline: 2px solid transparent;
outline-offset: 2px;
}
}
&:disabled {
pointer-events: none;
}
&[data-state="open"] {
background-color: var(--color-accent);
}
&[data-state="open"] {
color: var(--color-muted-foreground);
}
& svg {
pointer-events: none;
}
& svg {
flex-shrink: 0;
}
& svg:not([class*='size-']) {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
}
}
}
@@ -2580,16 +2580,6 @@
color: var(--color-muted-foreground);
}
&:not([aria-disabled='true']) {
&:hover {
@media (hover: hover) {
background-color: var(--color-accent);
}
}
&:hover {
@media (hover: hover) {
color: var(--color-accent-foreground);
}
}
&:focus-visible {
background-color: var(--color-accent);
}
@@ -2617,6 +2607,20 @@
border-color: var(--color-border);
}
}
&:not([data-dropdown-menu-initialized]) [data-popover] {
[role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio'] {
&:hover {
@media (hover: hover) {
background-color: var(--color-accent);
}
}
&:hover {
@media (hover: hover) {
color: var(--color-accent-foreground);
}
}
}
}
}
}
@layer components {
@@ -3193,6 +3197,10 @@
background-position: center right 0.5rem;
background-repeat: no-repeat;
}
&.active, &:focus-visible {
background-color: var(--color-accent);
color: var(--color-accent-foreground);
}
}
[role='listbox'] [role='heading'] {
display: flex;
@@ -3275,9 +3283,17 @@
content: var(--tw-content);
}
}
[role='option']:hover, &[data-select-initialized] [role='option'].active, &[data-select-initialized] [role='option']:focus-visible {
background-color: var(--color-accent);
color: var(--color-accent-foreground);
&:not([data-select-initialized]) [data-popover] [role='option'] {
&:hover {
@media (hover: hover) {
background-color: var(--color-accent);
}
}
&:hover {
@media (hover: hover) {
color: var(--color-accent-foreground);
}
}
}
}
}
@@ -4073,6 +4089,7 @@
letter-spacing: var(--tracking-tight);
}
p {
word-break: break-all;
color: var(--color-muted-foreground);
}
}
@@ -4118,7 +4135,7 @@
pointer-events: none;
visibility: hidden;
position: absolute;
z-index: 50;
z-index: 60;
width: fit-content;
max-width: var(--container-xs);
--tw-scale-x: 95%;
+102
View File
@@ -0,0 +1,102 @@
---
layout: layouts/page.njk
title: Carousel
description: A carousel with motion and swipe.
toc:
- label: Usage
id: usage
---
<style>
.slider {
width: 300px;
text-align: center;
overflow: hidden;
}
.slides {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
/*
scroll-snap-points-x: repeat(300px);
scroll-snap-type: mandatory;
*/
}
.slides::-webkit-scrollbar {
display: none;
}
.slides {
scrollbar-width: none; /* Firefox */
}
.slides > div {
scroll-snap-align: start;
flex-shrink: 0;
width: 300px;
height: 300px;
margin-right: 50px;
border-radius: 10px;
background: #eee;
transform-origin: center center;
transform: scale(1);
transition: transform 0.5s;
position: relative;
display: flex;
justify-content: center;
align-items: center;
font-size: 100px;
}
</style>
<div class="slider">
<a href="#slide-1">1</a>
<a href="#slide-2">2</a>
<a href="#slide-3">3</a>
<a href="#slide-4">4</a>
<a href="#slide-5">5</a>
<a href="#slide-6">6</a>
<a href="#slide-7">7</a>
<a href="#slide-8">8</a>
<a href="#slide-9">9</a>
<div class="slides">
<div id="slide-1">
1
</div>
<div id="slide-2">
2
</div>
<div id="slide-3">
3
</div>
<div id="slide-4">
4
</div>
<div id="slide-5">
5
</div>
<div id="slide-6">
6
</div>
<div id="slide-7">
7
</div>
<div id="slide-8">
8
</div>
<div id="slide-9">
9
</div>
</div>
</div>
+2 -1
View File
@@ -60,7 +60,8 @@ toc:
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/select.min.js" defer></script>{% endset %}
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/select.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
+2 -2
View File
@@ -85,8 +85,8 @@ toc:
<dd>For the content.</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Usually contains actions.</dd>
<dt><code class="highlight language-html">&lt;form method="dialog"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>A form that contains a single button to close the dialog.</dd>
<dt><code class="highlight language-html">&lt;button type="button" onclick="this.closest('dialog').close()"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>A button to close the dialog. Alternatively, you can wrap a button in a <code>&lt;form method="dialog"&gt;</code> element.</dd>
</dl>
</dd>
</dl>
+2 -1
View File
@@ -88,7 +88,8 @@ toc:
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/dropdown-menu.min.js" defer></script>{% endset %}
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/dropdown-menu.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
+3 -3
View File
@@ -1,7 +1,7 @@
---
layout: layouts/page.njk
title: Input
description: Displays a form input field or a component that looks like an input field.
title: Form
description: Building forms with Basecoat components.
toc:
- label: Usage
id: usage
@@ -28,7 +28,7 @@ toc:
</div>
<div class="grid gap-2">
<label for="demo-form-text">Bio</label>
<label for="demo-form-textarea">Bio</label>
<textarea id="demo-form-textarea" placeholder="I like to..." rows="3"></textarea>
<p class="text-muted-foreground text-sm">You can @mention other users and organizations.</p>
</div>
+2 -1
View File
@@ -71,7 +71,8 @@ toc:
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/popover.min.js" defer></script>{% endset %}
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/popover.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
+3 -2
View File
@@ -92,7 +92,8 @@ toc:
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/select.min.js" defer></script>{% endset %}
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/select.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
@@ -239,7 +240,7 @@ toc:
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>
<div role="option" {% if i > 0 %}data-value="item-{{ i }}"{% endif %}>Item {{ i }}</div>
{% endfor %}
{% endcall %}
{% endset %}
+4 -3
View File
@@ -34,7 +34,8 @@ toc:
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/sidebar.min.js" defer></script>{% endset %}
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/sidebar.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
@@ -165,7 +166,7 @@ toc:
{% set code_trigger %}<!-- Toggles the sidebar -->
<button type="button" onclick="document.dispatchEvent(new CustomEvent('basecoat:sidebar'));">Toggle sidebar</button>
<!-- Opens the `#main-navigation` sidebar -->
<button type="button" onclick="document.dispatchEvent(new CustomEvent('basecoat:sidebar', { detail: { id: 'main-navigation', action: 'close' } }));">Close sidebar</button>
<button type="button" onclick="document.dispatchEvent(new CustomEvent('basecoat:sidebar', { detail: { id: 'main-navigation', action: 'open' } }));">Open sidebar</button>
<!-- Closes the sidebar -->
<button type="button" onclick="document.dispatchEvent(new CustomEvent('basecoat:sidebar', { detail: { action: 'close' } }));">Close sidebar</button>{% endset %}
{{ code_block(code_trigger | prettyHtml, "html") }}
@@ -215,4 +216,4 @@ toc:
<h1>Content</h1>
</main>
{% endraw %}{% endset%}
{{ code_block(raw_code, "jinja") }}
{{ code_block(raw_code, "jinja") }}
+2 -1
View File
@@ -94,7 +94,8 @@ toc:
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/tabs.min.js" defer></script>{% endset %}
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/tabs.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
+86 -28
View File
@@ -5,49 +5,107 @@ description: A component that allows the user to switch between light and dark m
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Default
id: example-default
- label: Right
id: example-right
- label: Bottom + Start
id: example-bottom-start
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1: Include the JavaScript script"
id: usage-html-js-1
- label: "Step 2: Add your switcher"
id: usage-html-js-2
- label: JavaScript events
id: usage-html-js-3
---
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated theme switcher component in Basecoat.</h2>
<section><p>Instead, we offer a short JavaScript <code>&lt;script&gt;</code> block you can insert in the <code>&lt;head&gt;</code> of your page.</p></section>
</div>
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<button class="btn-outline" data-tooltip="Add to library">Hover</button>{% endset %}
{% set code %}<button
type="button"
aria-label="Toggle dark mode"
data-tooltip="Toggle dark mode"
data-side="bottom"
onclick="document.dispatchEvent(new CustomEvent('basecoat:theme'))"
class="btn-icon-outline size-8"
>
<span class="hidden dark:block">{% lucide "sun" %}</span>
<span class="block dark:hidden">{% lucide "moon" %}</span>
</button>{% endset %}
{{ code_preview("button", code) }}
<h2 id="usage">Usage</h2>
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript script</a></h4>
<section class="prose">
<p>On any element, set the value of the <code>data-tooltip</code> attribute to the text you want to display in the tooltip. Additionally, you can set:</p>
<ul>
<li><code>data-side</code> attribute to <code>top</code>, <code>bottom</code>, <code>left</code>, or <code>right</code> to change the side of the tooltip.</li>
<li><code>data-align</code> attribute to <code>start</code>, <code>center</code>, or <code>end</code> to change the alignment of the tooltip.</li>
</ul>
<p>Insert the following JavaScript block right at the beginning of the <code>&lt;head&gt;</code> your page. We do this instead of using a separate file to ensure the theme is defined before the page loads, avoiding a flash of unstyled or mist-styled content.</p>
</section>
{% set code %}<button class="btn-outline" data-tooltip="Tooltip text" data-side="bottom" data-align="center">Bottom left</button>{% endset %}
{% set code %}<script>
(() => {
try {
const stored = localStorage.getItem('themeMode');
if (stored ? stored === 'dark'
: matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
} catch (_) {}
const apply = dark => {
document.documentElement.classList.toggle('dark', dark);
try { localStorage.setItem('themeMode', dark ? 'dark' : 'light'); } catch (_) {}
};
document.addEventListener('basecoat:theme', (event) => {
const mode = event.detail?.mode;
apply(mode === 'dark' ? true
: mode === 'light' ? false
: !document.documentElement.classList.contains('dark'));
});
})();
</script>{% endset %}
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your switcher</a></h4>
<h3 id="example-default"><a href="#example-default">Default</a></h3>
<section class="prose">
<p>To change the theme, you need to dispatch a <code>basecoat:theme</code> event:</p>
</section>
{% set code %}<button class="btn-outline" data-tooltip="Default tooltip">Default</button>{% endset %}
{{ code_preview("button", code) }}
{% set code %}<button
type="button"
aria-label="Toggle dark mode"
data-tooltip="Toggle dark mode"
data-side="bottom"
onclick="document.dispatchEvent(new CustomEvent('basecoat:theme'))"
class="btn-icon-outline size-8"
>
<span class="hidden dark:block">{% lucide "sun" %}</span>
<span class="block dark:hidden">{% lucide "moon" %}</span>
</button>{% endset %}
{{ code_block(code) }}
<h3 id="example-right"><a href="#example-right">Right</a></h3>
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">JavaScript events</a></h4>
{% set code %}<button class="btn-outline" data-tooltip="Right side tooltip" data-side="right">Right</button>{% endset %}
{{ code_preview("button", code) }}
<h3 id="example-bottom-start"><a href="#example-bottom-start">Bottom + Start</a></h3>
{% set code %}<button class="btn-outline" data-tooltip="Bottom side and start aligned tooltip" data-side="bottom" data-align="start">Bottom + Start</button>{% endset %}
{{ code_preview("button", code) }}
<section class="prose">
<dl>
<dt><code>basecoat:theme</code></dt>
<dd>
<p>By default, the event will toggle the theme, but you can also add a <code>mode</code> to the detail to set the theme explicitly:</p>
{% set code_trigger %}<!-- Toggles the theme -->
<button type="button" onclick="document.dispatchEvent(new CustomEvent('basecoat:theme'));">Toggle theme</button>
<!-- Sets the theme to dark -->
<button type="button" onclick="document.dispatchEvent(new CustomEvent('basecoat:theme', { detail: { mode: 'dark' } }));">Set dark theme</button>{% endset %}
{{ code_block(code_trigger | prettyHtml, "html") }}
</dd>
</dl>
</section>
+2 -1
View File
@@ -69,7 +69,8 @@ toc:
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/toast.min.js" defer></script>{% endset %}
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/toast.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
+11 -4
View File
@@ -5,6 +5,11 @@ description: How to install and use Basecoat in your app.
toc:
- label: Install using the CDN
id: install-cdn
children:
- label: Install all components
id: install-cdn-all
- label: Install specific components
id: install-cdn-specific
- label: Installing using NPM
id: install-npm
children:
@@ -43,13 +48,14 @@ toc:
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/all.min.js" defer></script>{% endset %}
{{ code_block(code, "html") }}
<h3 id="install-cdn-all"><a href="#install-cdn-all">Install specific components</a></h3>
<h3 id="install-cdn-specific"><a href="#install-cdn-specific">Install specific components</a></h3>
<section class="prose">
<p>While the JavaScript file for all components is small (around 3kB gzipped), you can cherry-pick the components you need as instructed in the component's page (<a href="/components/dropdown-menu">Dropdown Menu</a>, <a href="/components/popover">Popover</a>, <a href="/components/select">Select</a>, <a href="/components/sidebar">Sidebar</a>, <a href="/components/tabs">Tabs</a> and <a href="/components/toast">Toast</a>). For example, to add the <a href="/components/dropdown-menu">Dropdown Menu</a> component, add the following to the <code>&lt;head&gt;</code> of your HTML file:</p>
</section>
{% set code %}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/basecoat.cdn.min.css">
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script> <!-- Only needed once to register and initialize components -->
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/dropdown-menu.min.js" defer></script>{% endset %}
{{ code_block(code, "html") }}
@@ -112,9 +118,10 @@ import 'basecoat-css/tabs';{% endraw %}{% endset %}
{% set code %}npx copyfiles -u 1 "node_modules/basecoat-css/dist/js/**/*" public/js/basecoat{% endset %}
{{ code_block(code, "bash") }}
<div class="prose">
<p>Then reference the script you need:</p>
<p>Then reference the script you need as well as the <code>basecoat.min.js</code> file (used to register and initialize components):</p>
</div>
{% set code %}<script src="/js/basecoat/tabs.min.js" defer></script>{% endset %}
{% set code %}<script src="/js/basecoat/basecoat.min.js" defer></script>
<script src="/js/basecoat/tabs.min.js" defer></script>{% endset %}
{{ code_block(code, "html") }}
<div class="prose">
<p>See each component page for the minimal script required.</p>
@@ -138,7 +145,7 @@ import 'basecoat-css/tabs';{% endraw %}{% endset %}
<ul>
<li><b>CDN</b>: you either <a href="#install-cdn-all">add the code for all of the components</a> to the <code>&lt;head&gt;</code> of your HTML file, or just the file for the component you want to use as instructed on the component's page.</li>
<li><b>Local file</b>: you download the script as a separate file and include it in your project (<a href="#install-cli">or let the CLI do it for you</a>). Once again, you have the choice to include the fiel for all components at once (<a><code>all.min.js</code> or <code>all.js</code>).</p>
<li><b>Local file</b>: you download the script as a separate file and include it in your project (<a href="#install-cli">or let the CLI do it for you</a>). Once again, you have the choice to include the file for all components at once (<a><code>all.min.js</code> or <code>all.js</code>).</p>
</ul>
</div>
+38 -70
View File
@@ -1,71 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anchor Positioning Test (Top, End)</title>
<script type="module">
if (!("anchorName" in document.documentElement.style)) {
console.log("Loading CSS anchor positioning polyfill...");
import("https://unpkg.com/@oddbird/css-anchor-positioning");
}
</script>
<style>
body {
font-family: sans-serif;
display: grid;
place-items: center;
height: 100vh;
margin: 0;
background: #222;
color: #eee;
}
---
layout: layouts/layout.njk
---
button {
padding: 0.5em 1em;
font-size: 1rem;
border-radius: 6px;
border: 1px solid #888;
background: #444;
color: #eee;
cursor: pointer;
}
[popover] {
border: 1px solid #888;
border-radius: 6px;
background: #333;
color: #eee;
padding: 1em;
margin: 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
/* The core anchor positioning logic */
#my-popover {
position: absolute;
inset: auto; /* Required to override default popover centering */
bottom: anchor(top); /* Position bottom of popover to top of anchor */
right: anchor(right); /* Align right edge of popover with right edge of anchor */
margin-bottom: 8px; /* A little space between popover and button */
}
</style>
</head>
<body>
<button
popovertarget="my-popover"
id="my-anchor-button"
style="anchor-name: --my-anchor;">
Click Me
</button>
<div
popover
id="my-popover"
style="position-anchor: --my-anchor;">
I should be anchored above the button, aligned to the right.
</div>
</body>
</html>
{% from "dialog.njk" import dialog %}
{% set footer %}
<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",
title="Edit profile",
description="Make changes to your profile here. Click save when you're done.",
trigger="Edit Profile",
trigger_attrs={"class": "btn-outline"},
dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
footer=footer
) %}
<form class="flex-col gap-4">
{% from "select.njk" import select %}
{{ select(
trigger_attrs={"class": "w-[180px]"},
is_combobox=true,
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" }
]
}
]
) }}
</form>
{% endcall %}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "basecoat",
"version": "0.2.5",
"version": "0.3.2",
"description": "shadcn/ui for the rest of us",
"author": "hunvreus",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "basecoat-cli",
"version": "0.2.5",
"version": "0.3.2",
"description": "Add Basecoat components to your project",
"author": "hunvreus",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "basecoat-css",
"version": "0.2.5",
"version": "0.3.2",
"description": "Tailwind CSS for Basecoat components",
"author": {
"name": "hunvreus",
+1 -1
View File
@@ -121,7 +121,7 @@ async function build() {
// Create combined component files
console.log('Creating combined component files...');
const componentsToCombine = ['dropdown-menu.js', 'popover.js', 'select.js', 'sidebar.js', 'tabs.js', 'toast.js'];
const componentsToCombine = ['basecoat.js', 'dropdown-menu.js', 'popover.js', 'select.js', 'sidebar.js', 'tabs.js', 'toast.js'];
const componentPaths = componentsToCombine.map(f => path.join(srcJsDir, f));
// Create non-minified bundle
+1 -1
View File
@@ -1,2 +1,2 @@
@import "tailwindcss";
@import "tailwindcss" source(none);
@import "./basecoat.css";
+21 -11
View File
@@ -491,11 +491,13 @@
> footer {
@apply flex flex-col-reverse gap-2 sm:flex-row sm:justify-end;
}
> button,
> 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;
}
}
> button,
> form[method='dialog'] > 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;
}
}
}
@@ -516,7 +518,7 @@
@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;
&:not([aria-disabled='true']) {
@apply focus-visible:bg-accent focus-visible:text-accent-foreground hover:bg-accent hover:text-accent-foreground;
@apply focus-visible:bg-accent focus-visible:text-accent-foreground;
}
&.active {
@@ -530,6 +532,13 @@
@apply border-border -mx-1 my-1;
}
}
&:not([data-dropdown-menu-initialized]) [data-popover] {
[role='menuitem'],
[role='menuitemcheckbox'],
[role='menuitemradio'] {
@apply hover:bg-accent hover:text-accent-foreground;
}
}
}
}
@@ -737,6 +746,10 @@
&[aria-selected='true'] {
@apply bg-[image:var(--check-icon)] bg-no-repeat bg-position-[center_right_0.5rem] bg-size-[0.875rem];
}
&.active,
&:focus-visible {
@apply bg-accent text-accent-foreground;
}
}
[role='listbox'] [role='heading'] {
@apply flex text-muted-foreground px-2 py-1.5 text-xs;
@@ -767,11 +780,8 @@
@apply content-['No_results_found'];
}
}
[role='option']:hover,
&[data-select-initialized] [role='option'].active,
&[data-select-initialized] [role='option']:focus-visible {
@apply bg-accent text-accent-foreground;
&:not([data-select-initialized]) [data-popover] [role='option'] {
@apply hover:bg-accent hover:text-accent-foreground;
}
}
}
@@ -995,7 +1005,7 @@
@apply font-medium tracking-tight;
}
p {
@apply text-muted-foreground;
@apply text-muted-foreground break-all;
}
}
footer {
@@ -1030,7 +1040,7 @@
@apply relative;
&:before {
@apply absolute content-[attr(data-tooltip)] bg-primary text-primary-foreground z-50 truncate max-w-xs w-fit rounded-md px-3 py-1.5 text-xs invisible opacity-0 scale-95 transition-all pointer-events-none;
@apply absolute content-[attr(data-tooltip)] bg-primary text-primary-foreground z-[60] truncate max-w-xs w-fit rounded-md px-3 py-1.5 text-xs invisible opacity-0 scale-95 transition-all pointer-events-none;
}
&:hover:before {
@apply visible opacity-100 scale-100;
+2 -2
View File
@@ -48,12 +48,12 @@
class="dialog {{ dialog_attrs.class }}"
aria-labelledby="{{ id }}-title"
{% if description %}aria-describedby="{{ id }}-description"{% endif %}
{% if close_on_overlay_click %}onclick="this.close()"{% endif %}
{% if close_on_overlay_click %}onclick="if (event.target === this) this.close()"{% endif %}
{% for key, value in dialog_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<article {% if close_on_overlay_click %}onclick="event.stopPropagation()"{% endif %}>
<article>
{% if title or description %}
<header
{% for key, value in header_attrs %}
+99
View File
@@ -0,0 +1,99 @@
(() => {
const componentRegistry = {};
let observer = null;
const registerComponent = (name, selector, initFunction) => {
componentRegistry[name] = {
selector,
init: initFunction
};
};
const initComponent = (element, componentName) => {
const component = componentRegistry[componentName];
if (!component) return;
try {
component.init(element);
} catch (error) {
console.error(`Failed to initialize ${componentName}:`, error);
}
};
const initAllComponents = () => {
Object.entries(componentRegistry).forEach(([name, { selector, init }]) => {
document.querySelectorAll(selector).forEach(init);
});
};
const initNewComponents = (node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
Object.entries(componentRegistry).forEach(([name, { selector, init }]) => {
if (node.matches(selector)) {
init(node);
}
node.querySelectorAll(selector).forEach(init);
});
};
const startObserver = () => {
if (observer) return;
observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach(initNewComponents);
});
});
observer.observe(document.body, { childList: true, subtree: true });
};
const stopObserver = () => {
if (observer) {
observer.disconnect();
observer = null;
}
};
const reinitComponent = (componentName) => {
const component = componentRegistry[componentName];
if (!component) {
console.warn(`Component '${componentName}' not found in registry`);
return;
}
// Clear initialization flag for this component
const flag = `data-${componentName}-initialized`;
document.querySelectorAll(`[${flag}]`).forEach(el => {
el.removeAttribute(flag);
});
document.querySelectorAll(component.selector).forEach(component.init);
};
const reinitAll = () => {
// Clear all initialization flags using the registry
Object.entries(componentRegistry).forEach(([name, { selector }]) => {
const flag = `data-${name}-initialized`;
document.querySelectorAll(`[${flag}]`).forEach(el => {
el.removeAttribute(flag);
});
});
initAllComponents();
};
window.basecoat = {
register: registerComponent,
init: reinitComponent,
initAll: reinitAll,
start: startObserver,
stop: stopObserver
};
document.addEventListener('DOMContentLoaded', () => {
initAllComponents();
startObserver();
});
})();
+36 -23
View File
@@ -29,7 +29,7 @@
setActiveItem(-1);
};
const openPopover = () => {
const openPopover = (initialSelection = false) => {
document.dispatchEvent(new CustomEvent('basecoat:popover', {
detail: { source: dropdownMenuComponent }
}));
@@ -40,8 +40,13 @@
!item.hasAttribute('disabled') &&
item.getAttribute('aria-disabled') !== 'true'
);
if (menuItems.length > 0) {
setActiveItem(0);
if (menuItems.length > 0 && initialSelection) {
if (initialSelection === 'first') {
setActiveItem(0);
} else if (initialSelection === 'last') {
setActiveItem(menuItems.length - 1);
}
}
};
@@ -64,7 +69,7 @@
if (isExpanded) {
closePopover();
} else {
openPopover();
openPopover(false);
}
});
@@ -77,9 +82,15 @@
}
if (!isExpanded) {
if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) {
if (['Enter', ' '].includes(event.key)) {
event.preventDefault();
openPopover();
openPopover(false);
} else if (event.key === 'ArrowDown') {
event.preventDefault();
openPopover('first');
} else if (event.key === 'ArrowUp') {
event.preventDefault();
openPopover('last');
}
return;
}
@@ -91,11 +102,11 @@
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
nextIndex = Math.min(activeIndex + 1, menuItems.length - 1);
nextIndex = activeIndex === -1 ? 0 : Math.min(activeIndex + 1, menuItems.length - 1);
break;
case 'ArrowUp':
event.preventDefault();
nextIndex = Math.max(activeIndex - 1, 0);
nextIndex = activeIndex === -1 ? menuItems.length - 1 : Math.max(activeIndex - 1, 0);
break;
case 'Home':
event.preventDefault();
@@ -118,6 +129,20 @@
}
});
menu.addEventListener('mousemove', (event) => {
const menuItem = event.target.closest('[role^="menuitem"]');
if (menuItem && menuItems.includes(menuItem)) {
const index = menuItems.indexOf(menuItem);
if (index !== activeIndex) {
setActiveItem(index);
}
}
});
menu.addEventListener('mouseleave', () => {
setActiveItem(-1);
});
menu.addEventListener('click', (event) => {
if (event.target.closest('[role^="menuitem"]')) {
closePopover();
@@ -140,19 +165,7 @@
dropdownMenuComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
};
document.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches('.dropdown-menu:not([data-dropdown-menu-initialized])')) {
initDropdownMenu(node);
}
node.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
if (window.basecoat) {
window.basecoat.register('dropdown-menu', '.dropdown-menu:not([data-dropdown-menu-initialized])', initDropdownMenu);
}
})();
+3 -15
View File
@@ -67,19 +67,7 @@
popoverComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
};
document.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches('.popover:not([data-popover-initialized])')) {
initPopover(node);
}
node.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
if (window.basecoat) {
window.basecoat.register('popover', '.popover:not([data-popover-initialized])', initPopover);
}
})();
+67 -63
View File
@@ -1,7 +1,7 @@
(() => {
const initSelect = (selectComponent) => {
const trigger = selectComponent.querySelector(':scope > button');
const selectedValue = trigger.querySelector(':scope > span');
const selectedLabel = trigger.querySelector(':scope > span');
const popover = selectComponent.querySelector(':scope > [data-popover]');
const listbox = popover.querySelector('[role="listbox"]');
const input = selectComponent.querySelector(':scope > input[type="hidden"]');
@@ -20,17 +20,45 @@
let visibleOptions = [...options];
let activeIndex = -1;
const setActiveOption = (index) => {
if (activeIndex > -1 && options[activeIndex]) {
options[activeIndex].classList.remove('active');
}
activeIndex = index;
if (activeIndex > -1) {
const activeOption = options[activeIndex];
activeOption.classList.add('active');
if (activeOption.id) {
trigger.setAttribute('aria-activedescendant', activeOption.id);
} else {
trigger.removeAttribute('aria-activedescendant');
}
} else {
trigger.removeAttribute('aria-activedescendant');
}
};
const hasTransition = () => {
const style = getComputedStyle(popover);
return parseFloat(style.transitionDuration) > 0 || parseFloat(style.transitionDelay) > 0;
};
const updateValue = (option) => {
const updateValue = (option, triggerEvent = true) => {
if (option) {
selectedValue.innerHTML = option.dataset.label || option.innerHTML;
selectedLabel.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');
if (triggerEvent) {
const event = new CustomEvent('change', {
detail: { value: option.dataset.value },
bubbles: true
});
selectComponent.dispatchEvent(event);
}
}
};
@@ -54,49 +82,32 @@
if (focusOnTrigger) trigger.focus();
popover.setAttribute('aria-hidden', 'true');
trigger.setAttribute('aria-expanded', 'false');
trigger.removeAttribute('aria-activedescendant');
if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
activeIndex = -1;
setActiveOption(-1);
}
const selectOption = (option) => {
if (!option) return;
if (option.dataset.value != null) {
const oldValue = input.value;
const newValue = option.dataset.value;
if (newValue != null && newValue !== oldValue) {
updateValue(option);
}
closePopover();
const event = new CustomEvent('change', {
detail: { value: option.dataset.value },
bubbles: true
});
selectComponent.dispatchEvent(event);
};
const selectByValue = (value) => {
const option = options.find(opt => opt.dataset.value === value);
if (option) {
updateValue(option);
const event = new CustomEvent('change', {
detail: { value: option.dataset.value },
bubbles: true
});
selectComponent.dispatchEvent(event);
}
selectOption(option);
};
if (filter) {
const filterOptions = () => {
const searchTerm = filter.value.trim().toLowerCase();
if (activeIndex > -1) {
options[activeIndex].classList.remove('active');
trigger.removeAttribute('aria-activedescendant');
activeIndex = -1;
}
setActiveOption(-1);
visibleOptions = [];
options.forEach(option => {
@@ -113,9 +124,12 @@
}
let initialOption = options.find(opt => opt.dataset.value === input.value);
if (!initialOption && options.length > 0) initialOption = options[0];
if (!initialOption) {
initialOption = options.find(opt => opt.dataset.value !== undefined) ?? options[0];
}
updateValue(initialOption);
updateValue(initialOption, false);
const handleKeyNavigation = (event) => {
const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false';
@@ -173,21 +187,31 @@
}
if (nextVisibleIndex !== currentVisibleIndex) {
if (currentVisibleIndex > -1) {
visibleOptions[currentVisibleIndex].classList.remove('active');
}
const newActiveOption = visibleOptions[nextVisibleIndex];
newActiveOption.classList.add('active');
activeIndex = options.indexOf(newActiveOption);
if (newActiveOption.id) {
trigger.setAttribute('aria-activedescendant', newActiveOption.id);
}
setActiveOption(options.indexOf(newActiveOption));
newActiveOption.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
};
listbox.addEventListener('mousemove', (event) => {
const option = event.target.closest('[role="option"]');
if (option && visibleOptions.includes(option)) {
const index = options.indexOf(option);
if (index !== activeIndex) {
setActiveOption(index);
}
}
});
listbox.addEventListener('mouseleave', () => {
const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
if (selectedOption) {
setActiveOption(options.indexOf(selectedOption));
} else {
setActiveOption(-1);
}
});
trigger.addEventListener('keydown', handleKeyNavigation);
if (filter) {
filter.addEventListener('keydown', handleKeyNavigation);
@@ -213,14 +237,7 @@
const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
if (selectedOption) {
if (activeIndex > -1) {
options[activeIndex]?.classList.remove('active');
}
activeIndex = options.indexOf(selectedOption);
selectedOption.classList.add('active');
if (selectedOption.id) {
trigger.setAttribute('aria-activedescendant', selectedOption.id);
}
setActiveOption(options.indexOf(selectedOption));
selectedOption.scrollIntoView({ block: 'nearest' });
}
};
@@ -260,20 +277,7 @@
selectComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
};
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);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
if (window.basecoat) {
window.basecoat.register('select', 'div.select:not([data-select-initialized])', initSelect);
}
})();
+3 -16
View File
@@ -98,20 +98,7 @@
sidebarComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
};
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);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
if (window.basecoat) {
window.basecoat.register('sidebar', '.sidebar:not([data-sidebar-initialized])', initSidebar);
}
})();
+3 -16
View File
@@ -57,20 +57,7 @@
tabsComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
};
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);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
if (window.basecoat) {
window.basecoat.register('tabs', '.tabs:not([data-tabs-initialized])', initTabs);
}
})();
+5 -21
View File
@@ -99,7 +99,7 @@
clearTimeout(state.timeoutId);
toasts.delete(element);
if (document.activeElement) document.activeElement.blur();
if (element.contains(document.activeElement)) document.activeElement.blur();
element.setAttribute('aria-hidden', 'true');
element.addEventListener('transitionend', () => element.remove(), { once: true });
}
@@ -164,9 +164,6 @@
return template.content.firstChild;
}
const initialToaster = document.getElementById('toaster');
if (initialToaster) initToaster(initialToaster);
document.addEventListener('basecoat:toast', (event) => {
if (!toaster) {
console.error('Cannot create toast: toaster container not found on page.');
@@ -177,21 +174,8 @@
toaster.appendChild(toastElement);
});
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches('#toaster')) {
initToaster(node);
}
if (toaster && node.matches('.toast:not([data-toast-initialized])')) {
initToast(node);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
if (window.basecoat) {
window.basecoat.register('toaster', '#toaster:not([data-toaster-initialized])', initToaster);
window.basecoat.register('toast', '.toast:not([data-toast-initialized])', initToast);
}
})();
+4 -6
View File
@@ -48,12 +48,12 @@
class="dialog {{ dialog_attrs.class }}"
aria-labelledby="{{ id }}-title"
{% if description %}aria-describedby="{{ id }}-description"{% endif %}
{% if close_on_overlay_click %}onclick="this.close()"{% endif %}
{% if close_on_overlay_click %}onclick="if (event.target === this) this.close()"{% endif %}
{% for key, value in dialog_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<article {% if close_on_overlay_click %}onclick="event.stopPropagation()"{% endif %}>
<article>
{% if title or description %}
<header
{% for key, value in header_attrs %}
@@ -83,11 +83,9 @@
</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 type="button" aria-label="Close dialog" onclick="this.closest('dialog').close()">
<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>
</form>
{% endif %}
</article>
</dialog>