Compare commits

...
6 Commits
10 changed files with 211 additions and 86 deletions
+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 }
]}
+29 -13
View File
@@ -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);
}
}
}
}
}
+2 -2
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
+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>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "basecoat",
"version": "0.2.5",
"version": "0.2.7",
"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.2.7",
"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.2.7",
"description": "Tailwind CSS for Basecoat components",
"author": {
"name": "hunvreus",
+14 -6
View File
@@ -516,7 +516,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 +530,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 +744,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 +778,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;
}
}
}
+33 -8
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();
+43 -26
View File
@@ -20,6 +20,26 @@
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;
@@ -54,9 +74,7 @@
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) => {
@@ -92,11 +110,7 @@
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 => {
@@ -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' });
}
};