mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-11 04:38:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e1682df21 | ||
|
|
d280e21aac | ||
|
|
a03e225bd7 | ||
|
|
bd23745393 | ||
|
|
29600bd3ac | ||
|
|
97a53f1a67 | ||
|
|
ea4fd16b00 | ||
|
|
c5db76d677 | ||
|
|
246aa65860 | ||
|
|
150c20fd67 | ||
|
|
81acdd18cc | ||
|
|
16219ccae6 | ||
|
|
a2e9775a2c | ||
|
|
2a43d10da9 | ||
|
|
92ebe490b3 | ||
|
|
4a44656fd9 | ||
|
|
608e3b2197 | ||
|
|
a1a74c220f | ||
|
|
55e3c78131 | ||
|
|
113d34a842 | ||
|
|
972719f08f | ||
|
|
d125153921 | ||
|
|
2389133e12 | ||
|
|
3f12f91765 | ||
|
|
ae4019a713 |
@@ -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!
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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%;
|
||||
|
||||
@@ -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>
|
||||
@@ -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><head></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">
|
||||
|
||||
@@ -85,8 +85,8 @@ toc:
|
||||
<dd>For the content.</dd>
|
||||
<dt><code class="highlight language-html"><footer></code> <span class="badge-secondary">Optional</span></dt>
|
||||
<dd>Usually contains actions.</dd>
|
||||
<dt><code class="highlight language-html"><form method="dialog"></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"><button type="button" onclick="this.closest('dialog').close()"></code> <span class="badge-secondary">Optional</span></dt>
|
||||
<dd>A button to close the dialog. Alternatively, you can wrap a button in a <code><form method="dialog"></code> element.</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
@@ -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><head></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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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><head></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">
|
||||
|
||||
@@ -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><head></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 %}
|
||||
|
||||
@@ -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><head></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") }}
|
||||
|
||||
@@ -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><head></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">
|
||||
|
||||
@@ -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><script></code> block you can insert in the <code><head></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><head></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>
|
||||
@@ -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><head></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">
|
||||
|
||||
@@ -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><head></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><head></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
@@ -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
@@ -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,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,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
@@ -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,2 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss" source(none);
|
||||
@import "./basecoat.css";
|
||||
+21
-11
@@ -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;
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
})();
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user