@thinking.tools/teff 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ @layer utilities {
2
+ .align-left { text-align: start; }
3
+ .align-center { text-align: center; }
4
+ .align-right { text-align: end; }
5
+ .text-light { color: var(--muted-foreground); }
6
+ .text-lighter { color: var(--faint-foreground); }
7
+ .text-small { font-size: var(--text-7); }
8
+ .text-xsmall { font-size: var(--text-8); }
9
+ .font-normal { font-weight: var(--font-normal); }
10
+ .font-medium { font-weight: var(--font-medium); }
11
+ .font-semibold { font-weight: var(--font-semibold); }
12
+ .tabular { font-variant-numeric: tabular-nums; }
13
+
14
+ .truncate {
15
+ overflow: hidden;
16
+ text-overflow: ellipsis;
17
+ white-space: nowrap;
18
+ }
19
+
20
+ /* Visually hidden, still read by screen readers. */
21
+ .sr-only {
22
+ position: absolute;
23
+ width: 1px;
24
+ height: 1px;
25
+ padding: 0;
26
+ margin: -1px;
27
+ overflow: hidden;
28
+ clip-path: inset(50%);
29
+ white-space: nowrap;
30
+ border: 0;
31
+ }
32
+
33
+ .flex { display: flex; }
34
+ .flex-col { flex-direction: column; }
35
+ .items-center { align-items: center; }
36
+ .justify-center { justify-content: center; }
37
+ .justify-between { justify-content: space-between; }
38
+ .justify-end { justify-content: flex-end; }
39
+ .grow { flex-grow: 1; }
40
+ .shrink-0 { flex-shrink: 0; }
41
+ .min-w-0 { min-width: 0; }
42
+
43
+ /* Bootstrap inspired. */
44
+ .hstack {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: var(--space-3);
48
+ flex-wrap: wrap;
49
+ align-content: flex-start;
50
+ height: auto;
51
+
52
+ > * {
53
+ margin: 0;
54
+ }
55
+ }
56
+ .vstack {
57
+ display: flex;
58
+ flex-direction: column;
59
+ gap: var(--space-3);
60
+ }
61
+
62
+ .gap-1 { gap: var(--space-1); }
63
+ .gap-2 { gap: var(--space-2); }
64
+ .gap-3 { gap: var(--space-3); }
65
+ .gap-4 { gap: var(--space-4); }
66
+ .gap-6 { gap: var(--space-6); }
67
+
68
+ .mt-2 { margin-block-start: var(--space-2); }
69
+ .mt-4 { margin-block-start: var(--space-4); }
70
+ .mt-6 { margin-block-start: var(--space-6); }
71
+ .mt-8 { margin-block-start: var(--space-8); }
72
+
73
+ .mb-2 { margin-block-end: var(--space-2); }
74
+ .mb-4 { margin-block-end: var(--space-4); }
75
+ .mb-6 { margin-block-end: var(--space-6); }
76
+ .mb-8 { margin-block-end: var(--space-8); }
77
+
78
+ .ms-auto { margin-inline-start: auto; }
79
+ .me-auto { margin-inline-end: auto; }
80
+ .mx-auto { margin-inline: auto; }
81
+
82
+ .p-2 { padding: var(--space-2); }
83
+ .p-4 { padding: var(--space-4); }
84
+ .p-6 { padding: var(--space-6); }
85
+
86
+ .w-100 { width: 100%; }
87
+
88
+ .rounded { border-radius: var(--radius-medium); }
89
+ .rounded-large { border-radius: var(--radius-large); }
90
+ .rounded-full { border-radius: var(--radius-full); }
91
+ .shadow-small { box-shadow: var(--shadow-small); }
92
+ .shadow-medium { box-shadow: var(--shadow-medium); }
93
+ .shadow-large { box-shadow: var(--shadow-large); }
94
+
95
+ /* 1px alpha outline keeps image edges crisp on any background. */
96
+ .img-outline {
97
+ outline: 1px solid light-dark(rgb(0 0 0 / 0.1), rgb(255 255 255 / 0.1));
98
+ outline-offset: -1px;
99
+ }
100
+
101
+ :is(ul, ol, a).unstyled {
102
+ list-style: none;
103
+ text-decoration: none;
104
+ padding: 0;
105
+ }
106
+ }
package/js/base.js ADDED
@@ -0,0 +1,137 @@
1
+ // teff - Base Web Component Class
2
+ // Provides lifecycle management, event handling, and utilities.
3
+
4
+ export class TeffBase extends HTMLElement {
5
+ #initialized = false;
6
+
7
+ // Called when element is added to DOM.
8
+ connectedCallback() {
9
+ if (this.#initialized) return;
10
+
11
+ // Wait for DOM to be ready.
12
+ if (document.readyState === "loading") {
13
+ document.addEventListener("DOMContentLoaded", () => this.#setup(), {
14
+ once: true,
15
+ });
16
+ } else {
17
+ this.#setup();
18
+ }
19
+ }
20
+
21
+ // Private setup to ensure that init() is only called once.
22
+ #setup() {
23
+ if (this.#initialized) return;
24
+ this.#initialized = true;
25
+ this.init();
26
+ }
27
+
28
+ // Called when element is removed from DOM.
29
+ disconnectedCallback() {
30
+ this.cleanup();
31
+ }
32
+
33
+ // Override in subclass for cleanup logic.
34
+ cleanup() {}
35
+
36
+ // Central event handler - enables automatic cleanup.
37
+ // Usage: element.addEventListener('click', this)
38
+ handleEvent(event) {
39
+ const handler = this[`on${event.type}`];
40
+ if (handler) handler.call(this, event);
41
+ }
42
+
43
+ // Given a keyboard event (left, right, home, end), the current selection idx
44
+ // total items in a list, return 0-n index of the next/previous item
45
+ // for doing a roving keyboard nav.
46
+ keyNav(event, idx, len, prevKey, nextKey, homeEnd = false) {
47
+ const { key } = event;
48
+ let next = -1;
49
+
50
+ if (key === nextKey) {
51
+ next = (idx + 1) % len;
52
+ } else if (key === prevKey) {
53
+ next = (idx - 1 + len) % len;
54
+ } else if (homeEnd) {
55
+ if (key === "Home") {
56
+ next = 0;
57
+ } else if (key === "End") {
58
+ next = len - 1;
59
+ }
60
+ }
61
+
62
+ if (next >= 0) event.preventDefault();
63
+ return next;
64
+ }
65
+
66
+ // Emit a custom event.
67
+ emit(name, detail = null) {
68
+ return this.dispatchEvent(
69
+ new CustomEvent(name, {
70
+ bubbles: true,
71
+ composed: true,
72
+ cancelable: true,
73
+ detail,
74
+ }),
75
+ );
76
+ }
77
+
78
+ // Query selector within this element.
79
+ $(selector) {
80
+ return this.querySelector(selector);
81
+ }
82
+
83
+ // Query selector all within this element.
84
+ $$(selector) {
85
+ return Array.from(this.querySelectorAll(selector));
86
+ }
87
+
88
+ // Generate a unique ID string.
89
+ uid() {
90
+ return Math.random().toString(36).slice(2, 10);
91
+ }
92
+ }
93
+
94
+ // Polyfill for command/commandfor (Safari)
95
+ if (!("commandForElement" in HTMLButtonElement.prototype)) {
96
+ document.addEventListener("click", (e) => {
97
+ const btn = e.target.closest("button[commandfor]");
98
+ if (!btn) return;
99
+
100
+ const target = document.getElementById(btn.getAttribute("commandfor"));
101
+ if (!target) return;
102
+
103
+ const command = btn.getAttribute("command") || "toggle";
104
+
105
+ if (target instanceof HTMLDialogElement) {
106
+ if (command === "show-modal") target.showModal();
107
+ else if (command === "close") target.close();
108
+ else target.open ? target.close() : target.showModal();
109
+ }
110
+ });
111
+ }
112
+
113
+ // Close modal dialogs on backdrop click/tap; opt out with <dialog data-static>.
114
+ // pointerdown (not click) so selecting text inside and releasing outside
115
+ // doesn't close, and preventDefault stops the tap bleeding through on touch.
116
+ document.addEventListener(
117
+ "pointerdown",
118
+ (e) => {
119
+ const d = e.target;
120
+ if (!(d instanceof HTMLDialogElement) || !d.open || d.hasAttribute("data-static")) {
121
+ return;
122
+ }
123
+
124
+ // Backdrop clicks target the dialog itself; double-check the point is
125
+ // outside the dialog's box so padding clicks never close it.
126
+ const r = d.getBoundingClientRect();
127
+ const inside =
128
+ e.clientX >= r.left && e.clientX <= r.right &&
129
+ e.clientY >= r.top && e.clientY <= r.bottom;
130
+
131
+ if (!inside) {
132
+ e.preventDefault();
133
+ d.close();
134
+ }
135
+ },
136
+ { passive: false },
137
+ );
package/js/dropdown.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * teff - Dropdown Component
3
+ * Provides positioning, keyboard navigation, and ARIA state management.
4
+ *
5
+ * Usage:
6
+ * <teff-dropdown>
7
+ * <button popovertarget="menu-id">Options</button>
8
+ * <menu popover id="menu-id">
9
+ * <button role="menuitem">Item 1</button>
10
+ * <button role="menuitem">Item 2</button>
11
+ * </menu>
12
+ * </teff-dropdown>
13
+ */
14
+
15
+ import { TeffBase } from "./base.js";
16
+
17
+ class TeffDropdown extends TeffBase {
18
+ #menu;
19
+ #trigger;
20
+ #position;
21
+ #items;
22
+
23
+ init() {
24
+ this.#menu = this.$("[popover]");
25
+ this.#trigger = this.$("[popovertarget]");
26
+
27
+ if (!this.#menu || !this.#trigger) return;
28
+
29
+ this.#menu.addEventListener("toggle", this);
30
+ this.#menu.addEventListener("keydown", this);
31
+
32
+ this.#position = () => {
33
+ // Position has to be calculated and applied manually because
34
+ // popover positioning is like fixed, relative to the window.
35
+ const r = this.#trigger.getBoundingClientRect();
36
+ const m = this.#menu.getBoundingClientRect();
37
+
38
+ // Flip if menu overflows viewport.
39
+ this.#menu.style.top = `${r.bottom + m.height > window.innerHeight ? r.top - m.height : r.bottom}px`;
40
+ this.#menu.style.left = `${r.left + m.width > window.innerWidth ? r.right - m.width : r.left}px`;
41
+ };
42
+ }
43
+
44
+ ontoggle(e) {
45
+ if (e.newState === "open") {
46
+ this.#position();
47
+ window.addEventListener("scroll", this.#position, true);
48
+ window.addEventListener("resize", this.#position);
49
+ this.#items = this.$$('[role="menuitem"]');
50
+ this.#items[0]?.focus();
51
+ this.#trigger.ariaExpanded = "true";
52
+ } else {
53
+ this.cleanup();
54
+ this.#items = null;
55
+ this.#trigger.ariaExpanded = "false";
56
+ this.#trigger.focus();
57
+ }
58
+ }
59
+
60
+ onkeydown(e) {
61
+ if (!e.target.matches('[role="menuitem"]')) return;
62
+
63
+ const idx = this.#items.indexOf(e.target);
64
+ const next = this.keyNav(
65
+ e,
66
+ idx,
67
+ this.#items.length,
68
+ "ArrowUp",
69
+ "ArrowDown",
70
+ true,
71
+ );
72
+ if (next >= 0) this.#items[next].focus();
73
+ }
74
+
75
+ cleanup() {
76
+ window.removeEventListener("scroll", this.#position, true);
77
+ window.removeEventListener("resize", this.#position);
78
+ }
79
+ }
80
+
81
+ customElements.define("teff-dropdown", TeffDropdown);
package/js/index.js ADDED
@@ -0,0 +1,15 @@
1
+ import './base.js';
2
+ import './tabs.js';
3
+ import './dropdown.js';
4
+ import './tooltip.js';
5
+ import './sidebar.js';
6
+ import './password.js';
7
+ import { toast, toastEl, toastClear } from './toast.js';
8
+ import { shake } from './shake.js';
9
+
10
+ // Register the global window.teff.* APIs.
11
+ const teff = window.teff || (window.teff = {});
12
+ teff.toast = toast;
13
+ teff.toast.el = toastEl;
14
+ teff.toast.clear = toastClear;
15
+ teff.shake = shake;
package/js/password.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * teff - Password visibility toggle
3
+ * Enhances <input type="password"> with a show/hide button that flips the
4
+ * input between password and text:
5
+ *
6
+ * <input type="password"> → toggle injected on load
7
+ * <input type="password" data-static> → left alone
8
+ *
9
+ * Progressive enhancement: without this script the field stays a plain
10
+ * password input — no dead UI ships in the CSS-only setup. Inputs inside
11
+ * fieldset.group are skipped (the fused group has its own trailing control).
12
+ */
13
+
14
+ const EYE =
15
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>';
16
+ const EYE_OFF =
17
+ '<svg class="off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg>';
18
+
19
+ const SEL = 'input[type="password"]';
20
+
21
+ const apply = (input) => {
22
+ if (
23
+ input.hasAttribute("data-static") ||
24
+ input.closest("[data-password], fieldset.group")
25
+ )
26
+ return;
27
+
28
+ const wrap = document.createElement("span");
29
+ wrap.setAttribute("data-password", "");
30
+ input.before(wrap);
31
+ wrap.append(input);
32
+
33
+ const btn = document.createElement("button");
34
+ btn.type = "button";
35
+ btn.setAttribute("aria-label", "Show password");
36
+ btn.setAttribute("aria-pressed", "false");
37
+ btn.innerHTML = EYE + EYE_OFF;
38
+ btn.addEventListener("click", () => {
39
+ const show = input.type === "password";
40
+ input.type = show ? "text" : "password";
41
+ btn.setAttribute("aria-pressed", String(show));
42
+ });
43
+ wrap.append(btn);
44
+ };
45
+
46
+ document.addEventListener("DOMContentLoaded", () => {
47
+ document.querySelectorAll(SEL).forEach(apply);
48
+
49
+ // Enhance password inputs added later (apply() is idempotent).
50
+ new MutationObserver((muts) => {
51
+ for (const m of muts)
52
+ for (const n of m.addedNodes)
53
+ if (n.nodeType === 1) {
54
+ if (n.matches?.(SEL)) apply(n);
55
+ n.querySelectorAll?.(SEL).forEach(apply);
56
+ }
57
+ }).observe(document.body, { childList: true, subtree: true });
58
+ });
package/js/shake.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * teff - Attention shake
3
+ * Imperative trigger for the [data-shake] animation:
4
+ *
5
+ * teff.shake(input) // e.g. on a rejected login attempt
6
+ *
7
+ * Re-adds the attribute with a reflow in between so repeat calls restart a
8
+ * mid-flight animation, and removes it on animationend so the element is
9
+ * left clean.
10
+ */
11
+
12
+ export const shake = (el) => {
13
+ if (!el) return;
14
+ el.removeAttribute("data-shake");
15
+ void el.offsetWidth;
16
+ el.setAttribute("data-shake", "");
17
+ const done = (e) => {
18
+ if (e.target !== el) return;
19
+ el.removeAttribute("data-shake");
20
+ el.removeEventListener("animationend", done);
21
+ };
22
+ el.addEventListener("animationend", done);
23
+ };
package/js/sidebar.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Sidebar toggle handler
3
+ * Toggles data-sidebar-open on layout when toggle button is clicked
4
+ */
5
+ document.addEventListener('click', (e) => {
6
+ const toggle = e.target.closest('[data-sidebar-toggle]');
7
+ if (toggle) {
8
+ const layout = toggle.closest('[data-sidebar-layout]');
9
+ layout?.toggleAttribute('data-sidebar-open');
10
+ return;
11
+ }
12
+
13
+ // Dismiss the mobile drawer when clicking outside it, or when following a
14
+ // link inside it (the drawer would otherwise sit open over the new content).
15
+ if (!e.target.closest('[data-sidebar]') || e.target.closest('[data-sidebar] a[href]')) {
16
+ const layout = document.querySelector('[data-sidebar-layout][data-sidebar-open]');
17
+ // Hardcode breakpoint (for now) as there's no way to use a CSS variable in
18
+ // the @media{} query which could've been picked up here.
19
+ if (layout && window.matchMedia('(max-width: 768px)').matches) {
20
+ layout.removeAttribute('data-sidebar-open');
21
+ }
22
+ }
23
+ });
package/js/tabs.js ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * teff - Tabs Component
3
+ * Provides keyboard navigation and ARIA state management.
4
+ *
5
+ * Usage:
6
+ * <teff-tabs>
7
+ * <div role="tablist">
8
+ * <button role="tab">Tab 1</button>
9
+ * <button role="tab">Tab 2</button>
10
+ * </div>
11
+ * <div role="tabpanel">Content 1</div>
12
+ * <div role="tabpanel">Content 2</div>
13
+ * </teff-tabs>
14
+ */
15
+
16
+ import { TeffBase } from "./base.js";
17
+
18
+ class TeffTabs extends TeffBase {
19
+ #tabs = [];
20
+ #panels = [];
21
+
22
+ init() {
23
+ const tablist = this.$(':scope > [role="tablist"]');
24
+ this.#tabs = tablist ? [...tablist.querySelectorAll('[role="tab"]')] : [];
25
+ this.#panels = this.$$(':scope > [role="tabpanel"]');
26
+
27
+ if (this.#tabs.length === 0 || this.#panels.length === 0) {
28
+ console.warn("teff-tabs: Missing tab or tabpanel elements");
29
+ return;
30
+ }
31
+
32
+ // Generate IDs and set up ARIA.
33
+ this.#tabs.forEach((tab, i) => {
34
+ const panel = this.#panels[i];
35
+ if (!panel) return;
36
+
37
+ const tabId = tab.id || `teff-tab-${this.uid()}`;
38
+ const panelId = panel.id || `teff-panel-${this.uid()}`;
39
+
40
+ tab.id = tabId;
41
+ panel.id = panelId;
42
+ tab.setAttribute("aria-controls", panelId);
43
+ panel.setAttribute("aria-labelledby", tabId);
44
+ });
45
+
46
+ tablist.addEventListener("click", this);
47
+ tablist.addEventListener("keydown", this);
48
+
49
+ // Find initially active tab or default to first.
50
+ const activeTab = this.#tabs.findIndex((t) => t.ariaSelected === "true");
51
+ this.#activate(activeTab >= 0 ? activeTab : 0);
52
+ }
53
+
54
+ onclick(e) {
55
+ const index = this.#tabs.indexOf(e.target.closest('[role="tab"]'));
56
+ if (index >= 0) this.#activate(index);
57
+ }
58
+
59
+ onkeydown(e) {
60
+ if (!e.target.closest('[role="tab"]')) return;
61
+
62
+ const next = this.keyNav(
63
+ e,
64
+ this.activeIndex,
65
+ this.#tabs.length,
66
+ "ArrowLeft",
67
+ "ArrowRight",
68
+ );
69
+ if (next >= 0) {
70
+ this.#activate(next);
71
+ this.#tabs[next].focus();
72
+ }
73
+ }
74
+
75
+ #activate(idx) {
76
+ this.#tabs.forEach((tab, i) => {
77
+ const isActive = i === idx;
78
+ tab.ariaSelected = String(isActive);
79
+ tab.tabIndex = isActive ? 0 : -1;
80
+ });
81
+
82
+ this.#panels.forEach((panel, i) => {
83
+ panel.hidden = i !== idx;
84
+ });
85
+
86
+ this.emit("teff-tab-change", { index: idx, tab: this.#tabs[idx] });
87
+ }
88
+
89
+ get activeIndex() {
90
+ return this.#tabs.findIndex((t) => t.ariaSelected === "true");
91
+ }
92
+
93
+ set activeIndex(value) {
94
+ if (value >= 0 && value < this.#tabs.length) {
95
+ this.#activate(value);
96
+ }
97
+ }
98
+ }
99
+
100
+ customElements.define("teff-tabs", TeffTabs);
package/js/toast.js ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * teff - Toast Notifications
3
+ *
4
+ * Usage:
5
+ * teff.toast('Saved!')
6
+ * teff.toast('Action completed successfully', 'All good')
7
+ * teff.toast('Operation completed.', 'Success', { variant: 'success' })
8
+ * teff.toast('Something went wrong.', 'Error', { variant: 'danger', placement: 'bottom-center' })
9
+ *
10
+ * // Custom markup
11
+ * teff.toast.el(element)
12
+ * teff.toast.el(element, { duration: 4000, placement: 'bottom-center' })
13
+ * teff.toast.el(document.querySelector('#my-template'))
14
+ */
15
+
16
+ const toasts = {};
17
+
18
+ function _get(placement) {
19
+ if (!toasts[placement]) {
20
+ const el = document.createElement("div");
21
+ el.className = "toast-container";
22
+ el.setAttribute("popover", "manual");
23
+ el.setAttribute("data-placement", placement);
24
+ document.body.appendChild(el);
25
+ toasts[placement] = el;
26
+ }
27
+
28
+ return toasts[placement];
29
+ }
30
+
31
+ function _show(el, options = {}) {
32
+ const { placement = "top-right", duration = 4000 } = options;
33
+ const p = _get(placement);
34
+
35
+ el.classList.add("toast");
36
+
37
+ let timeout;
38
+
39
+ // Pause on hover.
40
+ el.onmouseenter = () => clearTimeout(timeout);
41
+ el.onmouseleave = () => {
42
+ if (duration > 0) {
43
+ timeout = setTimeout(() => _remove(el, p), duration);
44
+ }
45
+ };
46
+
47
+ // Show with animation.
48
+ el.setAttribute("data-entering", "");
49
+ p.appendChild(el);
50
+ p.showPopover();
51
+
52
+ // Double RAF to compute styles before transition starts.
53
+ requestAnimationFrame(() => {
54
+ requestAnimationFrame(() => {
55
+ el.removeAttribute("data-entering");
56
+ });
57
+ });
58
+
59
+ if (duration > 0) {
60
+ timeout = setTimeout(() => _remove(el, p), duration);
61
+ }
62
+
63
+ return el;
64
+ }
65
+
66
+ function _remove(el, container) {
67
+ // Ignore if already in the process of exiting.
68
+ if (el.hasAttribute("data-exiting")) {
69
+ return;
70
+ }
71
+ el.setAttribute("data-exiting", "");
72
+
73
+ const cleanup = () => {
74
+ el.remove();
75
+ if (!container.children.length) {
76
+ container.hidePopover();
77
+ }
78
+ };
79
+
80
+ el.addEventListener("transitionend", cleanup, { once: true });
81
+
82
+ // Couldn't confirm what unit this actually returns across browsers, so
83
+ // assume that it could be ms or s. Also, setTimeout() is required because
84
+ // there's no guarantee that the `transitionend` event will always fire,
85
+ // eg: clients that disable animations.
86
+ const t = getComputedStyle(el).getPropertyValue("--transition").trim();
87
+ const val = parseFloat(t);
88
+ const ms = t.endsWith("ms") ? val : val * 1000;
89
+ setTimeout(cleanup, ms);
90
+ }
91
+
92
+ // Show a text toast.
93
+ export function toast(message, title, options = {}) {
94
+ const { variant = "info", ...rest } = options;
95
+
96
+ const el = document.createElement("output");
97
+ el.setAttribute("data-variant", variant);
98
+
99
+ if (title) {
100
+ const titleEl = document.createElement("h6");
101
+ titleEl.className = "toast-title";
102
+ titleEl.textContent = title;
103
+ el.appendChild(titleEl);
104
+ }
105
+
106
+ const msgEl = document.createElement("div");
107
+ msgEl.className = "toast-message";
108
+ msgEl.textContent = message;
109
+ el.appendChild(msgEl);
110
+
111
+ return _show(el, rest);
112
+ }
113
+
114
+ // Element-based toast.
115
+ export function toastEl(el, options = {}) {
116
+ let t;
117
+
118
+ if (el instanceof HTMLTemplateElement) {
119
+ t = el.content.firstElementChild?.cloneNode(true);
120
+ } else if (el) {
121
+ t = el.cloneNode(true);
122
+ }
123
+
124
+ if (!t) {
125
+ return;
126
+ }
127
+
128
+ t.removeAttribute("id");
129
+
130
+ return _show(t, options);
131
+ }
132
+
133
+ // Clear all toasts.
134
+ export function toastClear(placement) {
135
+ if (placement && toasts[placement]) {
136
+ toasts[placement].innerHTML = "";
137
+ toasts[placement].hidePopover();
138
+ } else {
139
+ Object.values(toasts).forEach((c) => {
140
+ c.innerHTML = "";
141
+ c.hidePopover();
142
+ });
143
+ }
144
+ }