Working on CLI

This commit is contained in:
Ronan Berder
2025-05-01 20:53:03 +08:00
parent 930e0ee6f1
commit 0c4a14816e
73 changed files with 1961 additions and 706 deletions
+34
View File
@@ -1,10 +1,36 @@
import eleventyLucideicons from "@grimlink/eleventy-plugin-lucide-icons";
import prettier from "prettier";
import Nunjucks from "nunjucks";
import fs from "fs";
import path from "path";
export default async function(eleventyConfig) {
eleventyConfig.setInputDirectory("./docs/src");
eleventyConfig.addPassthroughCopy({"docs/src/assets": "assets"});
eleventyConfig.addPassthroughCopy({ "src/js": "assets/js" });
eleventyConfig.addPlugin(eleventyLucideicons);
// eleventyConfig.addShortcode("fetchCode", async function(filePath) {
// return "HELLO";
// const absolutePath = path.resolve(process.cwd(), filePath);
// return absolutePath;
// try {
// console.log(absolutePath);
// return fs.readFileSync(absolutePath, 'utf8');
// } catch (error) {
// console.error(`[Eleventy FetchCode Error] Failed to read: ${absolutePath}`, error);
// return `<!-- Error fetching code for ${filePath} -->`;
// }
// });
eleventyConfig.addShortcode("fetchCode", function(filePath) {
const absolutePath = path.resolve(process.cwd(), filePath);
try {
return fs.readFileSync(absolutePath, 'utf8');
} catch (error) {
console.error(`[Eleventy FetchCode Error] Failed to read: ${absolutePath}`, error);
return `<!-- Error fetching code for ${filePath} -->`;
}
});
eleventyConfig.addNunjucksAsyncFilter("prettyHtml", async (code, callback) => {
try {
const formattedCode = await prettier.format(code, {
@@ -20,4 +46,12 @@ export default async function(eleventyConfig) {
callback(err, code);
}
});
let nunjucksEnvironment = new Nunjucks.Environment(
new Nunjucks.FileSystemLoader([
"src/nunjucks",
"docs/src/_includes"
]),
{ autoescape: true }
);
eleventyConfig.setLibrary("njk", nunjucksEnvironment);
}
Executable
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env node
// ^^^ Makes the script executable
import { Command } from 'commander';
// We'll add fs-extra and inquirer later
const program = new Command();
console.log('Basecoat CLI');
program
.name('basecoat')
.description('Add Basecoat components to your project')
.version('0.1.0'); // TODO: Get version from package.json
program
.command('add')
.description('Add one or more components to your project')
.argument('[components...]', 'Names of components to add') // Optional arg for now
.action(async (components) => {
if (!components || components.length === 0) {
console.log('Please specify which component(s) to add, or run add without args for a list (TODO).');
// TODO: Implement interactive component selection if no args
return;
}
console.log(`Attempting to add: ${components.join(', ')}`);
for (const componentName of components) {
await addComponent(componentName);
}
console.log('\nComponent addition process finished.');
});
// Placeholder function for adding a single component
async function addComponent(componentName) {
console.log(`\nProcessing component: ${componentName}`);
// 1. Check if config file exists (TODO)
// 2. If not, run init command/ask questions (TODO)
// 3. Read config (template engine, paths) (TODO)
// 4. Determine source file paths based on component name and config (TODO)
// 5. Check if source files exist in the package (TODO)
// 6. Copy files to destination paths using fs-extra (TODO)
console.log(` -> Placeholder: Logic to copy ${componentName} files would go here.`);
}
// TODO: Implement init command
// program
// .command('init')
// .description('Initialize basecoat configuration')
// .action(async () => {
// console.log('Running init...');
// // Ask config questions
// // Save config file
// });
program.parse(process.argv);
+7 -1
View File
@@ -23,6 +23,12 @@
ul {
@apply list-disc ml-6;
}
ol,
ul {
li {
@apply mt-2;
}
}
b,
strong {
@apply font-semibold;
@@ -35,7 +41,7 @@
> table,
> pre,
> p {
@apply my-4;
@apply my-6;
}
> *:first-child {
@apply !mt-0;
@@ -1,55 +0,0 @@
<script>
if (!window._dialogComponentLoaded) {
window._dialogComponentLoaded = true;
const registerDialogComponent = () => {
Alpine.data('dialog', (initialOpen = false, initialCloseOnOverlayClick = true) => ({
id: null,
open: false,
closeOnOverlayClick: true,
init() {
this.id = this.$el.id;
this.open = initialOpen;
this.closeOnOverlayClick = initialCloseOnOverlayClick;
},
show() {
if (!this.open) {
this.open = true;
this.$nextTick(() => {
this.$el.dispatchEvent(new CustomEvent('dialog:opened', { bubbles: true }));
setTimeout(() => this.$refs.dialog.focus(), 50);
});
}
},
hide() {
if (this.open) {
this.open = false;
this.$el.dispatchEvent(new CustomEvent('dialog:closed', { bubbles: true }));
}
},
$main: {
'@dialog:open.window'(e) {
if (event.detail && event.detail.id === this.id) this.show();
},
},
$trigger: {
'@click'() { this.show() },
':aria-expanded'() { return this.open }
},
$content: {
'@keydown.escape'() { this.open && this.hide() },
':inert'() { return !this.open },
'@click'(e) { e.target === this.$el && this.closeOnOverlayClick && this.hide() },
'x-cloak': ''
}
}));
}
{% for event in (register_on or ["alpine:init"]) %}
document.addEventListener('{{ event }}', registerDialogComponent);
{% endfor %}
}
</script>
@@ -1,98 +0,0 @@
<script>
if (!window._dropdownMenuComponentLoaded) {
window._dropdownMenuComponentLoaded = true;
const registerDropdownMenuComponent = () => {
Alpine.data('dropdownMenu', () => ({
open: false,
focusedIndex: null,
menuItems: [],
init() {
this.$nextTick(() => {
this.menuItems = Array.from(this.$el.querySelectorAll('[role=menuitem]:not([disabled]),[role=menuitemcheckbox]:not([disabled]),[role=menuitemradio]:not([disabled])'));
});
},
focusMenuitem() {
if (this.menuItems.length === 0) return;
if (this.focusedIndex >= this.menuItems.length) {
this.focusedIndex = this.menuItems.length - 1;
} else if (this.focusedIndex < 0 || this.focusedIndex === null) {
this.focusedIndex = 0;
}
this.menuItems.forEach(item => item.blur());
this.menuItems[this.focusedIndex].focus();
},
moveMenuitemFocus(delta) {
if (this.menuItems.length === 0) return;
let wasOpen = this.open;
if (!this.open) {
this.open = true;
this.focusedIndex = 0;
} else {
this.focusedIndex = this.focusedIndex === null
? 0
: this.focusedIndex + delta;
}
if (wasOpen) {
this.focusMenuitem();
} else {
setTimeout(() => this.focusMenuitem(), 50);
}
},
handleMenuitemClick(event) {
const menuitem = event.target.closest('[role=menuitem],[role=menuitemcheckbox],[role=menuitemradio]');
if (menuitem && menuitem.getAttribute('aria-disabled') !== 'true' && !menuitem.disabled) {
this.$nextTick(() => {
this.$refs.trigger.focus();
this.open = false;
});
}
},
handleMenuitemMousemove(event) {
const menuitem = event.target.closest('[role=menuitem],[role=menuitemcheckbox],[role=menuitemradio]');
if (menuitem && menuitem.getAttribute('aria-disabled') !== 'true' && !menuitem.disabled) {
this.focusedIndex = this.menuItems.indexOf(menuitem);
this.focusMenuitem();
}
},
$trigger: {
'@click'() { this.open = !this.open },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveMenuitemFocus(+1); },
'@keydown.up.prevent'() { this.moveMenuitemFocus(-1); },
'@keydown.home.prevent'() { this.focusMenuitem(0) },
'@keydown.end.prevent'() { this.focusMenuitem(this.menuItems.length - 1) },
'@keydown.enter.prevent'() { this.open = !this.open },
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@click'(e) { this.handleMenuitemClick(e) },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveMenuitemFocus(+1); },
'@keydown.up.prevent'() { this.moveMenuitemFocus(-1); },
'@keydown.home.prevent'() { this.focusMenuitem(0) },
'@keydown.end.prevent'() { this.focusMenuitem(this.menuItems.length - 1) },
'@mousemove'(e) { this.handleMenuitemMousemove(e) },
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
}));
}
{% for event in (register_on or ["alpine:init"]) %}
document.addEventListener('{{ event }}', registerDropdownMenuComponent);
{% endfor %}
}
</script>
@@ -1,33 +0,0 @@
<script>
if (!window._popoverComponentLoaded) {
window._popoverComponentLoaded = true;
const registerPopoverComponent = () => {
Alpine.data('popover', () => ({
open: false,
$trigger: {
'@click'() { this.open = !this.open },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
}));
}
{% for event in (register_on or ["alpine:init"]) %}
document.addEventListener('{{ event }}', registerPopoverComponent);
{% endfor %}
}
</script>
@@ -1,157 +0,0 @@
<script>
if (!window._selectComponentLoaded) {
window._selectComponentLoaded = true;
const registerSelectComponent = () => {
Alpine.data('select', (name = null, initialValue = null) => ({
open: false,
name: null,
options: [],
disabledOptions: [],
focusedIndex: null,
selectedLabel: null,
selectedValue: null,
query: '',
init() {
this.$nextTick(() => {
if (name) this.name = name;
this.options = Array.from(this.$el.querySelectorAll('[role=option]:not([aria-disabled])'));
this.disabledOptions = Array.from(this.$el.querySelectorAll('[role=option][aria-disabled=true]'));
if (this.options.length === 0) return;
if (initialValue) {
const option = this.options.find(opt => opt.getAttribute('data-value') === initialValue);
this.selectOption(option, false);
this.focusedIndex = this.options.indexOf(option);
} else {
this.selectOption(this.options[0], false);
}
});
},
focusOption() {
if (this.options.length === 0) return;
if (this.focusedIndex >= this.options.length) {
this.focusedIndex = this.options.length - 1;
} else if (this.focusedIndex < 0 || this.focusedIndex === null) {
this.focusedIndex = 0;
}
this.options.forEach(opt => opt.removeAttribute('data-focus'));
const option = this.options[this.focusedIndex];
option.setAttribute('data-focus', '');
option.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
},
focusOnSelectedOption() {
if (this.options.length === 0 || this.selectedValue === null) return;
const option = this.options.find(opt => opt.getAttribute('data-value') === this.selectedValue);
if (!option) return;
this.focusedIndex = this.options.indexOf(option);
this.focusOption();
},
moveOptionFocus(delta) {
if (this.options.length === 0) return;
if (!this.open) {
this.open = true;
this.focusOnSelectedOption();
} else {
this.focusedIndex = this.focusedIndex === null
? 0
: this.focusedIndex + delta;
}
this.focusOption();
},
handleOptionClick(event) {
const option = event.target.closest('[role=option]');
if (option && option.getAttribute('aria-disabled') !== 'true') {
this.selectOption(option);
this.open = false;
this.$nextTick(() => this.$refs.trigger.focus());
}
},
handleOptionMousemove(event) {
const option = event.target.closest('[role=option]');
if (option && option.getAttribute('aria-disabled') !== 'true') {
this.focusedIndex = this.options.indexOf(option);
this.focusOption();
}
},
handleOptionEnter(event) {
this.selectOption(this.options[this.focusedIndex]);
this.open = !this.open;
},
selectOption(option, dispatch = true) {
if (this.options.length === 0 || !option || option.disabled || option.getAttribute('data-value') == null) {
return;
}
this.options.forEach(opt => {
opt.setAttribute('aria-selected', opt === option);
});
this.selectedLabel = option.innerHTML;
this.selectedValue = option.getAttribute('data-value');
if (dispatch) {
this.$dispatch('select:change', {
value: this.selectedValue,
label: this.selectedLabel
});
this.$dispatch('change');
}
},
filterOptions(query) {
if (query.length > 0) {
this.disabledOptions.forEach(opt => { opt.setAttribute('aria-hidden', 'true'); });
} else {
this.disabledOptions.forEach(opt => { opt.removeAttribute('aria-hidden'); });
}
this.options.forEach(opt => {
opt.removeAttribute('aria-hidden');
if (opt.getAttribute('data-value') != null && !opt.innerHTML.toLowerCase().includes(query.toLowerCase())) {
opt.setAttribute('aria-hidden', 'true');
}
});
},
$trigger: {
'@click'() { this.open = !this.open; this.focusOnSelectedOption(); },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveOptionFocus(+1); },
'@keydown.up.prevent'() { this.moveOptionFocus(-1); },
'@keydown.home.prevent'() { this.focusOption(0) },
'@keydown.end.prevent'() { this.focusOption(this.options.length - 1) },
'@keydown.enter.prevent'() {
if (this.open) this.handleOptionEnter();
else this.open = true;
},
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@click'(e) { this.handleOptionClick(e) },
'@mousemove'(e) { this.handleOptionMousemove(e) },
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
$filter: {
'@input'(e) { this.filterOptions(e.target.value) },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveOptionFocus(+1); },
'@keydown.up.prevent'() { this.moveOptionFocus(-1); },
'@keydown.home.prevent'() { this.focusOption(0) },
'@keydown.end.prevent'() { this.focusOption(this.options.length - 1) },
'@keydown.enter.prevent'() { this.handleOptionEnter() },
}
}));
}
{% for event in (register_on or ["alpine:init"]) %}
document.addEventListener('{{ event }}', registerSelectComponent);
{% endfor %}
}
</script>
@@ -1,71 +0,0 @@
<script>
if (!window._tabsComponentLoaded) {
window._tabsComponentLoaded = true;
const registerTabsComponent = () => {
Alpine.data('tabs', (initialTabIndex = 0) => ({
activeTabIndex: initialTabIndex,
tabs: [],
panels: [],
init() {
this.$nextTick(() => {
this.tabs = Array.from(this.$el.querySelectorAll(':scope > [role=tablist] [role=tab]:not([disabled])'));
this.panels = Array.from(this.$el.querySelectorAll(':scope > [role=tabpanel]'));
if (this.tabs.length > 0) {
this.selectTab(this.tabs[initialTabIndex], false);
}
});
},
nextTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex + 1) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
},
prevTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex - 1 + this.tabs.length) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
},
selectTab(tab, focus = true) {
if (!tab || this.tabs.length === 0) return;
this.tabs.forEach((t, index) => {
const isSelected = t === tab;
t.setAttribute('aria-selected', isSelected);
t.setAttribute('tabindex', isSelected ? '0' : '-1');
if (isSelected) {
this.activeTabIndex = index;
this.activeTab = t;
if (focus) {
t.focus();
}
}
});
const panelId = tab.getAttribute('aria-controls');
if (!panelId) return;
this.panels.forEach(panel => {
panel.hidden = (panel.getAttribute('id') !== panelId);
});
},
$tablist: {
['@click'](event) {
const clickedTab = event.target.closest('[role=tab]');
if (clickedTab) {
this.selectTab(clickedTab);
}
},
['@keydown.arrow-right.prevent']() { this.nextTab() },
['@keydown.arrow-left.prevent']() { this.prevTab() },
},
}));
}
{% for event in (register_on or ["alpine:init"]) %}
document.addEventListener('{{ event }}', registerTabsComponent);
{% endfor %}
}
</script>
@@ -1,75 +0,0 @@
<script>
if (!window._toastComponentLoaded) {
window._toastComponentLoaded = true;
const registerToastComponent = () => {
Alpine.store('toaster', { isPaused: false });
Alpine.data('toast', (config={}) => ({
config: config,
open: false,
timeoutDuration: null,
timeoutId: null,
init() {
this.timeoutDuration = config.duration || (config.category === 'error' ? 5000 : 3000);
this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
this.open = true;
this.$watch('$store.toaster.isPaused', (isPaused) => {
if (!this.open) return;
if (isPaused) {
this.pauseTimeout();
} else {
this.resumeTimeout();
}
});
},
pauseTimeout() {
clearTimeout(this.timeoutId);
this.timeoutId = null;
},
resumeTimeout(index) {
if (this.open && this.timeoutId === null) {
this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
}
},
close() {
this.pauseTimeout();
this.open = false;
this.$el.blur();
},
$toastBindings: {
['@mouseenter']() { this.$store.toaster.isPaused = true },
['@mouseleave']() { this.$store.toaster.isPaused = false },
['@keydown.escape.prevent']() { this.close() },
[':aria-hidden']() { return !this.open }
},
}));
Alpine.magic('toast', (el) => (config, toasterId='toaster') => {
const toaster = document.getElementById(toasterId);
const template = document.getElementById('toast-template');
if (!toaster) {
console.error(`Toaster container with id #${targetId} not found.`);
return;
}
if (!template) {
console.error('Toast template with id #toast-template not found.');
return;
}
const clone = template.content.firstElementChild.cloneNode(true);
clone.setAttribute('x-data', `toast(${JSON.stringify(config)})`);
clone.removeAttribute('id');
toaster.appendChild(clone);
});
}
{% for event in (register_on or ["alpine:init"]) %}
document.addEventListener('{{ event }}', registerToastComponent);
{% endfor %}
}
</script>
+18 -8
View File
@@ -45,14 +45,24 @@
<link rel="stylesheet" href="/assets/styles.css">
<!-- JS -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="/assets/js/dialog.js" defer></script>
<script src="/assets/js/dropdown-menu.js" defer></script>
<script src="/assets/js/popover.js" defer></script>
<script src="/assets/js/select.js" defer></script>
<script src="/assets/js/tabs.js" defer></script>
<script src="/assets/js/toast.js" defer></script>
<script>
document.addEventListener('alpine:init', () => {
window.basecoat.registerDialog(Alpine);
window.basecoat.registerDropdownMenu(Alpine);
window.basecoat.registerPopover(Alpine);
window.basecoat.registerSelect(Alpine);
window.basecoat.registerTabs(Alpine);
window.basecoat.registerToast(Alpine);
});
</script>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
{% include "components/dialog-script.njk" %}
{% include "components/dropdown-menu-script.njk" %}
{% include "components/popover-script.njk" %}
{% include "components/select-script.njk" %}
{% include "components/tabs-script.njk" %}
{% include "components/toast-script.njk" %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/django.min.js"></script>
@@ -128,7 +138,7 @@
</div>
</main>
{% from "components/toast.njk" import toaster %}
{% from "toast.njk" import toaster %}
{{ toaster() }}
</body>
</html>
+1 -1
View File
@@ -12,7 +12,7 @@
{% endset %}
<div class="relative my-6">
{% from "components/tabs.njk" import tabs %}
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "Preview", panel: preview_panel },
{ tab: "Code", panel: code_panel }
@@ -1,4 +1,4 @@
{% from "components/dialog.njk" import dialog %}
{% from "dialog.njk" import dialog %}
<section id="alert-dialog" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
@@ -1,4 +1,4 @@
{% from "components/popover.njk" import popover %}
{% from "popover.njk" import popover %}
<section id="breadcrumb" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
@@ -1,4 +1,4 @@
{% from "components/select.njk" import select %}
{% from "select.njk" import select %}
<section id="combobox" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
@@ -1,4 +1,4 @@
{% from "components/dialog.njk" import dialog %}
{% from "dialog.njk" import dialog %}
<section id="dialog" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
@@ -1,4 +1,4 @@
{% from "components/dropdown-menu.njk" import dropdown_menu %}
{% from "dropdown-menu.njk" import dropdown_menu %}
<section id="dropdown-menu" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
@@ -1,4 +1,4 @@
{% from "components/popover.njk" import popover %}
{% from "popover.njk" import popover %}
<section id="popover" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
@@ -1,4 +1,4 @@
{% from "components/select.njk" import select %}
{% from "select.njk" import select %}
<section id="select" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
@@ -1,4 +1,4 @@
{% from "components/tabs.njk" import tabs %}
{% from "tabs.njk" import tabs %}
<section id="tabs" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
+1 -10
View File
@@ -55,7 +55,7 @@
href="/"
class="h-8 flex items-center gap-2 rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0"
>
{% lucide "book-open" %}
{% lucide "info" %}
Introduction
</a>
</li>
@@ -77,15 +77,6 @@
Kitchen sink
</a>
</li>
<li>
<a
href="/theming"
class="h-8 flex items-center gap-2 rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0"
>
{% lucide "palette" %}
Theming
</a>
</li>
<li>
<a
href="https://github.com/hunvreus/basecoat"
+6 -1
View File
@@ -3839,12 +3839,17 @@
margin-left: calc(var(--spacing) * 6);
list-style-type: disc;
}
ol, ul {
li {
margin-top: calc(var(--spacing) * 2);
}
}
b, strong {
--tw-font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-semibold);
}
> p, > blockquote, > ul, > ol, > dl, > table, > pre, > p {
margin-block: calc(var(--spacing) * 4);
margin-block: calc(var(--spacing) * 6);
}
> *:first-child {
margin-top: calc(var(--spacing) * 0) !important;
+13 -9
View File
@@ -14,7 +14,7 @@ toc:
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "components/dialog.njk" import dialog %}
{% from "dialog.njk" import dialog %}
{% set code_html %}
{% set footer %}
@@ -34,9 +34,13 @@ toc:
) }}
{% endset %}
{% set code_script %}
{% include "components/dialog-script.njk" %}
{% endset %}
{% set code_script %}<script>
{% fetchCode "src/js/dialog.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerDialog(Alpine);
});
</script>{% endset %}
{{ code_preview("alert-dialog", code_script | prettyHtml ~ code_html | prettyHtml) }}
@@ -52,18 +56,18 @@ toc:
</section>
</div>
<p class="prose my-6">The Alert Dialog component uses the same Alpine.js code as the <a href="/components/dialog" class="font-medium underline underline-offset-4">Dialog</a> component with one small difference: we set <code>initialCloseOnOverlayClick</code> to <code>false</code> to prevent the dialog from closing when the overlay is clicked.</p>
<p class="prose">The Alert Dialog component uses the same Alpine.js code as the <a href="/components/dialog" class="font-medium underline underline-offset-4">Dialog</a> component with one small difference: we set <code>initialCloseOnOverlayClick</code> to <code>false</code> to prevent the dialog from closing when the overlay is clicked.</p>
{% from "components/tabs.njk" import tabs %}
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml) },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml) }
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="alert-dialog", tabsets=tabsets) }}
<h3 id="usage-jinja"><a href="#usage-jinja">Jinja or Nunjucks</a></h3>
<p class="my-6 prose">If you use Jinja (or Nunjucks), you can use the <code>dialog()</code> macro directly, making sure to set <code>close_on_overlay_click</code> and <code>close_button</code> to <code>False</code> (or <code>false</code> for Nunjucks). <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
<p class="prose">If you use Jinja (or Nunjucks), you can use the <code>dialog()</code> macro directly, making sure to set <code>close_on_overlay_click</code> and <code>close_button</code> to <code>False</code> (or <code>false</code> for Nunjucks). <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
{% set raw_code %}{% raw %}{% set footer %}
<button class="btn-outline" @click="open = false">Cancel</button>
+1 -1
View File
@@ -28,7 +28,7 @@ toc:
<h2 id="usage">Usage</h2>
<section class="prose my-6">
<section class="prose">
<p>You can use one of the two classes: <code>alert</code> or <code>alert-dialog</code> with any of the following children:</p>
<ul>
<li>A svg icon,</li>
+1 -1
View File
@@ -20,4 +20,4 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<p class="my-6 prose">Avatars are fairly simple to implement with Tailwind. The example above shows how to implement one with Tailwind and an <code>&lt;img&gt;</code> element. More examples can be found in the <a href="/kitchen-sink#avatar">kitchen sink</a>.</p>
<p class="prose">Avatars are fairly simple to implement with Tailwind. The example above shows how to implement one with Tailwind and an <code>&lt;img&gt;</code> element. More examples can be found in the <a href="/kitchen-sink#avatar">kitchen sink</a>.</p>
+1 -1
View File
@@ -30,7 +30,7 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose my-6">
<section class="prose">
<p>Use one of the badge classes: <code>badge</code>, <code>badge-primary</code>, <code>badge-secondary</code>, <code>badge-destructive</code> or <code>badge-outline</code>.
</section>
+12 -6
View File
@@ -9,7 +9,7 @@ toc:
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "components/dropdown-menu.njk" import dropdown_menu %}
{% from "dropdown-menu.njk" import dropdown_menu %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
@@ -17,7 +17,13 @@ toc:
</div>
{% set code_script %}
{% include "components/dropdown-menu-script.njk" %}
<script>
{% fetchCode "src/js/dropdown-menu.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerDropdownMenu(Alpine);
});
</script>
{% endset %}
{% set code_html %}
@@ -58,13 +64,13 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose my-6">
<section class="prose">
<p>The example above uses some simple Tailwind CSS along with the <a href="/components/dropdown-menu">Dropdown Menu</a>.</p>
</section>
{% from "components/tabs.njk" import tabs %}
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml) },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml) }
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="alert-dialog", tabsets=tabsets) }}
+2 -4
View File
@@ -40,7 +40,7 @@ toc:
<h2 id="usage">Usage</h2>
<section class="prose my-6">
<section class="prose">
<p>You can use one of the <code>btn</code> classes to style any element as a button:</p>
<ul>
@@ -68,9 +68,7 @@ toc:
</ul>
</section>
<section class="my-6">
{{ code_block(code) }}
</section>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
+1 -1
View File
@@ -47,7 +47,7 @@ toc:
<h2 id="usage">Usage</h2>
<section class="prose my-6">
<section class="prose">
<p>Add the <code>card</code> class to any element and then define any of the children elements:</p>
<ul>
<li>A <code>&lt;header&gt;</code> for the card header, which can contain a <code>&lt;h2&gt;</code> for the title and a <code>&lt;p&gt;</code> for the description.</li>
+3 -5
View File
@@ -27,14 +27,12 @@ toc:
<h2 id="usage">Usage</h2>
<section class="prose my-6">
<section class="prose">
<p>Add the <code>input</code> class to your <code>&lt;input type="radio"&gt;</code> elements or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). You can also set the <code>aria-invalid</code> attribute to <code>true</code> to make the input invalid.</p>
</section>
<section class="my-6">
{% set code %}<input type="checkbox" class="input">{% endset %}
{{ code_block(code) }}
</section>
{% set code %}<input type="checkbox" class="input">{% endset %}
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
+13 -9
View File
@@ -21,7 +21,7 @@ toc:
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "components/select.njk" import select %}
{% from "select.njk" import select %}
{% set options_frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] %}
{% set code_html %}
@@ -39,7 +39,13 @@ toc:
{% endset %}
{% set code_script %}
{% include "components/select-script.njk" %}
<script>
{% fetchCode "src/js/select.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerSelect(Alpine);
});
</script>
{% endset %}
{{ code_preview("combobox", code_script | prettyHtml ~ code_html | prettyHtml) }}
@@ -56,7 +62,7 @@ toc:
</section>
</div>
<section class="prose my-6">
<section class="prose">
<p>Combobox uses the same Alpine.js code and markup as the <a href="/components/select">Select</a> component, the only difference being the search box at the top of the listbox.</p>
<p>You can set up a few additional options on the <code>&lt;div data-popover&gt;</code> element:</p>
<ul>
@@ -66,18 +72,16 @@ toc:
</ul>
</section>
{% from "components/tabs.njk" import tabs %}
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml) },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml) }
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="combobox", tabsets=tabsets) }}
<h3 id="usage-jinja"><a href="#usage-jinja">Jinja or Nunjucks</a></h3>
<section class="prose my-6">
<p class="my-6 prose">If you use Jinja (or Nunjucks), you can use the <code>select()</code> macro directly, making sure to set <code>is_combobox</code> to <code>True</code> (or <code>true</code> for Nunjucks). <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
</section>
<p class="prose">If you use Jinja (or Nunjucks), you can use the <code>select()</code> macro directly, making sure to set <code>is_combobox</code> to <code>True</code> (or <code>true</code> for Nunjucks). <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
{% set raw_code %}{% raw %}{% call select(
listbox_attrs={"data-empty": "No framework found."},
+15 -9
View File
@@ -19,7 +19,7 @@ toc:
{% from "macros/code_block.njk" import code_block %}
{% from "macros/code_preview.njk" import code_preview %}
{% from "components/dialog.njk" import dialog %}
{% from "dialog.njk" import dialog %}
{% set code_html %}
{% set footer %}
@@ -40,7 +40,7 @@ toc:
<form class="form grid gap-4">
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-name">Name</label>
<input type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" />
<input type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" x-ref="focusOnOpen" />
</div>
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-username">Username</label>
@@ -51,10 +51,16 @@ toc:
{% endset %}
{% set code_script %}
{% include "components/dialog-script.njk" %}
<script>
{% fetchCode "src/js/dialog.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerDialog(Alpine);
});
</script>
{% endset %}
{{ code_preview("dialog", code_script | prettyHtml ~ code_html | prettyHtml) }}
{{ code_preview("dialog", code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
@@ -68,7 +74,7 @@ toc:
</section>
</div>
<section class="prose my-6">
<section class="prose">
<p>The component is structured as such:</p>
<ul>
<li>A <code>&lt;div class="dialog"&gt;</code> which wraps around the entire component and holds it state (e.g. open/close).</li>
@@ -85,16 +91,16 @@ toc:
</ul>
</section>
{% from "components/tabs.njk" import tabs %}
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml) },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml) }
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="combobox", tabsets=tabsets) }}
<h3 id="usage-jinja"><a href="#usage-jinja">Jinja or Nunjucks</a></h3>
<p class="my-6 prose">If you use Jinja (or Nunjucks), you can use the <code>dialog()</code> macro directly. <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
<p class="prose">If you use Jinja (or Nunjucks), you can use the <code>dialog()</code> macro directly. <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
{% set raw_code %}{% raw %}{% set footer %}
<button class="btn-outline" @click="open = false">Cancel</button>
+13 -7
View File
@@ -21,7 +21,7 @@ toc:
{% from "macros/code_block.njk" import code_block %}
{% from "macros/code_preview.njk" import code_preview %}
{% from "components/dropdown-menu.njk" import dropdown_menu %}
{% from "dropdown-menu.njk" import dropdown_menu %}
{% set code_html %}
{% call dropdown_menu(
@@ -69,7 +69,13 @@ toc:
{% endset %}
{% set code_script %}
{% include "components/dropdown-menu-script.njk" %}
<script>
{% fetchCode "src/js/dropdown-menu.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerDropdownMenu(Alpine);
});
</script>
{% endset %}
{{ code_preview("dropdown-menu", code_script | prettyHtml ~ code_html | prettyHtml) }}
@@ -86,7 +92,7 @@ toc:
</section>
</div>
<section class="prose my-6">
<section class="prose">
<p>The component is structured as such:</p>
<ul>
<li>A <code>&lt;div class="popover"&gt;</code> which wraps around the entire component and holds it state (e.g. open/close).</li>
@@ -109,16 +115,16 @@ toc:
</ul>
</section>
{% from "components/tabs.njk" import tabs %}
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml) },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml) }
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="dropdown-menu", tabsets=tabsets) }}
<h3 id="usage-jinja"><a href="#usage-jinja">Jinja or Nunjucks</a></h3>
<p class="my-6 prose">If you use Jinja (or Nunjucks), you can use the <code>dropdown_menu()</code> macro directly. <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
<p class="prose">If you use Jinja (or Nunjucks), you can use the <code>dropdown_menu()</code> macro directly. <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
{% set raw_code %}{% raw %}{% call dropdown_menu(
id="dropdown-menu",
+1 -1
View File
@@ -72,7 +72,7 @@ toc:
<h2 id="usage">Usage</h2>
<section class="prose my-6">
<section class="prose">
<p>Adding the <code>form</code> class to the parent of any form element (<code>&lt;label&gt;</code>, <code>&lt;input&gt;</code>, <code>&lt;textarea&gt;</code>, <code>&lt;select&gt;</code>) will automatically apply the style you would get from adding <code>label</code>, <code>input</code>, <code>textarea</code>, or <code>select</code> classes. Read more about <a href="/components/label">Label</a>, <a href="/components/input">Input</a>, <a href="/components/textarea">Textarea</a>, and <a href="/components/select">Select</a>.</p>
</section>
+1 -1
View File
@@ -32,7 +32,7 @@ toc:
<h2 id="usage">Usage</h2>
<section class="prose my-6">
<section class="prose">
<p>Simply add the <code>input</code> class to your <code>&lt;input&gt;</code> element or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). You can also set the <code>aria-invalid</code> attribute to <code>true</code> to make the input invalid.</p>
</section>
+1 -1
View File
@@ -23,7 +23,7 @@ toc:
<h2 id="usage">Usage</h2>
<section class="prose my-6">
<section class="prose">
<p>Simply add the <code>label</code> class to your <code>&lt;label&gt;</code> element or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). Labels' style may be affected by the presence of a disabled peer or child element.</p>
</section>
+1 -1
View File
@@ -41,7 +41,7 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose my-6">
<section class="prose">
<p>A pagination is fairly simple to implement with Tailwind using the Basecoat button classes:</p>
</section>
+15 -9
View File
@@ -14,7 +14,7 @@ toc:
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "components/popover.njk" import popover %}
{% from "popover.njk" import popover %}
{% set code_html %}
{% call popover(
@@ -52,10 +52,16 @@ toc:
{% endset %}
{% set code_script %}
{% include "components/popover-script.njk" %}
<script>
{% fetchCode "src/js/popover.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerPopover(Alpine);
});
</script>
{% endset %}
{{ code_preview("combobox", code_script | prettyHtml ~ code_html | prettyHtml) }}
{{ code_preview("popover", code_script | prettyHtml ~ code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
@@ -69,7 +75,7 @@ toc:
</section>
</div>
<section class="prose my-6">
<section class="prose">
<p>The component is structured as such:</p>
<ul>
<li>A <code>&lt;div class="popover"&gt;</code> which wraps around the entire component and holds it state (e.g. open/close).</li>
@@ -83,17 +89,17 @@ toc:
</ul>
</section>
{% from "components/tabs.njk" import tabs %}
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml) },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml) }
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="popover", tabsets=tabsets) }}
<h3 id="usage-jinja"><a href="#usage-jinja">Jinja or Nunjucks</a></h3>
<section class="prose my-6">
<p class="my-6 prose">If you use Jinja (or Nunjucks), you can use the <code>popover()</code> macro directly. <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
<section class="prose">
<p>If you use Jinja (or Nunjucks), you can use the <code>popover()</code> macro directly. <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
</section>
{% set raw_code %}{% raw %}{% call popover(
+1 -1
View File
@@ -24,7 +24,7 @@ toc:
<h2 id="usage">Usage</h2>
<section class="prose my-6">
<section class="prose">
<p>Simply add the <code>input</code> class to your <code>&lt;input type="radio"&gt;</code> elements or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). You can also set the <code>aria-invalid</code> attribute to <code>true</code> to make the input invalid.</p>
</section>
+15 -9
View File
@@ -25,7 +25,7 @@ toc:
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "components/select.njk" import select %}
{% from "select.njk" import select %}
{% set code_native %}<select class="select w-[180px]">
<optgroup label="Fruits">
@@ -55,16 +55,22 @@ toc:
{% endset %}
{% set code_script %}
{% include "components/select-script.njk" %}
<script>
{% fetchCode "src/js/select.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerSelect(Alpine);
});
</script>
{% endset %}
{{ code_preview("combobox", code_script | prettyHtml ~ code_html | prettyHtml) }}
{{ code_preview("select", code_script | prettyHtml ~ code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html"><a href="#usage-html">HTML</a></h3>
<section class="prose my-6">
<section class="prose">
<p>If you use a <code>&lt;select&gt;</code> element, just add the <code>select</code> class to it or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>).</p>
</section>
@@ -80,7 +86,7 @@ toc:
</section>
</div>
<section class="prose my-6">
<section class="prose">
<p>If you need to do more than what <code>&lt;select&gt;</code> allows (e.g. HTML options), you can use this component. It is structured as such:</p>
<ul>
<li>A <code>&lt;div class="popover"&gt;</code> which wraps around the entire component and holds it state (e.g. open/close).</li>
@@ -103,16 +109,16 @@ toc:
</ul>
</section>
{% from "components/tabs.njk" import tabs %}
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml) },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml) }
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="select", tabsets=tabsets) }}
<h3 id="usage-jinja"><a href="#usage-jinja">Jinja or Nunjucks</a></h3>
<p class="my-6 prose">If you use Jinja (or Nunjucks), you can use the <code>dropdown_menu()</code> macro directly. <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
<p class="prose">If you use Jinja (or Nunjucks), you can use the <code>dropdown_menu()</code> macro directly. <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
{% set raw_code %}{% raw %}{% call select(name="select-js") %}
<div role="group" aria-labelledby="fruit-options-label">
+1 -1
View File
@@ -31,7 +31,7 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<p class="prose my-6">Simply use the <code>animate-pulse</code> class to create a skeleton.</p>
<p class="prose">Simply use the <code>animate-pulse</code> class to create a skeleton.</p>
{{ code_block(code) }}
+1 -1
View File
@@ -32,7 +32,7 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose my-6">
<section class="prose">
<p>You can style an <code>&lt;input type="range"&gt;</code> element by either adding the <code>input</code> class or having a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). However, the range input also requries some Javacript code to be able to paint the left side of the slider.</p>
</section>
+1 -1
View File
@@ -23,7 +23,7 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose my-6">
<section class="prose">
<p>You can style a <code>&lt;input type="checkbox"&gt;</code> element as a switch by adding a <code>role="switch"</code> attribute, and adding the <code>input</code> class or having a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>).</p>
</section>
+1 -1
View File
@@ -75,7 +75,7 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose my-6">
<section class="prose">
<p>Simply add the <code>table</code> class to your table element.</p>
</section>
+13 -7
View File
@@ -14,7 +14,7 @@ toc:
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "components/tabs.njk" import tabs %}
{% from "tabs.njk" import tabs %}
{% set code_html %}
{% set account_panel %}
@@ -77,7 +77,13 @@ toc:
{% endset %}
{% set code_script %}
{% include "components/tabs-script.njk" %}
<script>
{% fetchCode "src/js/tabs.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerTabs(Alpine);
});
</script>
{% endset %}
{{ code_preview("tabs", code_script | prettyHtml ~ code_html | prettyHtml, "max-w-[300px] w-full") }}
@@ -94,7 +100,7 @@ toc:
</section>
</div>
<section class="prose my-6">
<section class="prose">
<p>The component is structured as such:</p>
<ul>
<li>A <code>&lt;div class="tabs"&gt;</code> which wraps around the entire component and holds it state (e.g. open/close).</li>
@@ -103,16 +109,16 @@ toc:
</ul>
</section>
{% from "components/tabs.njk" import tabs %}
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml) },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml) }
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="combobox", tabsets=tabsets) }}
<h3 id="usage-jinja"><a href="#usage-jinja">Jinja or Nunjucks</a></h3>
<p class="my-6 prose">If you use Jinja (or Nunjucks), you can use the <code>tabs()</code> macro directly. <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
<p class="prose">If you use Jinja (or Nunjucks), you can use the <code>tabs()</code> macro directly. <a href="/installation#jinja-and-nunjucks">Read more about using Basecoat with Jinja and Nunjucks</a>.</p>
{% set raw_code %}{% raw %}{% set account_panel %}
<div class="card">
+1 -1
View File
@@ -32,7 +32,7 @@ toc:
<h2 id="usage">Usage</h2>
<section class="prose my-6">
<section class="prose">
<p>Simply add the <code>textarea</code> class to your <code>&lt;textarea&gt;</code> element or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). You can also set the <code>aria-invalid</code> attribute to <code>true</code> to make the textarea invalid.</p>
</section>
+1 -1
View File
@@ -24,7 +24,7 @@ toc:
<h2 id="usage">Usage</h2>
<section class="prose my-6">
<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>
+1 -1
View File
@@ -1,4 +1,4 @@
{% from "../../_includes/components/toast.njk" import toaster %}
{% from "toast.njk" import toaster %}
{% set toasts = [{
category: "error",
title: "Event has not been created",
+1 -1
View File
@@ -1,4 +1,4 @@
{% from "../../_includes/components/toast.njk" import toaster %}
{% from "toast.njk" import toaster %}
{% set toasts = [{
category: "info",
title: "Be at the area 10 minutes before the event time"
+1 -1
View File
@@ -1,4 +1,4 @@
{% from "../../_includes/components/toast.njk" import toaster %}
{% from "toast.njk" import toaster %}
{% set toasts = [{
title: "Event has been created",
description: "Sunday, December 03, 2023 at 9:00 AM",
+1 -1
View File
@@ -1,4 +1,4 @@
{% from "../../_includes/components/toast.njk" import toaster %}
{% from "toast.njk" import toaster %}
{% set toasts = [{
category: "warning",
title: "Event start time cannot be earlier than 8am"
+1 -3
View File
@@ -2,6 +2,4 @@
layout: layouts/page.njk
---
What is it?
Who is it for?
HELLO
+22 -29
View File
@@ -81,37 +81,30 @@ toc:
</div>
{% endcall %}{% endraw %}{% endset %}
{{ code_block(code, "jinja") }}
<p>You still need to make sure to include the Javascript code, to do so you have two options:</p>
<p>Make sure to include the Javascript code <b>before</b> Alpine.js core JS file:</p>
{% set code %}<script src="/assets/js/select.js" defer></script>
<script>
document.addEventListener('alpine:init', () => {
window.basecoat.registerSelect(Alpine);
});
</script>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>{% endset %}
{{ code_block(code, "jinja") }}
<h2 id="install-theming"><a href="#install-theming">Theming</a></h2>
<div class="prose">
<p>You can import any <a href="https://shadcn-ui.com" target="_blank">shadcn/ui</a> compatible theme. For example:</p>
<ul>
<li>Go to <a href="https://ui.shadcn.com/themes" target="_blank">ui.shadcn.com/themes</a> and select a theme.</li>
<li>Click "Copy code" and save the theme variables in a file (e.g. <code>theme.css</code>).</li>
<li>
Set <code>include_script</code> to <code>true</code> when calling the macro. This will include the <code>&lt;script&gt;</code> code snippet directly before the component's HTML:
{% set code %}{% raw %}{% call select(include_script=true) %} {# Or `True` for Jinja #}
<div role="group" aria-labelledby="fruit-options-label">
<span id="fruit-options-label" role="heading">Fruits</span>
<div role="option" data-value="apple">Apple</div>
<div role="option" data-value="banana">Banana</div>
<div role="option" data-value="blueberry">Blueberry</div>
<div role="option" data-value="pineapple">Grapes</div>
<div role="option" data-value="pineapple">Pineapple</div>
</div>
{% endcall %}{% endraw %}{% endset %}
{{ code_block(code, "jinja") }}
</li>
<li>
Manually include the <code>&lt;script&gt;</code> code snippet anywhere before the component's HTML. You can use the relevant script partial to do this, for example:
{% set code_script %}{% raw %}{% include "select-script.njk" %}
{% call select() %}
<div role="group" aria-labelledby="fruit-options-label">
<span id="fruit-options-label" role="heading">Fruits</span>
<div role="option" data-value="apple">Apple</div>
<div role="option" data-value="banana">Banana</div>
<div role="option" data-value="blueberry">Blueberry</div>
<div role="option" data-value="pineapple">Grapes</div>
<div role="option" data-value="pineapple">Pineapple</div>
</div>
{% endcall %}{% endraw %}{% endset %}
{{ code_block(code_script, "jinja") }}
Import the theme in your CSS file:
{% set code %}@import "tailwindcss";
@import "basecoat";
@import "theme.css";{% endset %}
{{ code_block(code, "css") }}
</li>
</ul>
<p>Documentation for each macro's parameters is available in the <a href="https://github.com/hunvreus/basecoat/tree/main/docs/src/_includes/components" target="_blank">source code</a>.</p>
<p><a href="https://ui.shadcn.com/docs/theming" target="_blank">Read more about theming on the shadcn/ui website.</a></p>
</div>
+600 -4
View File
@@ -8,11 +8,17 @@
"name": "basecoat",
"version": "0.1.0",
"license": "MIT",
"bin": {
"basecoat": "cli/index.js"
},
"devDependencies": {
"@11ty/eleventy": "^3.0.0",
"@grimlink/eleventy-plugin-lucide-icons": "^2.1.4",
"@tailwindcss/cli": "^4.1.3",
"commander": "^13.1.0",
"concurrently": "^9.1.2",
"fs-extra": "^11.3.0",
"inquirer": "^12.6.0",
"prettier": "^3.5.3",
"tailwindcss": "^4.1.3"
}
@@ -250,6 +256,384 @@
"lucide-static": "^0.486.0"
}
},
"node_modules/@inquirer/checkbox": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.5.tgz",
"integrity": "sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
"@inquirer/figures": "^1.0.11",
"@inquirer/type": "^3.0.6",
"ansi-escapes": "^4.3.2",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/confirm": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz",
"integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
"@inquirer/type": "^3.0.6"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/core": {
"version": "10.1.10",
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz",
"integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/figures": "^1.0.11",
"@inquirer/type": "^3.0.6",
"ansi-escapes": "^4.3.2",
"cli-width": "^4.1.0",
"mute-stream": "^2.0.0",
"signal-exit": "^4.1.0",
"wrap-ansi": "^6.2.0",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/core/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@inquirer/core/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/@inquirer/core/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@inquirer/core/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@inquirer/core/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@inquirer/editor": {
"version": "4.2.10",
"resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.10.tgz",
"integrity": "sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
"@inquirer/type": "^3.0.6",
"external-editor": "^3.1.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/expand": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.12.tgz",
"integrity": "sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
"@inquirer/type": "^3.0.6",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/figures": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz",
"integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@inquirer/input": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.9.tgz",
"integrity": "sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
"@inquirer/type": "^3.0.6"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/number": {
"version": "3.0.12",
"resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.12.tgz",
"integrity": "sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
"@inquirer/type": "^3.0.6"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/password": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.12.tgz",
"integrity": "sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
"@inquirer/type": "^3.0.6",
"ansi-escapes": "^4.3.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/prompts": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.5.0.tgz",
"integrity": "sha512-tk8Bx7l5AX/CR0sVfGj3Xg6v7cYlFBkEahH+EgBB+cZib6Fc83dwerTbzj7f2+qKckjIUGsviWRI1d7lx6nqQA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/checkbox": "^4.1.5",
"@inquirer/confirm": "^5.1.9",
"@inquirer/editor": "^4.2.10",
"@inquirer/expand": "^4.0.12",
"@inquirer/input": "^4.1.9",
"@inquirer/number": "^3.0.12",
"@inquirer/password": "^4.0.12",
"@inquirer/rawlist": "^4.1.0",
"@inquirer/search": "^3.0.12",
"@inquirer/select": "^4.2.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/rawlist": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.0.tgz",
"integrity": "sha512-6ob45Oh9pXmfprKqUiEeMz/tjtVTFQTgDDz1xAMKMrIvyrYjAmRbQZjMJfsictlL4phgjLhdLu27IkHNnNjB7g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
"@inquirer/type": "^3.0.6",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/search": {
"version": "3.0.12",
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.12.tgz",
"integrity": "sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
"@inquirer/figures": "^1.0.11",
"@inquirer/type": "^3.0.6",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/select": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.0.tgz",
"integrity": "sha512-KkXQ4aSySWimpV4V/TUJWdB3tdfENZUU765GjOIZ0uPwdbGIG6jrxD4dDf1w68uP+DVtfNhr1A92B+0mbTZ8FA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
"@inquirer/figures": "^1.0.11",
"@inquirer/type": "^3.0.6",
"ansi-escapes": "^4.3.2",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/type": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz",
"integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -965,6 +1349,22 @@
"node": ">=0.4.0"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"type-fest": "^0.21.3"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
@@ -1213,6 +1613,16 @@
"fsevents": "~2.3.2"
}
},
"node_modules/cli-width": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 12"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -1312,13 +1722,13 @@
"license": "MIT"
},
"node_modules/commander": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
"node": ">=18"
}
},
"node_modules/concat-map": {
@@ -1647,6 +2057,28 @@
"node": ">=0.10.0"
}
},
"node_modules/external-editor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
"integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
"dev": true,
"license": "MIT",
"dependencies": {
"chardet": "^0.7.0",
"iconv-lite": "^0.4.24",
"tmp": "^0.0.33"
},
"engines": {
"node": ">=4"
}
},
"node_modules/external-editor/node_modules/chardet": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -1760,6 +2192,21 @@
"node": ">= 0.8"
}
},
"node_modules/fs-extra": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1962,6 +2409,19 @@
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -1969,6 +2429,33 @@
"dev": true,
"license": "ISC"
},
"node_modules/inquirer": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.6.0.tgz",
"integrity": "sha512-3zmmccQd/8o65nPOZJZ+2wqt76Ghw3+LaMrmc6JE/IzcvQhJ1st+QLCOo/iLS85/tILU0myG31a2TAZX0ysAvg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.10",
"@inquirer/prompts": "^7.5.0",
"@inquirer/type": "^3.0.6",
"ansi-escapes": "^4.3.2",
"mute-stream": "^2.0.0",
"run-async": "^3.0.0",
"rxjs": "^7.8.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/is-alphabetical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
@@ -2135,6 +2622,19 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/junk": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/junk/-/junk-1.0.3.tgz",
@@ -2445,6 +2945,16 @@
"url": "https://opencollective.com/liquidjs"
}
},
"node_modules/liquidjs/node_modules/commander": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/list-to-array": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/list-to-array/-/list-to-array-1.1.0.tgz",
@@ -2677,6 +3187,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/mute-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
"integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -2755,6 +3275,16 @@
"node": ">= 0.8"
}
},
"node_modules/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -3023,6 +3553,16 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/run-async": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
"integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -3057,6 +3597,13 @@
"tslib": "^2.1.0"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"license": "MIT"
},
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
@@ -3367,6 +3914,19 @@
"node": ">=6"
}
},
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"os-tmpdir": "~1.0.2"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -3407,6 +3967,19 @@
"dev": true,
"license": "0BSD"
},
"node_modules/type-fest": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
@@ -3414,6 +3987,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -3647,6 +4230,19 @@
"engines": {
"node": ">=8"
}
},
"node_modules/yoctocolors-cjs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz",
"integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
}
}
+33 -4
View File
@@ -3,9 +3,35 @@
"version": "0.1.0",
"description": "shadcn/ui for the rest of us",
"type": "module",
"files": ["src"],
"exports": "./src/basecoat.css",
"keywords": ["tailwind", "tailwindcss", "css", "components"],
"files": [
"css",
"js",
"nunjucks",
"jinja",
"cli"
],
"bin": {
"basecoat": "./cli/index.js"
},
"exports": {
".": "./css/basecoat.css"
},
"keywords": [
"components",
"component library",
"component system",
"ui",
"ui kit",
"shadcn",
"shadcn/ui",
"tailwind",
"tailwindcss",
"css",
"html",
"jinja",
"nunjucks",
"alpinejs"
],
"license": "MIT",
"repository": {
"type": "git",
@@ -25,8 +51,11 @@
"@11ty/eleventy": "^3.0.0",
"@grimlink/eleventy-plugin-lucide-icons": "^2.1.4",
"@tailwindcss/cli": "^4.1.3",
"commander": "^13.1.0",
"concurrently": "^9.1.2",
"fs-extra": "^11.3.0",
"inquirer": "^12.6.0",
"prettier": "^3.5.3",
"tailwindcss": "^4.1.3"
}
}
}
+110
View File
@@ -0,0 +1,110 @@
{#
Renders a dialog component with optional trigger, header, body, and footer.
@param id {string} - Unique identifier for the dialog component.
@param trigger {string} [optional] - Text or HTML for the button that triggers the dialog.
@param title {string} [optional] - Title text displayed in the dialog header.
@param description {string} [optional] - Description text displayed below the title.
@param footer {string} [optional] - HTML content for the dialog footer.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
@param content_attrs {object} [optional] - Additional HTML attributes for the dialog content article.
@param content_header_attrs {object} [optional] - Additional HTML attributes for the dialog header.
@param content_body_attrs {object} [optional] - Additional HTML attributes for the dialog body section.
@param content_footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer.
@param open {boolean} [optional] [default=False] - Whether the dialog should be open initially.
@param close_button {boolean} [optional] [default=True] - Whether to include a close button.
@param close_on_overlay_click {boolean} [optional] [default=True] - Whether clicking the overlay closes the dialog.
#}
{% macro dialog(
id,
trigger=None,
title=None,
description=None,
footer=None,
main_attrs={},
trigger_attrs={},
content_attrs={},
content_header_attrs={},
content_body_attrs={},
content_footer_attrs={},
open=False,
close_button=True,
close_on_overlay_click=True
) %}
<div
id="{{ id }}"
x-data="dialog({{ 'true' if open else 'false' }}, {{ 'true' if close_on_overlay_click else 'false' }})"
x-bind="$main"
class="dialog"
{% for key, value in main_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if trigger %}
<button
type="button"
aria-expanded="false"
aria-controls="{{ id }}-dialog"
x-bind="$trigger"
{% for key, value in trigger_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ trigger }}
</button>
{% endif %}
<div
role="dialog"
id="{{ id }}-dialog"
tabindex="-1"
aria-modal="true"
aria-labelledby="{{ id }}-title"
inert
x-bind="$content"
>
<article
{% for key, value in content_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if title or description %}
<header
{% for key, value in content_header_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
<h2 id="{{ id }}-title">{{ title | safe }}</h2>
<p>{{ description | safe }}</p>
</header>
{% endif %}
{% if caller %}
<section
{% for key, value in content_body_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if caller %}{{ caller() }}{% endif %}
</section>
{% endif %}
{% if footer %}
<footer
{% for key, value in content_footer_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ footer | safe }}
</footer>
{% endif %}
{% if close_button %}
<button
@click="hide()"
aria-label="Close dialog"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
{% endif %}
</article>
</div>
</div>
{% endmacro %}
+57
View File
@@ -0,0 +1,57 @@
{#
Renders a dropdown menu component.
@param id {string} [optional] - Unique identifier for the dropdown component.
@param trigger {string} [optional] - HTML content for the button that triggers the dropdown.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
@param content_attrs {object} [optional] - Additional HTML attributes for the dropdown content div.
#}
{% macro dropdown_menu(
id=None,
trigger=None,
main_attrs={},
trigger_attrs={},
content_attrs={}
) %}
<div
class="popover {{ main_attrs.class }}"
x-data="dropdownMenu"
@click.away="open = false"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in main_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% if trigger %}
<button
type="button"
aria-haspopup="menu"
aria-expanded="false"
x-bind="$trigger"
{% if id %}
id="{{ id }}-trigger"
aria-controls="{{ id }}-menu"
{% endif %}
{% for key, value in trigger_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ trigger | safe }}
</button>
{% endif %}
<div
data-popover
aria-hidden="true"
x-bind="$content"
{% if id %}id="{{ id }}-menu"{% endif %}
{% for key, value in content_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
<nav role="menu">
{% if caller %}{{ caller() }}{% endif %}
</nav>
</div>
</div>
{% endmacro %}
+55
View File
@@ -0,0 +1,55 @@
{#
Renders a generic popover component, often used internally by other components like dropdowns or selects.
@param id {string} [optional] - Unique identifier for the popover component.
@param trigger {string} [optional] - HTML content for the element that triggers the popover.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger element.
@param content_attrs {object} [optional] - Additional HTML attributes for the popover content div.
#}
{% macro popover(
id=None,
trigger=None,
main_attrs=None,
trigger_attrs=None,
content_attrs=None
) %}
<div
class="popover {{ main_attrs.class }}"
x-data="popover"
@click.away="open = false"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in main_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% if trigger %}
<button
type="button"
aria-haspopup="menu"
aria-expanded="false"
x-bind="$trigger"
{% if id %}
id="{{ id }}-trigger"
aria-controls="{{ id }}-menu"
{% endif %}
{% for key, value in trigger_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ trigger | safe }}
</button>
{% endif %}
<div
data-popover
aria-hidden="true"
x-bind="$content"
{% if id %}id="{{ id }}-menu"{% endif %}
{% for key, value in content_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if caller %}{{ caller() }}{% endif %}
</div>
</div>
{% endmacro %}
+108
View File
@@ -0,0 +1,108 @@
{#
Renders a select or combobox component.
@param id {string} [optional] - Unique identifier for the select component.
@param selected {string} [optional] - The initially selected value.
@param name {string} [optional] - The name attribute for the hidden input storing the selected value.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
@param content_attrs {object} [optional] - Additional HTML attributes for the popover content div.
@param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
@param input_attrs {object} [optional] - Additional HTML attributes for the hidden input.
@param search_placeholder {string} [optional] [default="Search entries..."] - Placeholder text for the search input (combobox only).
@param is_combobox {boolean} [optional] [default=False] - Renders a combobox with search functionality if true.
#}
{% macro select(
id=None,
selected=None,
name=None,
main_attrs={},
trigger_attrs={},
content_attrs={},
listbox_attrs={},
input_attrs={},
search_placeholder="Search entries...",
is_combobox=False
) %}
<div
class="popover {{ main_attrs.class }}"
x-data="select('{{ name }}', '{{ selected or '' }}')"
@click.away="open = false"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in main_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<button
type="button"
aria-haspopup="listbox"
aria-expanded="false"
x-bind="$trigger"
{% if id %}
id="{{ id }}-trigger"
aria-controls="{{ id }}-content"
{% endif %}
class="btn-outline justify-between font-normal {{ trigger_attrs.class }}"
{% for key, value in trigger_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<div x-html="selectedLabel" class="flex items-center gap-x-2"
></div>
{% if is_combobox %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevrons-up-down-icon lucide-chevrons-up-down text-muted-foreground opacity-50 shrink-0"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
{% else %}
<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-chevron-down-icon lucide-chevron-down text-muted-foreground opacity-50 shrink-0"><path d="m6 9 6 6 6-6"/></svg>
{% endif %}
</button>
<div
data-popover
aria-hidden="true"
x-bind="$content"
{% if id %}id="{{ id }}-content"{% endif %}
{% for key, value in content_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if is_combobox %}
<header>
<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-search-icon lucide-search"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input
type="text"
value=""
placeholder="{{ search_placeholder }}"
autocomplete="off"
autocorrect="off"
spellcheck="false"
aria-autocomplete="list"
role="combobox"
aria-expanded="true"
aria-controls="{{ id }}-content"
aria-labelledby="{{ id }}-trigger"
x-model="query"
x-bind="$filter"
>
</header>
{% endif %}
<div
role="listbox"
aria-orientation="vertical"
{% for key, value in listbox_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if caller %}{{ caller() }}{% endif %}
</div>
</div>
{% if name is defined %}
<input
type="hidden"
name="{{ name }}"
x-model="selectedValue"
{% for key, value in input_attrs.items() %}
{% if key != 'name' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% endif %}
</div>
{% endmacro %}
+75
View File
@@ -0,0 +1,75 @@
{#
Renders a tabs component.
@param id {string} - Unique identifier for the tabs component.
@param tabsets {array} - An array of objects, each representing a tab and its panel.
Each object should have:
- id {string}: Unique identifier prefix for the tab and panel.
- tab {string}: HTML content for the tab button.
- panel {string} [optional]: HTML content for the tab panel.
- tab_attrs {object} [optional]: Additional HTML attributes for the tab button.
- panel_attrs {object} [optional]: Additional HTML attributes for the tab panel div.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param tablist_attrs {object} [optional] - Additional HTML attributes for the tablist nav element.
@param default_tab_index {number} [optional] [default=1] - The 1-based index of the tab to be active initially.
#}
{% macro tabs(
id,
tabsets=[],
main_attrs=None,
tablist_attrs=None,
default_tab_index=1
)
%}
<div
class="tabs {{ main_attrs.class }}"
x-data="tabs({{ default_tab_index - 1 }})"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in main_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<nav
role="tablist"
aria-orientation="horizontal"
x-bind="$tablist"
{% for key, value in tablist_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% for tabset in tabsets %}
<button
type="button"
role="tab"
id="{{ tabset.id }}-tab-{{ loop.index }}"
aria-controls="{{ tabset.id }}-panel-{{ loop.index }}"
aria-selected="{{ 'true' if loop.index == default_tab_index else 'false' }}"
tabindex="0"
{% for key, value in tabset.tab_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ tabset.tab | safe }}
</button>
{% endfor %}
</nav>
{% for tabset in tabsets %}
{% if tabset.panel %}
<div
role="tabpanel"
id="{{ tabset.id }}-panel-{{ loop.index }}"
aria-labelledby="{{ tabset.id }}-tab-{{ loop.index }}"
tabindex="-1"
aria-selected="{{ 'true' if loop.index == default_tab_index else 'false' }}"
{% if loop.index != default_tab_index %}hidden{% endif %}
{% for key, value in tabset.panel_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ tabset.panel | safe }}
</div>
{% endif %}
{% endfor %}
</div>
{% endmacro %}
+173
View File
@@ -0,0 +1,173 @@
{#
Renders a toaster container and individual toast messages.
Can render the initial container or be used with htmx OOB swaps to add toasts dynamically.
@param id {string} [optional] [default="toaster"] - Unique identifier for the toaster container.
@param toasts {array} [optional] - An array of toast objects to render initially.
Each toast object can have:
- category {string} [optional]: Type of toast ('success', 'error', 'info', 'warning'). Determines icon and ARIA role.
- title {string} [optional]: The main title text of the toast.
- description {string} [optional]: The secondary description text.
- duration {number} [optional] [default=5000]: Duration in milliseconds before the toast auto-dismisses.
- action {object} [optional]: Defines an action button.
- label {string}: Button text.
- click {string}: JavaScript code to execute on click (e.g., '$dispatch(\'custom-event\')').
- url {string}: URL for an anchor link button.
- cancel {object} [optional]: Defines a cancel/dismiss button (similar structure to action).
@param main_attrs {object} [optional] - Additional HTML attributes for the main toaster container div.
@param is_fragment {boolean} [optional] [default=False] - If true, renders only the toast elements with hx-swap-oob="beforeend", suitable for htmx responses. Skips script and template inclusion.
#}
{% macro toaster(
id="toaster",
toasts=[],
main_attrs=None,
is_fragment=False
) %}
{% set icons = {
'success': '<svg aria-hidden="true" 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-circle-check-icon lucide-circle-check"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>',
'error': '<svg aria-hidden="true" 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-circle-x-icon lucide-circle-x"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>',
'info': '<svg aria-hidden="true" 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-info-icon lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
'warning': '<svg aria-hidden="true" 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-triangle-alert-icon lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>'
} %}
<div
id="{{ id }}"
class="toaster"
{% for key, value in main_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
{% if is_fragment %}hx-swap-oob="beforeend"{% endif %}
>
{% for toast in toasts %}
<div
class="toast"
role="{{ 'alert' if toast.category == 'error' else 'status' }}"
aria-atomic="true"
aria-hidden="false"
{% if toast.category %}data-category="{{ toast.category }}"{% endif %}
x-data="toast('{{ toast.category }}', {{ toast.duration }})"
x-bind="$toastBindings"
>
<div class="toast-content">
<div class="flex items-center justify-between gap-x-3 p-4 [&>svg]:size-4 [&>svg]:shrink-0 [&>[role=img]]:size-4 [&>[role=img]]:shrink-0 [&>[role=img]>svg]:size-4">
{% if toast.category in ["error", "success", "info", "warning"] %}
{{ icons[toast.category] | safe }}
{% endif %}
<section class="flex-1 flex flex-col gap-0.5 items-start">
{% if toast.title %}
<h2 class="font-medium">{{ toast.title }}</h2>
{% endif %}
{% if toast.description %}
<p class="text-muted-foreground">{{ toast.description }}</p>
{% endif %}
</section>
{% if toast.action or toast.cancel %}
<footer class="flex flex-col gap-1 self-start">
{% if toast.action %}
{% if toast.action.click %}
<button
type="button"
class="btn h-6 text-xs px-2.5 rounded-sm"
@click="{{ toast.action.click }}"
>{{ toast.action.label }}</button>
{% elif toast.action.url %}
<a
href="{{ toast.action.url }}"
class="btn h-6 text-xs px-2.5 rounded-sm"
>{{ toast.action.label }}</a>
{% endif %}
{% endif %}
{% if toast.cancel %}
{% if toast.cancel.click %}
<button
type="button"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
@click="{{ toast.cancel.click }}"
>{{ toast.cancel.label }}</button>
{% elif toast.cancel.url %}
<a
href="{{ toast.cancel.url }}"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
>{{ toast.cancel.label }}</a>
{% endif %}
{% endif %}
</footer>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% if not is_fragment %}
<template id="toast-template">
<div
class="toast"
role="status"
aria-atomic="true"
x-bind="$toastBindings"
>
<div class="toast-content">
<div class="flex items-center justify-between gap-x-3 p-4 [&>svg]:size-4 [&>svg]:shrink-0 [&>[role=img]]:size-4 [&>[role=img]]:shrink-0 [&>[role=img]>svg]:size-4">
<template x-if="config.icon">
<span aria-hidden="true" role="img" x-html="config.icon"></span>
</template>
<template x-if="!config.icon && config.category === 'success'">
{{ icons.success | safe }}
</template>
<template x-if="!config.icon && config.category === 'error'">
{{ icons.error | safe }}
</template>
<template x-if="!config.icon && config.category === 'info'">
{{ icons.info | safe }}
</template>
<template x-if="!config.icon && config.category === 'warning'">
{{ icons.warning | safe }}
</template>
<section class="flex-1 flex flex-col gap-0.5 items-start">
<template x-if="config.title">
<h2 class="font-medium" x-text="config.title"></h2>
</template>
<template x-if="config.description">
<p class="text-muted-foreground" x-text="config.description"></p>
</template>
</section>
<template x-if="config.action || config.cancel">
<footer class="flex flex-col gap-1 self-start">
<template x-if="config.action?.click">
<button
type="button"
class="btn h-6 text-xs px-2.5 rounded-sm"
@click="$evaluate(config.action.click)"
x-text="config.action.label"
></button>
</template>
<template x-if="config.action?.url">
<a
:href="config.action.url"
class="btn h-6 text-xs px-2.5 rounded-sm"
x-text="config.action.label"
></a>
</template>
<template x-if="config.cancel?.click">
<button
type="button"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
@click="$evaluate(config.cancel.click)"
x-text="config.cancel.label"
></button>
</template>
<template x-if="config.cancel?.url">
<a
:href="config.cancel.url"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
x-text="config.cancel.label"
></a>
</template>
</footer>
</div>
</div>
</div>
</template>
{% endif %}
{% endmacro %}
+54
View File
@@ -0,0 +1,54 @@
window.basecoat = window.basecoat || {};
window.basecoat.registerDialog = function(Alpine) {
if (Alpine.components && Alpine.components.dialog) return;
Alpine.data('dialog', (initialOpen = false, initialCloseOnOverlayClick = true) => ({
id: null,
open: false,
closeOnOverlayClick: true,
init() {
this.id = this.$el.id;
if (!this.id) {
console.warn('Dialog component initialized without an `id`. This may cause issues with event targeting and accessibility.');
}
this.open = initialOpen;
this.closeOnOverlayClick = initialCloseOnOverlayClick;
},
show() {
if (!this.open) {
this.open = true;
this.$nextTick(() => {
this.$el.dispatchEvent(new CustomEvent('dialog:opened', { bubbles: true, detail: { id: this.id } }));
setTimeout(() => {
const focusTarget = this.$refs.focusOnOpen || this.$el.querySelector('[role="dialog"]');
if (focusTarget) focusTarget.focus();
}, 50);
});
}
},
hide() {
if (this.open) {
this.open = false;
this.$nextTick(() => {
this.$el.dispatchEvent(new CustomEvent('dialog:closed', { bubbles: true, detail: { id: this.id } }));
});
}
},
$main: {
'@dialog:open.window'(e) { if (e.detail && e.detail.id === this.id) this.show() },
'@dialog:close.window'(e) { if (e.detail && e.detail.id === this.id) this.hide() },
'@keydown.escape.window'() { this.open && this.hide(); },
},
$trigger: {
'@click'() { this.show() },
':aria-expanded'() { return this.open }
},
$content: {
':inert'() { return !this.open },
'@click.self'() { if (this.closeOnOverlayClick) this.hide() },
'x-cloak': ''
}
}));
};
+91
View File
@@ -0,0 +1,91 @@
window.basecoat = window.basecoat || {};
window.basecoat.registerDropdownMenu = function(Alpine) {
if (Alpine.components && Alpine.components.dropdownMenu) return;
Alpine.data('dropdownMenu', () => ({
open: false,
focusedIndex: null,
menuItems: [],
init() {
this.$nextTick(() => {
this.menuItems = Array.from(this.$el.querySelectorAll('[role=menuitem]:not([disabled]),[role=menuitemcheckbox]:not([disabled]),[role=menuitemradio]:not([disabled])'));
});
},
focusMenuitem() {
if (this.menuItems.length === 0) return;
if (this.focusedIndex >= this.menuItems.length) {
this.focusedIndex = this.menuItems.length - 1;
} else if (this.focusedIndex < 0 || this.focusedIndex === null) {
this.focusedIndex = 0;
}
this.menuItems.forEach(item => item.blur());
this.menuItems[this.focusedIndex].focus();
},
moveMenuitemFocus(delta) {
if (this.menuItems.length === 0) return;
let wasOpen = this.open;
if (!this.open) {
this.open = true;
this.focusedIndex = 0;
} else {
this.focusedIndex = this.focusedIndex === null
? 0
: this.focusedIndex + delta;
}
if (wasOpen) {
this.focusMenuitem();
} else {
setTimeout(() => this.focusMenuitem(), 50);
}
},
handleMenuitemClick(event) {
const menuitem = event.target.closest('[role=menuitem],[role=menuitemcheckbox],[role=menuitemradio]');
if (menuitem && menuitem.getAttribute('aria-disabled') !== 'true' && !menuitem.disabled) {
this.$nextTick(() => {
this.$refs.trigger.focus();
this.open = false;
});
}
},
handleMenuitemMousemove(event) {
const menuitem = event.target.closest('[role=menuitem],[role=menuitemcheckbox],[role=menuitemradio]');
if (menuitem && menuitem.getAttribute('aria-disabled') !== 'true' && !menuitem.disabled) {
this.focusedIndex = this.menuItems.indexOf(menuitem);
this.focusMenuitem();
}
},
$trigger: {
'@click'() { this.open = !this.open },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveMenuitemFocus(+1); },
'@keydown.up.prevent'() { this.moveMenuitemFocus(-1); },
'@keydown.home.prevent'() { this.focusMenuitem(0) },
'@keydown.end.prevent'() { this.focusMenuitem(this.menuItems.length - 1) },
'@keydown.enter.prevent'() { this.open = !this.open },
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@click'(e) { this.handleMenuitemClick(e) },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveMenuitemFocus(+1); },
'@keydown.up.prevent'() { this.moveMenuitemFocus(-1); },
'@keydown.home.prevent'() { this.focusMenuitem(0) },
'@keydown.end.prevent'() { this.focusMenuitem(this.menuItems.length - 1) },
'@mousemove'(e) { this.handleMenuitemMousemove(e) },
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
}));
};
+26
View File
@@ -0,0 +1,26 @@
window.basecoat = window.basecoat || {};
window.basecoat.registerPopover = function(Alpine) {
if (Alpine.components && Alpine.components.popover) return;
Alpine.data('popover', () => ({
open: false,
$trigger: {
'@click'() { this.open = !this.open },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
}));
};
+150
View File
@@ -0,0 +1,150 @@
window.basecoat = window.basecoat || {};
window.basecoat.registerSelect = function(Alpine) {
if (Alpine.components && Alpine.components.select) return;
Alpine.data('select', (name = null, initialValue = null) => ({
open: false,
name: null,
options: [],
disabledOptions: [],
focusedIndex: null,
selectedLabel: null,
selectedValue: null,
query: '',
init() {
this.$nextTick(() => {
if (name) this.name = name;
this.options = Array.from(this.$el.querySelectorAll('[role=option]:not([aria-disabled])'));
this.disabledOptions = Array.from(this.$el.querySelectorAll('[role=option][aria-disabled=true]'));
if (this.options.length === 0) return;
if (initialValue) {
const option = this.options.find(opt => opt.getAttribute('data-value') === initialValue);
this.selectOption(option, false);
this.focusedIndex = this.options.indexOf(option);
} else {
this.selectOption(this.options[0], false);
}
});
},
focusOption() {
if (this.options.length === 0) return;
if (this.focusedIndex >= this.options.length) {
this.focusedIndex = this.options.length - 1;
} else if (this.focusedIndex < 0 || this.focusedIndex === null) {
this.focusedIndex = 0;
}
this.options.forEach(opt => opt.removeAttribute('data-focus'));
const option = this.options[this.focusedIndex];
option.setAttribute('data-focus', '');
option.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
},
focusOnSelectedOption() {
if (this.options.length === 0 || this.selectedValue === null) return;
const option = this.options.find(opt => opt.getAttribute('data-value') === this.selectedValue);
if (!option) return;
this.focusedIndex = this.options.indexOf(option);
this.focusOption();
},
moveOptionFocus(delta) {
if (this.options.length === 0) return;
if (!this.open) {
this.open = true;
this.focusOnSelectedOption();
} else {
this.focusedIndex = this.focusedIndex === null
? 0
: this.focusedIndex + delta;
}
this.focusOption();
},
handleOptionClick(event) {
const option = event.target.closest('[role=option]');
if (option && option.getAttribute('aria-disabled') !== 'true') {
this.selectOption(option);
this.open = false;
this.$nextTick(() => this.$refs.trigger.focus());
}
},
handleOptionMousemove(event) {
const option = event.target.closest('[role=option]');
if (option && option.getAttribute('aria-disabled') !== 'true') {
this.focusedIndex = this.options.indexOf(option);
this.focusOption();
}
},
handleOptionEnter(event) {
this.selectOption(this.options[this.focusedIndex]);
this.open = !this.open;
},
selectOption(option, dispatch = true) {
if (this.options.length === 0 || !option || option.disabled || option.getAttribute('data-value') == null) {
return;
}
this.options.forEach(opt => {
opt.setAttribute('aria-selected', opt === option);
});
this.selectedLabel = option.innerHTML;
this.selectedValue = option.getAttribute('data-value');
if (dispatch) {
this.$dispatch('select:change', {
value: this.selectedValue,
label: this.selectedLabel
});
this.$dispatch('change');
}
},
filterOptions(query) {
if (query.length > 0) {
this.disabledOptions.forEach(opt => { opt.setAttribute('aria-hidden', 'true'); });
} else {
this.disabledOptions.forEach(opt => { opt.removeAttribute('aria-hidden'); });
}
this.options.forEach(opt => {
opt.removeAttribute('aria-hidden');
if (opt.getAttribute('data-value') != null && !opt.innerHTML.toLowerCase().includes(query.toLowerCase())) {
opt.setAttribute('aria-hidden', 'true');
}
});
},
$trigger: {
'@click'() { this.open = !this.open; this.focusOnSelectedOption(); },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveOptionFocus(+1); },
'@keydown.up.prevent'() { this.moveOptionFocus(-1); },
'@keydown.home.prevent'() { this.focusOption(0) },
'@keydown.end.prevent'() { this.focusOption(this.options.length - 1) },
'@keydown.enter.prevent'() {
if (this.open) this.handleOptionEnter();
else this.open = true;
},
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@click'(e) { this.handleOptionClick(e) },
'@mousemove'(e) { this.handleOptionMousemove(e) },
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
$filter: {
'@input'(e) { this.filterOptions(e.target.value) },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveOptionFocus(+1); },
'@keydown.up.prevent'() { this.moveOptionFocus(-1); },
'@keydown.home.prevent'() { this.focusOption(0) },
'@keydown.end.prevent'() { this.focusOption(this.options.length - 1) },
'@keydown.enter.prevent'() { this.handleOptionEnter() },
}
}));
};
+64
View File
@@ -0,0 +1,64 @@
window.basecoat = window.basecoat || {};
window.basecoat.registerTabs = function(Alpine) {
if (Alpine.components && Alpine.components.tabs) return;
Alpine.data('tabs', (initialTabIndex = 0) => ({
activeTabIndex: initialTabIndex,
tabs: [],
panels: [],
init() {
this.$nextTick(() => {
this.tabs = Array.from(this.$el.querySelectorAll(':scope > [role=tablist] [role=tab]:not([disabled])'));
this.panels = Array.from(this.$el.querySelectorAll(':scope > [role=tabpanel]'));
if (this.tabs.length > 0) {
this.selectTab(this.tabs[initialTabIndex], false);
}
});
},
nextTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex + 1) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
},
prevTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex - 1 + this.tabs.length) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
},
selectTab(tab, focus = true) {
if (!tab || this.tabs.length === 0) return;
this.tabs.forEach((t, index) => {
const isSelected = t === tab;
t.setAttribute('aria-selected', isSelected);
t.setAttribute('tabindex', isSelected ? '0' : '-1');
if (isSelected) {
this.activeTabIndex = index;
this.activeTab = t;
if (focus) {
t.focus();
}
}
});
const panelId = tab.getAttribute('aria-controls');
if (!panelId) return;
this.panels.forEach(panel => {
panel.hidden = (panel.getAttribute('id') !== panelId);
});
},
$tablist: {
['@click'](event) {
const clickedTab = event.target.closest('[role=tab]');
if (clickedTab) {
this.selectTab(clickedTab);
}
},
['@keydown.arrow-right.prevent']() { this.nextTab() },
['@keydown.arrow-left.prevent']() { this.prevTab() },
},
}));
};
+68
View File
@@ -0,0 +1,68 @@
window.basecoat = window.basecoat || {};
window.basecoat.registerToast = function(Alpine) {
if (Alpine.components && Alpine.components.toast) return;
Alpine.store('toaster', { isPaused: false });
Alpine.data('toast', (config={}) => ({
config: config,
open: false,
timeoutDuration: null,
timeoutId: null,
init() {
this.timeoutDuration = config.duration || (config.category === 'error' ? 5000 : 3000);
this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
this.open = true;
this.$watch('$store.toaster.isPaused', (isPaused) => {
if (!this.open) return;
if (isPaused) {
this.pauseTimeout();
} else {
this.resumeTimeout();
}
});
},
pauseTimeout() {
clearTimeout(this.timeoutId);
this.timeoutId = null;
},
resumeTimeout(index) {
if (this.open && this.timeoutId === null) {
this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
}
},
close() {
this.pauseTimeout();
this.open = false;
this.$el.blur();
},
$toastBindings: {
['@mouseenter']() { this.$store.toaster.isPaused = true },
['@mouseleave']() { this.$store.toaster.isPaused = false },
['@keydown.escape.prevent']() { this.close() },
[':aria-hidden']() { return !this.open }
},
}));
Alpine.magic('toast', (el) => (config, toasterId='toaster') => {
const toaster = document.getElementById(toasterId);
const template = document.getElementById('toast-template');
if (!toaster) {
console.error(`Toaster container with id #${targetId} not found.`);
return;
}
if (!template) {
console.error('Toast template with id #toast-template not found.');
return;
}
const clone = template.content.firstElementChild.cloneNode(true);
clone.setAttribute('x-data', `toast(${JSON.stringify(config)})`);
clone.removeAttribute('id');
toaster.appendChild(clone);
});
};
@@ -15,8 +15,6 @@
@param open {boolean} [optional] [default=false] - Whether the dialog should be open initially.
@param close_button {boolean} [optional] [default=true] - Whether to include a close button.
@param close_on_overlay_click {boolean} [optional] [default=true] - Whether clicking the overlay closes the dialog.
@param include_script {boolean} [optional] [default=true] - Whether to include the Alpine.js component script.
@param register_on {array} [optional] [default=["alpine:init"]] - Alpine.js events to register the component on.
#}
{% macro dialog(
id,
@@ -32,14 +30,8 @@
content_footer_attrs={},
open=false,
close_button=true,
close_on_overlay_click=true,
include_script=false,
register_on=["alpine:init"]
close_on_overlay_click=true
) %}
{% if include_script %}
{% include "components/dialog-script.njk" %}
{% endif %}
<div
id="{{ id }}"
x-data="dialog({{ 'true' if open else 'false' }}, {{ 'true' if close_on_overlay_click else 'false' }})"
@@ -65,7 +57,6 @@
<div
role="dialog"
id="{{ id }}-dialog"
x-ref="dialog"
tabindex="-1"
aria-modal="true"
aria-labelledby="{{ id }}-title"
@@ -6,22 +6,14 @@
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
@param content_attrs {object} [optional] - Additional HTML attributes for the dropdown content div.
@param include_script {boolean} [optional] [default=false] - Whether to include the Alpine.js component script.
@param register_on {array} [optional] [default=[\"alpine:init\"]] - Alpine.js events to register the component on.
#}
{% macro dropdown_menu(
id=None,
trigger=None,
main_attrs={},
trigger_attrs={},
content_attrs={},
include_script=false,
register_on=["alpine:init"]
content_attrs={}
) %}
{% if include_script %}
{% include "components/dropdown-menu-script.njk" %}
{% endif %}
<div
class="popover {{ main_attrs.class }}"
x-data="dropdownMenu"
@@ -6,22 +6,14 @@
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger element.
@param content_attrs {object} [optional] - Additional HTML attributes for the popover content div.
@param include_script {boolean} [optional] [default=false] - Whether to include the Alpine.js component script.
@param register_on {array} [optional] [default=[\"alpine:init\"]] - Alpine.js events to register the component on.
#}
{% macro popover(
id=None,
trigger=None,
main_attrs=None,
trigger_attrs=None,
content_attrs=None,
include_script=false,
register_on=["alpine:init"]
content_attrs=None
) %}
{% if include_script %}
{% include "components/popover-script.njk" %}
{% endif %}
<div
class="popover {{ main_attrs.class }}"
x-data="popover"
@@ -11,8 +11,6 @@
@param input_attrs {object} [optional] - Additional HTML attributes for the hidden input.
@param search_placeholder {string} [optional] [default="Search entries..."] - Placeholder text for the search input (combobox only).
@param is_combobox {boolean} [optional] [default=false] - Renders a combobox with search functionality if true.
@param include_script {boolean} [optional] [default=false] - Whether to include the Alpine.js component script.
@param register_on {array} [optional] [default=[\"alpine:init\"]] - Alpine.js events to register the component on.
#}
{% macro select(
id=None,
@@ -24,14 +22,8 @@
listbox_attrs={},
input_attrs={},
search_placeholder="Search entries...",
is_combobox=false,
include_script=false,
register_on=["alpine:init"]
is_combobox=false
) %}
{% if include_script %}
{% include "components/select-script.njk" %}
{% endif %}
<div
class="popover {{ main_attrs.class }}"
x-data="select('{{ name }}', '{{ selected or '' }}')"
@@ -12,23 +12,15 @@
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param tablist_attrs {object} [optional] - Additional HTML attributes for the tablist nav element.
@param default_tab_index {number} [optional] [default=1] - The 1-based index of the tab to be active initially.
@param include_script {boolean} [optional] [default=false] - Whether to include the Alpine.js component script.
@param register_on {array} [optional] [default=[\"alpine:init\"]] - Alpine.js events to register the component on.
#}
{% macro tabs(
id,
tabsets=[],
main_attrs=None,
tablist_attrs=None,
default_tab_index=1,
include_script=false,
register_on=["alpine:init"]
default_tab_index=1
)
%}
{% if include_script %}
{% include "components/tabs-script.njk" %}
{% endif %}
<div
class="tabs {{ main_attrs.class }}"
x-data="tabs({{ default_tab_index - 1 }})"
@@ -16,21 +16,13 @@
- cancel {object} [optional]: Defines a cancel/dismiss button (similar structure to action).
@param main_attrs {object} [optional] - Additional HTML attributes for the main toaster container div.
@param is_fragment {boolean} [optional] [default=false] - If true, renders only the toast elements with hx-swap-oob="beforeend", suitable for htmx responses. Skips script and template inclusion.
@param include_script {boolean} [optional] [default=false] - Whether to include the Alpine.js component script. Ignored if is_fragment is true.
@param register_on {array} [optional] [default=[\"alpine:init\"]] - Alpine.js events to register the component on. Ignored if is_fragment is true.
#}
{% macro toaster(
id="toaster",
toasts=[],
main_attrs=None,
is_fragment=false,
include_script=false,
register_on=["alpine:init"]
is_fragment=false
) %}
{% if not is_fragment and include_script %}
{% include "components/toast-script.njk" %}
{% endif %}
{% set icons = {
'success': '<svg aria-hidden="true" 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-circle-check-icon lucide-circle-check"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>',
'error': '<svg aria-hidden="true" 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-circle-x-icon lucide-circle-x"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>',