mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-11 04:38:18 +00:00
Working on CLI
This commit is contained in:
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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><img></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><img></code> element. More examples can be found in the <a href="/kitchen-sink#avatar">kitchen sink</a>.</p>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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) }}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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><header></code> for the card header, which can contain a <code><h2></code> for the title and a <code><p></code> for the description.</li>
|
||||
|
||||
@@ -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><input type="radio"></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>
|
||||
|
||||
|
||||
@@ -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><div data-popover></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."},
|
||||
|
||||
@@ -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><div class="dialog"></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>
|
||||
|
||||
@@ -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><div class="popover"></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",
|
||||
|
||||
@@ -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><label></code>, <code><input></code>, <code><textarea></code>, <code><select></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>
|
||||
|
||||
|
||||
@@ -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><input></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>
|
||||
|
||||
|
||||
@@ -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><label></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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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><div class="popover"></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(
|
||||
|
||||
@@ -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><input type="radio"></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>
|
||||
|
||||
|
||||
@@ -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><select></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><select></code> allows (e.g. HTML options), you can use this component. It is structured as such:</p>
|
||||
<ul>
|
||||
<li>A <code><div class="popover"></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">
|
||||
|
||||
@@ -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) }}
|
||||
|
||||
|
||||
@@ -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><input type="range"></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>
|
||||
|
||||
|
||||
@@ -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><input type="checkbox"></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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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><div class="tabs"></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">
|
||||
|
||||
@@ -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><textarea></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>
|
||||
|
||||
|
||||
@@ -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,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,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,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,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
@@ -2,6 +2,4 @@
|
||||
layout: layouts/page.njk
|
||||
---
|
||||
|
||||
What is it?
|
||||
|
||||
Who is it for?
|
||||
HELLO
|
||||
+22
-29
@@ -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><script></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><script></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>
|
||||
|
||||
Generated
+600
-4
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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': ''
|
||||
}
|
||||
}));
|
||||
};
|
||||
@@ -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': ''
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -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': ''
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -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() },
|
||||
}
|
||||
}));
|
||||
};
|
||||
@@ -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() },
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -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>',
|
||||
Reference in New Issue
Block a user