- {% lucide "circle-alert" %}
-
This component requires JavaScript.
+
+
+
+ Include the sidebar.js file. Add this to the <head> of your page:
+
+
+{% set code_script %}{% raw %}{% endraw %}{% endset %}
+{{ code_block(code_script | prettyHtml, "html") }}
+
+
+
+
+{% set code_html %}
+{% set icon_square_terminal %}{% lucide "square-terminal" %}{% endset %}
+{% set icon_bot %}{% lucide "bot" %}{% endset %}
+{% set icon_settings %}{% lucide "settings" %}{% endset %}
+{% set menu = [
+ { type: "group", label: "Getting started", items: [
+ { label: "Playground", url: "#", icon: icon_square_terminal },
+ { label: "Models", url: "#", icon: icon_bot },
+ { label: "Settings", type: "submenu", icon: icon_settings, items: [
+ { label: "General", url: "#" },
+ { label: "Team", url: "#" },
+ { label: "Billing", url: "#" },
+ { label: "Limits", url: "#" }
+ ] }
+ ]}
+] %}
+{{ sidebar(
+ label="Sidebar navigation",
+ content_attrs={
+ "class": "scrollbar"
+ },
+ menu=menu
+) }}
+
+ Toggle sidebar
+ Content
+
+{% endset %}
+{{ code_block(code_html | prettyHtml, "html") }}
+
To use a sidebar, you will need the sidebar itself and a sibling container for the content of the page (e.g. <main>), with the following structure for the sidebar:
@@ -43,68 +100,74 @@ toc:
You can include the JavaScript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. x-bind, x-data, @click).
-
+
-{% set code_html %}
-{% set icon_square_terminal %}{% lucide "square-terminal" %}{% endset %}
-{% set icon_bot %}{% lucide "bot" %}{% endset %}
-{% set icon_settings %}{% lucide "settings" %}{% endset %}
-
-{% set menu = [
- { type: "group", label: "Getting started", items: [
- { label: "Playground", url: "#", icon: icon_square_terminal },
- { label: "Models", url: "#", icon: icon_bot },
- { label: "Settings", type: "submenu", icon: icon_settings, items: [
- { label: "General", url: "#" },
- { label: "Team", url: "#" },
- { label: "Billing", url: "#" },
- { label: "Limits", url: "#" }
- ] }
- ]}
-] %}
-
-{{ sidebar(
- label="Sidebar navigation",
- content_attrs={
- "class": "scrollbar"
- },
- menu=menu
-) }}
-
- Toggle sidebar
- Content
-
-{% endset %}
-
-{% set code_script %}
-
-{% endset %}
-
-{% from "tabs.njk" import tabs %}
-{% set tabsets = [
- { tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
- { tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
-] %}
-{{ tabs(id="sidebar", tabsets=tabsets) }}
+
+
+ <aside class="sidebar" aria-hidden="false">
+ Wraps around the entire component. It can have the following attributes:
+
+ aria-hidden="true": controls the default state of the sidebar (hidden or visible).
+ data-side="left": specifies the side of the sidebar (left or right, defaults to left).
+
+
+
+ <nav>
+
+ The navigation element that contains the sidebar's content. It can have the following attributes:
+
+ id="{BUTTON_ID}": linked to by the aria-labelledby attribute of the listbox.
+ aria-haspopup="menu": indicates that the button opens a menu.
+ aria-controls="{ MENU_ID }": points to the menu's id.
+ aria-expanded="false": tracks the popover's state.
+
+
+ <header> Optional
+ The header of the sidebar.
+ <section>
+ The main navigation list.
+
+ <div role="group">
+ Group of options, can have a aria-labelledby attribute to link to a heading.
+ <span role="heading">
+ Group heading, must have an id attribute if you use the aria-labelledby attribute on the group.
+ <ul>
+ List of links or buttons.
+
+ <li>
+ Individual item.
+
+ <a>
+ A link. By default, clicking on a link will close the sidebar on mobile unless the data-keep-mobile-sidebar-open attribute is present.
+ <button>
+ A button. By default, clicking on a button will close the sidebar on mobile unless the data-keep-mobile-sidebar-open attribute is present.
+ <details>
+ Collapsible section.
+
+ <summary>
+ Summary of the collapsible section.
+ <ul>
+ List of links or buttons.
+
+
+
+
+
+
+
+
+ <footer> Optional
+ The footer of the sidebar.
+
+
+
+
+ <main>
+ A wrapper for the content of the page.
+ <button type="button" onclick="window.dispatchEvent(new CustomEvent('sidebar:toggle'))">
+ A button to toggle the sidebar. If you want to use multiple sidebars you will need to add unique ids to the sidebars (i.e. <aside class="sidebar" id="{SIDEBAR_ID}">) and refer to them in the event detail (i.e. window.dispatchEvent(new CustomEvent('sidebar:toggle', { detail: { id: '{SIDEBAR_ID}' } }));).
+
+
diff --git a/docs/src/components/tabs.njk b/docs/src/components/tabs.njk
index 79c352d..b990bb2 100644
--- a/docs/src/components/tabs.njk
+++ b/docs/src/components/tabs.njk
@@ -76,39 +76,19 @@ toc:
) }}
{% endset %}
-{% set code_script %}
-
-{% endset %}
-
-{{ code_preview("tabs", code_script | prettyHtml ~ code_html | prettyHtml, "max-w-[300px] w-full") }}
+{{ code_preview("tabs", code_html | prettyHtml, "max-w-[300px] w-full") }}
-
-
-
- {% lucide "circle-alert" %}
-
This component requires JavaScript.
-
+
- The component is structured as such:
-
-
- A <div class="tabs"> which wraps around the entire component and holds it state (e.g. open/close).
- A <nav role="tablist"> that holds the tab buttons (<button role="tab">).
- A series of <div role="tabpanel"> that holds the tab panels corresponding to each tab button.
-
-
- You can include the JavaScript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. x-bind, x-data, @click).
+ Include the tabs.js file. Add this to the <head> of your page:
+{% set code_script %}{% raw %}{% endraw %}{% endset %}
+{{ code_block(code_script | prettyHtml, "html") }}
+
-{% from "tabs.njk" import tabs %}
-{% set tabsets = [
- { 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) }}
+
+
+{{ code_block(code_html | prettyHtml, "html") }}
+
+
+
+
+
+ <div class="tabs">
+ Wraps around the entire component.
+
+ <nav role="tablist" aria-orientation="horizontal">
+ The tablist containing the tab buttons
+
+ <button role="tab" id="{ TAB_ID }" aria-controls="{ PANEL_ID }" aria-selected="false" tabindex="0">
+ The tab button. When active, the aria-selected attribute is set to true. tabindex="0" is required for keyboard navigation.
+
+
+
+
+
+ <div role="tabpanel" id="{ PANEL_ID }" aria-labelledby="{ TAB_ID }" tabindex="-1" aria-selected="false">
+ The tab panel. When active, the aria-selected attribute is set to true.
+
+
+
diff --git a/docs/src/components/toast.njk b/docs/src/components/toast.njk
index c328077..3a6a7f4 100644
--- a/docs/src/components/toast.njk
+++ b/docs/src/components/toast.njk
@@ -7,12 +7,18 @@ toc:
id: usage
children:
- label: HTML + JavaScript
- id: usage-default
+ id: usage-html-js
children:
- - label: Append a toast
- id: usage-append-toast
- - label: Use $toast()
- id: usage-use-magic-method
+ - label: "Step 1: Include the JavaScript file"
+ id: usage-html-js-1
+ - label: "Step 2: Add the toaster HTML"
+ id: usage-html-js-2
+ - label: "Step 3: Add your toasts"
+ id: usage-html-js-3
+ - label: HTML structure
+ id: usage-html-js-4
+ - label: JavaScript config object
+ id: usage-html-js-5
- label: Jinja and Nunjucks
id: usage-macro
---
@@ -21,52 +27,49 @@ toc:
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
-{% set code_htmx %}
Toast from backend (with HTMX)
{% endset %}
-{{ code_preview("toast-htmx", code_htmx, "max-w-md") }}
+{{ code_preview("toast-server-side", code_server_side, "max-w-md") }}
-{% set code_alpine %}
Toast from front-end
-
-{% endset %}
-{{ code_preview("toast-alpine", code_alpine, "max-w-md") }}
+{% endset %}
+{{ code_preview("toast-client-side", code_client_side, "max-w-md") }}
-
+
-
- {% lucide "circle-alert" %}
-
This component requires JavaScript.
-
+
- To use toasts, you will first need to set up the toaster code to the end of your <body> (see HTML below).
-
- The <template id="toast-template"> bit is only required if you plan on using the $toast() method (see below).
-
- You will also need to include the JavaScript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. x-bind, x-data, @click).
+ Include the toast.js file. Add this to the <head> of your page:
+{% set code_script %}{% raw %}{% endraw %}{% endset %}
+{{ code_block(code_script | prettyHtml, "html") }}
+
-
-{% set code_script %}
-{# #}
-{% endset %}
-
-{% set code_html_toaster %}{{ toaster() }}{% endset %}
-
-{% from "tabs.njk" import tabs %}
-{% set tabsets = [
- { tab: "HTML", panel: code_block(code_html_toaster | prettyHtml, class="") },
- { tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
-] %}
-{{ tabs(id="popover", tabsets=tabsets) }}
+
- Once that's in place, we can do one of two things:
-
-
- Append a <div class="toast"> to the toaster.
- Use the $toast() Alpine.js "magic method".
-
+ Toasts are displayed in a parent element, the "toaster". Add this to this to at the end of your <body>:
-
+{% set code_toaster %}
{% endset %}
+{{ code_block(code_toaster | prettyHtml, "html") }}
+
+
- To display a toast, just append the following to the toaster:
+ If you decide to server-render your toasts, or load them using asynchronoulsy with something like HTMX, you can just add the toast's markup to the toaster:
-{% set code_toast_simple %}
{% endset %}
-
-{{ code_block(code_toast_simple | prettyHtml, "html") }}
-
-
- You can implement the layout you want within the toast, but here is the standard format used in the examples above:
-
-
-{% set code_toast %}{{ toast(
+{% set code_toast_html %}
+ {{ toast(
category="success",
title="Success",
description="A success toast called from the front-end.",
@@ -137,66 +109,98 @@ document.addEventListener('alpine:init', () => {
label: "Dismiss",
click: "close()"
}
-) }}{% endset %}
-{{ code_block(code_toast | prettyHtml, "html") }}
+) }}
+
{% endset %}
+{{ code_block(code_toast_html | prettyHtml, "html") }}
+
+
- When setting up x-data, you can pass a config object with `category` (e.g. "success", "error", "info", "warning") and `duration` (in milliseconds). By default, the duration will be 3 seconds, and 5 seconds for errors.
-
- This approach is used in the first example with HTMX: when the button is clicked, we retrieve the a fragment that contains the markup for the toast and append it to #toaster.
+ If you need to create a toast from the front-end, you can dispatch a custom basecoat:toast event as such:
-{% set code_htmx %}
- Toast from backend (with HTMX)
- {% endset %}
+{{ code_block(code_client_side | prettyHtml, "html") }}
-
+
+
+
- If you want to display a toast directly from the front-end, you can use the $toast() Alpine.js magic method. This is what we're doing in the second example above:
+
+ <div class="toast">
+ Wraps around the toast component.
+
+ <div class="toast-content">
+ The content of the toast.
+
+ <svg aria-hidden="true"> Optional
+ The toast's icon.
+ <section>
+ The toast's message.
+
+ <h2>
+ The toast's title.
+ <p> Optional
+ The toast's description.
+
+
+ <footer> Optional
+ The toast's buttons. When clicked, the toast will be closed (unless the button's onclick is set to e.preventDefault()).
+
+ <button type="button" class="btn" onclick="{ ONCLICK }"> or <a href="{ URL }" class="btn" > Optional
+ The toast's action button. This can either be a link (with a href attribute) or a button (with an onclick attribute).
+
+
+ <button type="button" class="btn-outline" onclick="{ ONCLICK }"> Optional
+ The toast's cancel button (with an optional onclick attribute).
+
+
+
+
+
+
+
-{% set code_magic_method %}
- Toast from front-end
-
-{% endset %}
-{{ code_block(code_magic_method, "html") }}
+
- The $toast() method accepts a config object with the following properties:
-
-
- duration (optional): the duration of the toast in milliseconds. By default, the duration will be 3 seconds, or 5 seconds if category is "error".
- category (optional): "success", "error", "info", "warning" or any arbitrary string.
- title (optional): the title of the toast.
- description (optional): the description of the toast.
- action (optional): an object to define an action button:
-
- label: the label of the action button.
- click (optional): a string of JavaScript to execute when the button is clicked.
- url (optional): a URL to link to when the button is clicked.
-
-
- cancel (optional): an object to define a cancel button (similar structure to action).
-
+
+ category Optional
+ Category of the toast, either success, info, warning, or error.
+ title
+ The title of the toast.
+ description Optional
+ The description of the toast.
+ action Optional
+ Action button.
+
+ label Optional
+ The label of the cancel button. If not provided, the default label is "Dismiss".
+ onclick Optional
+ The onclick of the cancel button.
+
+
+ cancel Optional
+ Cancel button.
+
+ label Optional
+ The label of the cancel button. If not provided, the default label is "Dismiss".
+ onclick Optional
+ The onclick of the cancel button.
+
+
+
diff --git a/docs/src/fragments/toast/success.njk b/docs/src/fragments/toast/success.njk
index 8dedd4a..e94e52c 100644
--- a/docs/src/fragments/toast/success.njk
+++ b/docs/src/fragments/toast/success.njk
@@ -2,5 +2,6 @@
{{ toast(
title="Event has been created",
description="Sunday, December 03, 2023 at 9:00 AM",
- cancel={ label: "Undo", click: "close()" }
+ action={ label: "View" },
+ cancel={ label: "Undo" }
) }}
\ No newline at end of file
diff --git a/src/css/basecoat.css b/src/css/basecoat.css
index 24a40ed..e587a99 100644
--- a/src/css/basecoat.css
+++ b/src/css/basecoat.css
@@ -161,7 +161,6 @@
h2 {
@apply col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight;
}
-
section {
@apply text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed;
@@ -989,7 +988,27 @@
@apply pointer-events-auto w-full mt-4 animate-[toast-up_0.3s_ease-in-out] grid grid-rows-[1fr] transition-[grid-template-rows,opacity,margin] duration-300 ease-in-out;
.toast-content {
- @apply text-popover-foreground text-[13px] bg-popover border shadow-lg rounded-lg overflow-hidden;
+ @apply text-popover-foreground text-[13px] bg-popover border shadow-lg rounded-lg overflow-hidden flex gap-2.5 p-3 items-center;
+
+ svg {
+ @apply size-4 shrink-0;
+ }
+ section {
+ h2 {
+ @apply font-medium tracking-tight;
+ }
+ p {
+ @apply text-muted-foreground;
+ }
+ }
+ footer {
+ @apply ml-auto flex flex-col gap-2;
+
+ [data-toast-action],
+ [data-toast-cancel] {
+ @apply h-6 px-2.5 text-xs;
+ }
+ }
}
&[aria-hidden='true'] {
@apply grid-rows-[0fr] opacity-0 m-0 border-0 p-0 overflow-hidden;
diff --git a/src/css/cdn.css b/src/css/cdn.css
new file mode 100644
index 0000000..1e5feed
--- /dev/null
+++ b/src/css/cdn.css
@@ -0,0 +1,2 @@
+@import "tailwindcss";
+@import "./basecoat.css";
\ No newline at end of file
diff --git a/src/js/dropdown-menu.js b/src/js/dropdown-menu.js
index 5345a59..dbb2b6f 100644
--- a/src/js/dropdown-menu.js
+++ b/src/js/dropdown-menu.js
@@ -24,7 +24,6 @@
};
const handleKeyDown = (e) => {
- // If the menu isn't open, we only care about ArrowUp/Down/Enter/Space to open it.
if (!popover.matches(':popover-open')) {
if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) {
e.preventDefault();
@@ -33,7 +32,6 @@
return;
}
- // If the menu IS open, we handle all navigation.
let nextIndex = activeIndex;
if (menuItems.length === 0) return;
@@ -76,7 +74,6 @@
trigger.setAttribute('aria-expanded', e.newState === 'open');
if (e.newState === 'open') {
menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]:not([disabled])'));
- // Ensure all menu items have IDs for aria-activedescendant
menuItems.forEach((item, index) => {
if (!item.id) item.id = `${menu.id}-item-${index}`;
});
diff --git a/src/js/select.js b/src/js/select.js
index 9903d46..8a3fdb3 100644
--- a/src/js/select.js
+++ b/src/js/select.js
@@ -138,6 +138,8 @@
});
popover.addEventListener('toggle', (e) => {
+ trigger.setAttribute('aria-expanded', e.newState === 'open');
+
if (e.newState === 'open') {
if (filter) filter.focus();
diff --git a/src/js/sidebar.js b/src/js/sidebar.js
index cb88a4e..4994b56 100644
--- a/src/js/sidebar.js
+++ b/src/js/sidebar.js
@@ -71,7 +71,7 @@
const isMobile = window.innerWidth < breakpoint;
- if (isMobile && (target.closest('[data-close-mobile-sidebar]') || (target.closest('a, button') && !target.closest('[data-keep-mobile-sidebar-open]')))) {
+ if (isMobile && (target.closest('a, button') && !target.closest('[data-keep-mobile-sidebar-open]'))) {
if (document.activeElement) document.activeElement.blur();
setState(false);
return;
diff --git a/src/js/toast.js b/src/js/toast.js
index 672dafa..921cc50 100644
--- a/src/js/toast.js
+++ b/src/js/toast.js
@@ -1,75 +1,196 @@
-window.basecoat = window.basecoat || {};
-window.basecoat.registerToast = function(Alpine) {
- if (Alpine.components && Alpine.components.toast) return;
+(() => {
+ let toaster;
+ const toasts = new WeakMap();
+ let isPaused = false;
+ const ICONS = {
+ success: '
',
+ error: '
',
+ info: '
',
+ warning: '
'
+ };
- Alpine.store('toaster', { isPaused: false });
- Alpine.data('toast', (config={}) => ({
- config: config,
- open: false,
- timeoutDuration: null,
- timeoutId: null,
+ function initToaster(toasterElement) {
+ if (toasterElement.dataset.toasterInitialized) return;
+ toaster = toasterElement;
- init() {
- if (config.duration !== -1) {
- this.timeoutDuration = config.duration || (config.category === 'error' ? 5000 : 3000);
- this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
+ toaster.addEventListener('mouseenter', pauseAllTimeouts);
+ toaster.addEventListener('mouseleave', resumeAllTimeouts);
+ toaster.addEventListener('click', (e) => {
+ const actionLink = e.target.closest('.toast footer a');
+ const actionButton = e.target.closest('.toast footer button');
+ if (actionLink || actionButton) {
+ closeToast(e.target.closest('.toast'));
}
- this.open = true;
- this.$watch('$store.toaster.isPaused', (isPaused) => {
- if (!this.open) return;
- if (isPaused) {
- this.pauseTimeout();
+ });
+
+ toaster.querySelectorAll('.toast:not([data-toast-initialized])').forEach(initToast);
+ toaster.dataset.toasterInitialized = 'true';
+ }
+
+ function initToast(element) {
+ if (element.dataset.toastInitialized) return;
+
+ const duration = parseInt(element.dataset.duration);
+ const timeoutDuration = duration !== -1
+ ? duration || (element.dataset.category === 'error' ? 5000 : 3000)
+ : -1;
+
+ const state = {
+ remainingTime: timeoutDuration,
+ timeoutId: null,
+ startTime: null,
+ };
+
+ if (timeoutDuration !== -1) {
+ if (isPaused) {
+ state.timeoutId = null;
+ } else {
+ state.startTime = Date.now();
+ state.timeoutId = setTimeout(() => closeToast(element), timeoutDuration);
+ }
+ }
+ toasts.set(element, state);
+
+ element.dataset.toastInitialized = 'true';
+ }
+
+ function pauseAllTimeouts() {
+ if (isPaused) return;
+
+ isPaused = true;
+
+ toaster.querySelectorAll('.toast:not([aria-hidden="true"])').forEach(element => {
+ if (!toasts.has(element)) return;
+
+ const state = toasts.get(element);
+ if (state.timeoutId) {
+ clearTimeout(state.timeoutId);
+ state.timeoutId = null;
+ state.remainingTime -= Date.now() - state.startTime;
+ }
+ });
+ }
+
+ function resumeAllTimeouts() {
+ if (!isPaused) return;
+
+ isPaused = false;
+
+ toaster.querySelectorAll('.toast:not([aria-hidden="true"])').forEach(element => {
+ if (!toasts.has(element)) return;
+
+ const state = toasts.get(element);
+ if (state.remainingTime !== -1 && !state.timeoutId) {
+ if (state.remainingTime > 0) {
+ state.startTime = Date.now();
+ state.timeoutId = setTimeout(() => closeToast(element), state.remainingTime);
} else {
- this.resumeTimeout();
+ closeToast(element);
+ }
+ }
+ });
+ }
+
+ function closeToast(element) {
+ if (!toasts.has(element)) return;
+
+ const state = toasts.get(element);
+ clearTimeout(state.timeoutId);
+ toasts.delete(element);
+
+ if (document.activeElement) document.activeElement.blur();
+ element.setAttribute('aria-hidden', 'true');
+ element.addEventListener('transitionend', () => element.remove(), { once: true });
+ }
+
+ function executeAction(button, toast) {
+ const actionString = button.dataset.toastAction;
+ if (!actionString) return;
+ try {
+ const func = new Function('close', actionString);
+ func(() => closeToast(toast));
+ } catch (e) {
+ console.error('Error executing toast action:', e);
+ }
+ }
+
+ function createToast(config) {
+ const {
+ category = 'info',
+ title,
+ description,
+ action,
+ cancel,
+ duration,
+ icon,
+ } = config;
+
+ const iconHtml = icon || (category && ICONS[category]) || '';
+ const titleHtml = title ? `
${title} ` : '';
+ const descriptionHtml = description ? `
${description}
` : '';
+ const actionHtml = action?.href
+ ? `
${action.label} `
+ : action?.onclick
+ ? `
${action.label} `
+ : '';
+ const cancelHtml = cancel
+ ? `
${cancel.label} `
+ : '';
+
+ const footerHtml = actionHtml || cancelHtml ? `
${actionHtml}${cancelHtml} ` : '';
+
+ const html = `
+
+
+ ${iconHtml}
+
+ ${titleHtml}
+ ${descriptionHtml}
+
+ ${footerHtml}
+
+
+
+ `;
+ const template = document.createElement('template');
+ template.innerHTML = html.trim();
+ return template.content.firstChild;
+ }
+
+ const initialToaster = document.getElementById('toaster');
+ if (initialToaster) initToaster(initialToaster);
+
+ window.addEventListener('basecoat:toast', (e) => {
+ if (!toaster) {
+ console.error('Cannot create toast: toaster container not found on page.');
+ return;
+ }
+ const config = e.detail?.config || {};
+ const toastElement = createToast(config);
+ 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);
}
});
- },
- 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();
- },
- executeAction(actionString) {
- if (actionString) {
- Alpine.evaluate(this.$el, actionString);
- }
- },
-
- $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 #${toasterId} 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);
+ });
});
-};
\ No newline at end of file
+
+ observer.observe(document.body, { childList: true, subtree: true });
+})();
\ No newline at end of file
diff --git a/src/nunjucks/dialog.njk b/src/nunjucks/dialog.njk
index 64aeb5b..369d3aa 100644
--- a/src/nunjucks/dialog.njk
+++ b/src/nunjucks/dialog.njk
@@ -35,7 +35,7 @@
{% if trigger %}