diff --git a/docs/src/_includes/layouts/base.njk b/docs/src/_includes/layouts/base.njk index 16976b6..444b061 100644 --- a/docs/src/_includes/layouts/base.njk +++ b/docs/src/_includes/layouts/base.njk @@ -69,6 +69,7 @@ {# CSS #} {# JS #} + @@ -94,6 +95,12 @@ document.addEventListener('htmx:afterSwap', highlight); } + {% if head %}{{ head | safe }}{% endif %}
diff --git a/docs/src/assets/styles.css b/docs/src/assets/styles.css index f4bcd6f..3b784da 100644 --- a/docs/src/assets/styles.css +++ b/docs/src/assets/styles.css @@ -2465,61 +2465,61 @@ justify-content: flex-end; } } - > form[method='dialog'] { + > button, > form[method='dialog'] { position: absolute; top: calc(var(--spacing) * 4); right: calc(var(--spacing) * 4); - > button { - border-radius: var(--radius-xs); - opacity: 70%; - --tw-ring-offset-color: var(--color-background); - transition-property: opacity; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - &:hover { - @media (hover: hover) { - opacity: 100%; - } + } + > button, > form[method='dialog'] > button { + border-radius: var(--radius-xs); + opacity: 70%; + --tw-ring-offset-color: var(--color-background); + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + &:hover { + @media (hover: hover) { + opacity: 100%; } - &:focus { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - &:focus { - --tw-ring-color: var(--color-ring); - } - &:focus { - --tw-ring-offset-width: 2px; - --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); - } - &:focus { - --tw-outline-style: none; - outline-style: none; - @media (forced-colors: active) { - outline: 2px solid transparent; - outline-offset: 2px; - } - } - &:disabled { - pointer-events: none; - } - &[data-state="open"] { - background-color: var(--color-accent); - } - &[data-state="open"] { - color: var(--color-muted-foreground); - } - & svg { - pointer-events: none; - } - & svg { - flex-shrink: 0; - } - & svg:not([class*='size-']) { - width: calc(var(--spacing) * 4); - height: calc(var(--spacing) * 4); + } + &:focus { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + &:focus { + --tw-ring-color: var(--color-ring); + } + &:focus { + --tw-ring-offset-width: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + } + &:focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; } } + &:disabled { + pointer-events: none; + } + &[data-state="open"] { + background-color: var(--color-accent); + } + &[data-state="open"] { + color: var(--color-muted-foreground); + } + & svg { + pointer-events: none; + } + & svg { + flex-shrink: 0; + } + & svg:not([class*='size-']) { + width: calc(var(--spacing) * 4); + height: calc(var(--spacing) * 4); + } } } } diff --git a/docs/src/components/combobox.njk b/docs/src/components/combobox.njk index c7c4cc1..12f5c16 100644 --- a/docs/src/components/combobox.njk +++ b/docs/src/components/combobox.njk @@ -60,7 +60,8 @@ toc:You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:
<footer> Optional<form method="dialog"> Optional<button type="button" onclick="this.closest('dialog').close()"> Optional<form method="dialog"> element.You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:
You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:
You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:
You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:
You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:
You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:
Then reference the script you need:
+Then reference the script you need as well as the basecoat.min.js file (used to register and initialize components):
See each component page for the minimal script required.
diff --git a/package.json b/package.json index 9e9cdec..07fd75c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "basecoat", - "version": "0.2.8", + "version": "0.3.0", "description": "shadcn/ui for the rest of us", "author": "hunvreus", "type": "module", diff --git a/packages/cli/package.json b/packages/cli/package.json index 2336472..23d9447 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "basecoat-cli", - "version": "0.2.8", + "version": "0.3.0", "description": "Add Basecoat components to your project", "author": "hunvreus", "license": "MIT", diff --git a/packages/css/package.json b/packages/css/package.json index 28b208a..6a6d3fc 100644 --- a/packages/css/package.json +++ b/packages/css/package.json @@ -1,6 +1,6 @@ { "name": "basecoat-css", - "version": "0.2.8", + "version": "0.3.0", "description": "Tailwind CSS for Basecoat components", "author": { "name": "hunvreus", diff --git a/scripts/build.js b/scripts/build.js index 5eadfff..f0661b5 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -121,7 +121,7 @@ async function build() { // Create combined component files console.log('Creating combined component files...'); - const componentsToCombine = ['dropdown-menu.js', 'popover.js', 'select.js', 'sidebar.js', 'tabs.js', 'toast.js']; + const componentsToCombine = ['basecoat.js', 'dropdown-menu.js', 'popover.js', 'select.js', 'sidebar.js', 'tabs.js', 'toast.js']; const componentPaths = componentsToCombine.map(f => path.join(srcJsDir, f)); // Create non-minified bundle diff --git a/src/css/basecoat.css b/src/css/basecoat.css index 098f84d..483934e 100644 --- a/src/css/basecoat.css +++ b/src/css/basecoat.css @@ -491,11 +491,13 @@ > footer { @apply flex flex-col-reverse gap-2 sm:flex-row sm:justify-end; } + > button, > form[method='dialog'] { @apply absolute top-4 right-4; - > button { - @apply ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4; - } + } + > button, + > form[method='dialog'] > button { + @apply ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4; } } } diff --git a/src/js/basecoat.js b/src/js/basecoat.js new file mode 100644 index 0000000..1855b4b --- /dev/null +++ b/src/js/basecoat.js @@ -0,0 +1,99 @@ +(() => { + const componentRegistry = {}; + let observer = null; + + const registerComponent = (name, selector, initFunction) => { + componentRegistry[name] = { + selector, + init: initFunction + }; + }; + + const initComponent = (element, componentName) => { + const component = componentRegistry[componentName]; + if (!component) return; + + try { + component.init(element); + } catch (error) { + console.error(`Failed to initialize ${componentName}:`, error); + } + }; + + const initAllComponents = () => { + Object.entries(componentRegistry).forEach(([name, { selector, init }]) => { + document.querySelectorAll(selector).forEach(init); + }); + }; + + const initNewComponents = (node) => { + if (node.nodeType !== Node.ELEMENT_NODE) return; + + Object.entries(componentRegistry).forEach(([name, { selector, init }]) => { + if (node.matches(selector)) { + init(node); + } + node.querySelectorAll(selector).forEach(init); + }); + }; + + const startObserver = () => { + if (observer) return; + + observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach(initNewComponents); + }); + }); + + observer.observe(document.body, { childList: true, subtree: true }); + }; + + const stopObserver = () => { + if (observer) { + observer.disconnect(); + observer = null; + } + }; + + const reinitComponent = (componentName) => { + const component = componentRegistry[componentName]; + if (!component) { + console.warn(`Component '${componentName}' not found in registry`); + return; + } + + // Clear initialization flag for this component + const flag = `data-${componentName}-initialized`; + document.querySelectorAll(`[${flag}]`).forEach(el => { + el.removeAttribute(flag); + }); + + document.querySelectorAll(component.selector).forEach(component.init); + }; + + const reinitAll = () => { + // Clear all initialization flags using the registry + Object.entries(componentRegistry).forEach(([name, { selector }]) => { + const flag = `data-${name}-initialized`; + document.querySelectorAll(`[${flag}]`).forEach(el => { + el.removeAttribute(flag); + }); + }); + + initAllComponents(); + }; + + window.basecoat = { + register: registerComponent, + init: reinitComponent, + initAll: reinitAll, + start: startObserver, + stop: stopObserver + }; + + document.addEventListener('DOMContentLoaded', () => { + initAllComponents(); + startObserver(); + }); +})(); \ No newline at end of file diff --git a/src/js/dropdown-menu.js b/src/js/dropdown-menu.js index b1145e6..f261ba4 100644 --- a/src/js/dropdown-menu.js +++ b/src/js/dropdown-menu.js @@ -165,19 +165,7 @@ dropdownMenuComponent.dispatchEvent(new CustomEvent('basecoat:initialized')); }; - document.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu); - - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - mutation.addedNodes.forEach((node) => { - if (node.nodeType !== Node.ELEMENT_NODE) return; - if (node.matches('.dropdown-menu:not([data-dropdown-menu-initialized])')) { - initDropdownMenu(node); - } - node.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu); - }); - }); - }); - - observer.observe(document.body, { childList: true, subtree: true }); + if (window.basecoat) { + window.basecoat.register('dropdown-menu', '.dropdown-menu:not([data-dropdown-menu-initialized])', initDropdownMenu); + } })(); \ No newline at end of file diff --git a/src/js/popover.js b/src/js/popover.js index b1220b9..3786f47 100644 --- a/src/js/popover.js +++ b/src/js/popover.js @@ -67,19 +67,7 @@ popoverComponent.dispatchEvent(new CustomEvent('basecoat:initialized')); }; - document.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover); - - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - mutation.addedNodes.forEach((node) => { - if (node.nodeType !== Node.ELEMENT_NODE) return; - if (node.matches('.popover:not([data-popover-initialized])')) { - initPopover(node); - } - node.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover); - }); - }); - }); - - observer.observe(document.body, { childList: true, subtree: true }); + if (window.basecoat) { + window.basecoat.register('popover', '.popover:not([data-popover-initialized])', initPopover); + } })(); diff --git a/src/js/select.js b/src/js/select.js index 952600b..b648f28 100644 --- a/src/js/select.js +++ b/src/js/select.js @@ -272,20 +272,7 @@ selectComponent.dispatchEvent(new CustomEvent('basecoat:initialized')); }; - document.querySelectorAll('div.select:not([data-select-initialized])').forEach(initSelect); - - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - mutation.addedNodes.forEach((node) => { - if (node.nodeType === Node.ELEMENT_NODE) { - if (node.matches('div.select:not([data-select-initialized])')) { - initSelect(node); - } - node.querySelectorAll('div.select:not([data-select-initialized])').forEach(initSelect); - } - }); - }); - }); - - observer.observe(document.body, { childList: true, subtree: true }); + if (window.basecoat) { + window.basecoat.register('select', 'div.select:not([data-select-initialized])', initSelect); + } })(); \ No newline at end of file diff --git a/src/js/sidebar.js b/src/js/sidebar.js index b11076b..b327524 100644 --- a/src/js/sidebar.js +++ b/src/js/sidebar.js @@ -98,20 +98,7 @@ sidebarComponent.dispatchEvent(new CustomEvent('basecoat:initialized')); }; - document.querySelectorAll('.sidebar:not([data-sidebar-initialized])').forEach(initSidebar); - - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - mutation.addedNodes.forEach((node) => { - if (node.nodeType === Node.ELEMENT_NODE) { - if (node.matches('.sidebar:not([data-sidebar-initialized])')) { - initSidebar(node); - } - node.querySelectorAll('.sidebar:not([data-sidebar-initialized])').forEach(initSidebar); - } - }); - }); - }); - - observer.observe(document.body, { childList: true, subtree: true }); + if (window.basecoat) { + window.basecoat.register('sidebar', '.sidebar:not([data-sidebar-initialized])', initSidebar); + } })(); \ No newline at end of file diff --git a/src/js/tabs.js b/src/js/tabs.js index 8844958..01df568 100644 --- a/src/js/tabs.js +++ b/src/js/tabs.js @@ -57,20 +57,7 @@ tabsComponent.dispatchEvent(new CustomEvent('basecoat:initialized')); }; - document.querySelectorAll('.tabs:not([data-tabs-initialized])').forEach(initTabs); - - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - mutation.addedNodes.forEach((node) => { - if (node.nodeType === Node.ELEMENT_NODE) { - if (node.matches('.tabs:not([data-tabs-initialized])')) { - initTabs(node); - } - node.querySelectorAll('.tabs:not([data-tabs-initialized])').forEach(initTabs); - } - }); - }); - }); - - observer.observe(document.body, { childList: true, subtree: true }); + if (window.basecoat) { + window.basecoat.register('tabs', '.tabs:not([data-tabs-initialized])', initTabs); + } })(); \ No newline at end of file diff --git a/src/js/toast.js b/src/js/toast.js index 3cbd5d8..59ea4c5 100644 --- a/src/js/toast.js +++ b/src/js/toast.js @@ -164,9 +164,6 @@ return template.content.firstChild; } - const initialToaster = document.getElementById('toaster'); - if (initialToaster) initToaster(initialToaster); - document.addEventListener('basecoat:toast', (event) => { if (!toaster) { console.error('Cannot create toast: toaster container not found on page.'); @@ -177,21 +174,8 @@ toaster.appendChild(toastElement); }); - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - mutation.addedNodes.forEach((node) => { - if (node.nodeType !== Node.ELEMENT_NODE) return; - - if (node.matches('#toaster')) { - initToaster(node); - } - - if (toaster && node.matches('.toast:not([data-toast-initialized])')) { - initToast(node); - } - }); - }); - }); - - observer.observe(document.body, { childList: true, subtree: true }); + if (window.basecoat) { + window.basecoat.register('toaster', '#toaster:not([data-toaster-initialized])', initToaster); + window.basecoat.register('toast', '.toast:not([data-toast-initialized])', initToast); + } })(); \ No newline at end of file diff --git a/src/nunjucks/dialog.njk b/src/nunjucks/dialog.njk index 7c6bf5f..6214494 100644 --- a/src/nunjucks/dialog.njk +++ b/src/nunjucks/dialog.njk @@ -83,11 +83,9 @@ {% endif %} {% if close_button %} - {% endif %}