@ryanhelsing/ry-ui
Version:
Framework-agnostic, Light DOM web components. CSS is the source of truth.
1,149 lines • 189 kB
JavaScript
class u extends HTMLElement {
// Store for cleanup functions
#t = [];
// Store for MutationObserver
#e = null;
constructor() {
super();
}
/**
* Called when element is added to DOM.
* Override setup() in subclass instead of this.
*/
connectedCallback() {
this.setAttribute("data-ry-initialized", ""), this.setup?.();
}
/**
* Called when element is removed from DOM.
* Automatically cleans up event listeners and observers.
*/
disconnectedCallback() {
this.#t.forEach((t) => t()), this.#t = [], this.#e && (this.#e.disconnect(), this.#e = null), this.teardown?.();
}
on(t, e, s, i = {}) {
const r = s.bind(this);
return t.addEventListener(e, r, i), this.#t.push(() => {
t.removeEventListener(e, r, i);
}), r;
}
/**
* Emit a custom event with ry: prefix.
*/
emit(t, e, s = {}) {
const i = new CustomEvent(`ry:${t}`, {
bubbles: !0,
cancelable: !0,
detail: e,
...s
});
return this.dispatchEvent(i);
}
/**
* Get/set component state via data attribute.
*/
get state() {
return this.dataset.ryState ?? "closed";
}
set state(t) {
const e = this.state;
e !== t && (this.dataset.ryState = t, this.emit("state-change", { oldState: e, newState: t }));
}
/**
* Query within this element.
*/
$(t) {
return this.querySelector(t);
}
/**
* Query all within this element.
*/
$$(t) {
return [...this.querySelectorAll(t)];
}
/**
* Setup a MutationObserver on this element.
*/
observe(t, e = { childList: !0, subtree: !0 }) {
this.#e && this.#e.disconnect(), this.#e = new MutationObserver(t.bind(this)), this.#e.observe(this, e);
}
/**
* Wait for next animation frame.
*/
nextFrame() {
return new Promise((t) => requestAnimationFrame(t));
}
/**
* Trap focus within a container (for modals, dropdowns).
*/
trapFocus(t) {
const e = [
"a[href]",
"button:not([disabled])",
"input:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
'[tabindex]:not([tabindex="-1"])'
].join(","), s = t.querySelectorAll(e), i = s[0], r = s[s.length - 1], a = (n) => {
n.key === "Tab" && (n.shiftKey ? document.activeElement === i && (n.preventDefault(), r?.focus()) : document.activeElement === r && (n.preventDefault(), i?.focus()));
};
this.on(t, "keydown", a), i?.focus();
}
}
const P = [
// Layout
"page",
"header",
"main",
"footer",
"section",
"aside",
"grid",
"stack",
"cluster",
"split",
"center",
"nav",
"logo",
"actions",
// Components
"accordion",
"accordion-item",
"tabs",
"tab",
"modal",
"dropdown",
"menu",
"menu-item",
"drawer",
"toast",
"alert",
"card",
"button",
"badge",
"field",
"switch",
"tooltip",
"select",
"option",
"theme-toggle",
"divider",
"icon",
"example",
"toggle-button",
"slider",
"knob",
"number-select",
"color-picker",
"color-input",
"gradient-picker",
"tree",
"tree-item",
"tag",
"tag-input",
"hero",
"stat",
"feature",
"feature-grid",
"pricing",
"pricing-card",
"carousel",
"combobox",
"theme-panel",
"testimonial",
"heading",
"search-list",
"search-item",
"search-group",
"logo-bar"
], F = new RegExp(
`<(/?)(${P.join("|")})(\\s|>|/)`,
"g"
);
function C(h) {
return h.replace(F, "<$1ry-$2$3");
}
function T() {
document.querySelectorAll("ry").forEach((h) => {
const t = C(h.innerHTML), e = document.createElement("template");
e.innerHTML = t, h.replaceWith(e.content);
}), document.querySelectorAll("template[ry]").forEach((h) => {
const t = C(h.innerHTML), e = document.createElement("template");
e.innerHTML = t, h.replaceWith(e.content);
});
}
document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", T) : T();
class O extends u {
setup() {
this.#t(), this.$$('[data-ry-target="trigger"]').forEach((t) => {
this.on(t, "click", this.#i);
}), this.on(this, "keydown", this.#s), this.#e();
}
#t() {
this.$$("ry-accordion-item").forEach((t) => {
const e = t.getAttribute("title") ?? "Untitled", s = t.hasAttribute("open"), i = t.innerHTML;
t.outerHTML = `
<div data-ry-target="item" class="ry-accordion__item" data-ry-state="${s ? "open" : "closed"}">
<button data-ry-target="trigger" class="ry-accordion__trigger" aria-expanded="${s}">
${e}
<span data-ry-target="icon" class="ry-accordion__icon">▼</span>
</button>
<div data-ry-target="panel" class="ry-accordion__panel">
<div class="ry-accordion__content">${i}</div>
</div>
</div>
`;
});
}
#e() {
this.$$('[data-ry-target="item"]').forEach((t, e) => {
const s = t.querySelector('[data-ry-target="trigger"]'), i = t.querySelector('[data-ry-target="panel"]');
if (!s || !i) return;
const r = s.id || `ry-accordion-trigger-${this.id || ""}-${e}`, a = i.id || `ry-accordion-panel-${this.id || ""}-${e}`;
s.id = r, i.id = a, s.setAttribute("aria-controls", a), i.setAttribute("aria-labelledby", r), i.setAttribute("role", "region");
const n = t.dataset.ryState === "open";
s.setAttribute("aria-expanded", String(n));
});
}
#i = (t) => {
const s = t.currentTarget.closest('[data-ry-target="item"]');
s && this.toggle(s);
};
#s = (t) => {
const e = this.$$('[data-ry-target="trigger"]'), s = e.indexOf(document.activeElement);
if (s === -1) return;
let i;
switch (t.key) {
case "ArrowDown":
t.preventDefault(), i = (s + 1) % e.length, e[i]?.focus();
break;
case "ArrowUp":
t.preventDefault(), i = (s - 1 + e.length) % e.length, e[i]?.focus();
break;
case "Home":
t.preventDefault(), e[0]?.focus();
break;
case "End":
t.preventDefault(), e[e.length - 1]?.focus();
break;
}
};
toggle(t) {
t.dataset.ryState === "open" ? this.close(t) : this.open(t);
}
open(t) {
this.hasAttribute("multiple") || this.$$('[data-ry-target="item"][data-ry-state="open"]').forEach((s) => {
s !== t && this.close(s);
});
const e = t.querySelector('[data-ry-target="trigger"]');
t.dataset.ryState = "open", e?.setAttribute("aria-expanded", "true"), this.emit("open", { item: t });
}
close(t) {
const e = t.querySelector('[data-ry-target="trigger"]');
t.dataset.ryState = "closed", e?.setAttribute("aria-expanded", "false"), this.emit("close", { item: t });
}
openAll() {
this.$$('[data-ry-target="item"]').forEach((t) => this.open(t));
}
closeAll() {
this.$$('[data-ry-target="item"]').forEach((t) => this.close(t));
}
}
customElements.define("ry-accordion", O);
customElements.get("ry-accordion-item") || customElements.define("ry-accordion-item", class extends HTMLElement {
});
class j extends u {
#t = null;
#e = 0;
setup() {
this.#i();
const t = this.id;
t && (document.querySelectorAll(`[data-ry-modal-trigger="${t}"]`).forEach((s) => {
this.on(s, "click", () => this.open());
}), document.querySelectorAll(`ry-button[modal="${t}"], [modal="${t}"]`).forEach((s) => {
this.on(s, "click", () => this.open());
})), this.$$("[close]").forEach((s) => {
this.on(s, "click", () => this.close());
});
const e = this.$('[data-ry-target="backdrop"]');
e && this.on(e, "click", () => this.close()), this.on(document, "keydown", this.#s), this.#e = window.innerWidth - document.documentElement.clientWidth, this.hasAttribute("data-ry-state") || (this.state = "closed");
}
#i() {
if (this.$('[data-ry-target="dialog"]')) return;
const t = this.getAttribute("title") ?? "", s = this.$("footer")?.innerHTML ?? "", i = Array.from(this.childNodes).filter((r) => !(r instanceof HTMLElement && r.tagName === "FOOTER")).map((r) => r instanceof Element ? r.outerHTML : r.textContent).join("");
this.innerHTML = `
<div data-ry-target="backdrop" class="ry-modal__backdrop"></div>
<div data-ry-target="dialog" class="ry-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="ry-modal-title-${this.id}">
<div data-ry-target="header" class="ry-modal__header">
<h3 class="ry-modal__title" id="ry-modal-title-${this.id}">${t}</h3>
<button data-ry-target="close" class="ry-modal__close" close aria-label="Close">×</button>
</div>
<div data-ry-target="body" class="ry-modal__body">${i}</div>
${s ? `<div data-ry-target="footer" class="ry-modal__footer">${s}</div>` : ""}
</div>
`;
}
#s = (t) => {
t.key === "Escape" && this.state === "open" && this.close();
};
open() {
if (this.state === "open") return;
this.#t = document.activeElement, document.body.setAttribute("data-ry-scroll-lock", ""), document.body.style.setProperty("--ry-scrollbar-width", `${this.#e}px`), this.state = "open";
const t = this.$('[data-ry-target="dialog"]');
t && this.nextFrame().then(() => {
this.trapFocus(t);
}), this.emit("open");
}
close() {
this.state !== "closed" && (document.body.removeAttribute("data-ry-scroll-lock"), document.body.style.removeProperty("--ry-scrollbar-width"), this.state = "closed", this.#t instanceof HTMLElement && (this.#t.focus(), this.#t = null), this.emit("close"));
}
toggle() {
this.state === "open" ? this.close() : this.open();
}
}
customElements.define("ry-modal", j);
class N extends u {
setup() {
this.#t(), this.$$('[data-ry-target="trigger"]').forEach((t) => {
this.on(t, "click", this.#i);
}), this.on(this, "keydown", this.#s), this.#e();
}
#t() {
const t = this.$$("ry-tab");
if (t.length === 0) return;
const e = t.findIndex((r) => r.hasAttribute("active")), s = e === -1 ? 0 : e, i = document.createElement("div");
i.setAttribute("data-ry-target", "list"), i.className = "ry-tabs__list", i.setAttribute("role", "tablist"), t.forEach((r, a) => {
const n = r.getAttribute("title") ?? `Tab ${a + 1}`, o = a === s, l = document.createElement("button");
l.setAttribute("data-ry-target", "trigger"), l.className = "ry-tabs__trigger", l.setAttribute("role", "tab"), l.setAttribute("aria-selected", String(o)), l.textContent = n, i.appendChild(l), r.setAttribute("data-ry-target", "panel"), r.className = "ry-tabs__panel", r.setAttribute("role", "tabpanel"), r.dataset.ryState = o ? "active" : "", r.removeAttribute("title"), r.removeAttribute("active");
}), this.insertBefore(i, this.firstChild);
}
#e() {
const t = this.$$('[data-ry-target="trigger"]'), e = this.$$('[data-ry-target="panel"], ry-tab');
t.forEach((i, r) => {
const a = e[r];
if (!a) return;
const n = i.id || `ry-tab-${this.id || ""}-${r}`, o = a.id || `ry-tabpanel-${this.id || ""}-${r}`;
i.id = n, a.id = o, i.setAttribute("aria-controls", o), a.setAttribute("aria-labelledby", n), i.setAttribute("tabindex", i.getAttribute("aria-selected") === "true" ? "0" : "-1");
}), t.findIndex((i) => i.getAttribute("aria-selected") === "true") === -1 && t.length > 0 && this.select(0);
}
#i = (t) => {
const e = t.currentTarget, i = this.$$('[data-ry-target="trigger"]').indexOf(e);
i !== -1 && this.select(i);
};
#s = (t) => {
const e = this.$$('[data-ry-target="trigger"]'), s = e.indexOf(document.activeElement);
if (s === -1) return;
let i;
switch (t.key) {
case "ArrowRight":
t.preventDefault(), i = (s + 1) % e.length, this.select(i), e[i]?.focus();
break;
case "ArrowLeft":
t.preventDefault(), i = (s - 1 + e.length) % e.length, this.select(i), e[i]?.focus();
break;
case "Home":
t.preventDefault(), this.select(0), e[0]?.focus();
break;
case "End":
t.preventDefault(), this.select(e.length - 1), e[e.length - 1]?.focus();
break;
}
};
select(t) {
const e = this.$$('[data-ry-target="trigger"]'), s = this.$$('[data-ry-target="panel"], ry-tab');
if (t < 0 || t >= e.length) return;
e.forEach((a) => {
a.setAttribute("aria-selected", "false"), a.setAttribute("tabindex", "-1");
}), s.forEach((a) => {
a.dataset.ryState = "", a.removeAttribute("active");
});
const i = e[t], r = s[t];
i && r && (i.setAttribute("aria-selected", "true"), i.setAttribute("tabindex", "0"), r.dataset.ryState = "active", this.emit("change", { index: t, trigger: i, panel: r }));
}
get selectedIndex() {
return this.$$('[data-ry-target="trigger"]').findIndex((e) => e.getAttribute("aria-selected") === "true");
}
}
customElements.define("ry-tabs", N);
customElements.get("ry-tab") || customElements.define("ry-tab", class extends HTMLElement {
});
class q extends u {
setup() {
this.#t();
const t = this.$('[data-ry-target="trigger"]');
t && (this.on(t, "click", () => this.toggle()), t.setAttribute("aria-haspopup", "true"), t.setAttribute("aria-expanded", "false")), this.on(document, "click", this.#e), this.on(document, "keydown", this.#i), this.$$('[data-ry-target="item"], ry-menu-item').forEach((e) => {
this.on(e, "click", () => this.close());
}), this.state = "closed";
}
#t() {
const t = this.$("ry-button:first-of-type");
t && !this.$('[data-ry-target="trigger"]') && t.setAttribute("data-ry-target", "trigger");
const e = this.$("ry-menu");
e && !e.hasAttribute("data-ry-target") && (e.setAttribute("data-ry-target", "menu"), e.classList.add("ry-dropdown__menu"), e.setAttribute("role", "menu")), this.$$("ry-menu-item").forEach((s) => {
s.hasAttribute("data-ry-target") || (s.setAttribute("data-ry-target", "item"), s.classList.add("ry-dropdown__item"), s.setAttribute("role", "menuitem"));
});
}
#e = (t) => {
const e = t.target;
this.state === "open" && !this.contains(e) && this.close();
};
#i = (t) => {
if (t.key === "Escape" && this.state === "open" && (this.close(), this.$('[data-ry-target="trigger"]')?.focus()), this.state === "open" && ["ArrowDown", "ArrowUp"].includes(t.key)) {
t.preventDefault();
const e = this.$$('[data-ry-target="item"], ry-menu-item'), s = e.indexOf(document.activeElement);
let i;
t.key === "ArrowDown" ? i = s < e.length - 1 ? s + 1 : 0 : i = s > 0 ? s - 1 : e.length - 1, e[i]?.focus();
}
};
open() {
if (this.state === "open") return;
this.#s(), this.$('[data-ry-target="trigger"]')?.setAttribute("aria-expanded", "true"), this.state = "open", this.nextFrame().then(() => {
this.$('[data-ry-target="item"], ry-menu-item')?.focus();
}), this.emit("open");
}
close() {
if (this.state === "closed") return;
this.$('[data-ry-target="trigger"]')?.setAttribute("aria-expanded", "false"), this.state = "closed", this.removeAttribute("data-ry-position"), this.emit("close");
}
#s() {
const t = this.$('[data-ry-target="menu"], ry-menu');
if (!t) return;
const e = this.getBoundingClientRect(), s = t.scrollHeight || 240, i = window.innerHeight - e.bottom, r = e.top;
i < s && r > i ? this.setAttribute("data-ry-position", "top") : this.setAttribute("data-ry-position", "bottom");
}
toggle() {
this.state === "open" ? this.close() : this.open();
}
}
customElements.define("ry-dropdown", q);
customElements.get("ry-menu") || customElements.define("ry-menu", class extends HTMLElement {
});
customElements.get("ry-menu-item") || customElements.define("ry-menu-item", class extends HTMLElement {
});
class K extends u {
setup() {
this.hasAttribute("tabindex") || this.setAttribute("tabindex", "0"), this.setAttribute("role", "button"), this.on(this, "click", this.#t), this.on(this, "keydown", this.#e);
}
#t = (t) => {
if (this.hasAttribute("disabled")) {
t.preventDefault();
return;
}
this.emit("click", { originalEvent: t });
};
#e = (t) => {
(t.key === "Enter" || t.key === " ") && (t.preventDefault(), this.click());
};
}
customElements.define("ry-button", K);
class z extends u {
#t = null;
#e = !1;
#i = 0;
#s = 0;
setup() {
if (!this.hasAttribute("resizable")) return;
this.#y(), this.#t = document.createElement("div"), this.#t.setAttribute("data-ry-target", "handle"), this.#t.className = "ry-split__handle", this.#t.setAttribute("role", "separator"), this.#t.setAttribute("aria-orientation", "vertical"), this.#t.setAttribute("tabindex", "0");
const t = this.lastElementChild;
t && this.insertBefore(this.#t, t), this.on(this.#t, "mousedown", this.#u), this.on(this.#t, "touchstart", this.#n), this.on(this.#t, "keydown", this.#f), this.on(this.#t, "dblclick", this.#A);
}
#r() {
return this.lastElementChild;
}
#a() {
const t = this.#r();
return t ? t.getBoundingClientRect().width : 300;
}
#o() {
const t = getComputedStyle(this).getPropertyValue("--ry-split-min-width").trim();
return t ? parseFloat(t) : 100;
}
#l() {
const t = getComputedStyle(this).getPropertyValue("--ry-split-max-width").trim();
return t ? parseFloat(t) : this.getBoundingClientRect().width * 0.8;
}
#c(t) {
const e = this.#o(), s = this.#l(), i = Math.round(Math.max(e, Math.min(s, t)));
this.style.setProperty("--ry-split-width", `${i}px`);
}
#u = (t) => {
t.preventDefault(), this.#d(t.clientX);
const e = (i) => this.#h(i.clientX), s = () => {
document.removeEventListener("mousemove", e), document.removeEventListener("mouseup", s), this.#b();
};
document.addEventListener("mousemove", e), document.addEventListener("mouseup", s);
};
#n = (t) => {
const e = t.touches[0];
if (!e) return;
t.preventDefault(), this.#d(e.clientX);
const s = (r) => {
const a = r.touches[0];
a && this.#h(a.clientX);
}, i = () => {
document.removeEventListener("touchmove", s), document.removeEventListener("touchend", i), this.#b();
};
document.addEventListener("touchmove", s, { passive: !1 }), document.addEventListener("touchend", i);
};
#d(t) {
this.#e = !0, this.#i = t, this.#s = this.#a(), this.setAttribute("data-ry-resizing", ""), document.body.style.cursor = "col-resize", document.body.style.userSelect = "none";
}
#h(t) {
if (!this.#e) return;
const e = this.#i - t;
this.#c(this.#s + e);
}
#b() {
this.#e = !1, this.removeAttribute("data-ry-resizing"), document.body.style.cursor = "", document.body.style.userSelect = "", this.#p(), this.emit("resize", { width: this.#a() });
}
#f = (t) => {
const e = t.shiftKey ? 50 : 10;
let s = this.#a();
t.key === "ArrowLeft" ? (t.preventDefault(), this.#c(s + e), this.#p()) : t.key === "ArrowRight" ? (t.preventDefault(), this.#c(s - e), this.#p()) : t.key === "Home" ? (t.preventDefault(), this.#c(this.#o()), this.#p()) : t.key === "End" && (t.preventDefault(), this.#c(this.#l()), this.#p());
};
#A = () => {
this.style.removeProperty("--ry-split-width"), this.#v(), this.emit("resize", { width: this.#a() });
};
// Persistence
#g() {
const t = this.getAttribute("persist");
return t ? `ry-split:${t}` : null;
}
#p() {
const t = this.#g();
if (t)
try {
localStorage.setItem(t, String(this.#a()));
} catch {
}
}
#y() {
const t = this.#g();
if (t)
try {
const e = localStorage.getItem(t);
if (e) {
const s = parseFloat(e);
isNaN(s) || this.style.setProperty("--ry-split-width", `${s}px`);
}
} catch {
}
}
#v() {
const t = this.#g();
if (t)
try {
localStorage.removeItem(t);
} catch {
}
}
}
customElements.define("ry-split", z);
class V extends u {
#t = ["light", "dark"];
#e = 0;
setup() {
const t = this.getAttribute("themes");
t && (this.#t = t.split(",").map((s) => s.trim()));
const e = document.documentElement.dataset.ryTheme ?? "light";
this.#e = this.#t.indexOf(e), this.#e === -1 && (this.#e = 0), this.innerHTML.trim() || (this.innerHTML = `<button data-ry-target="button" class="ry-btn ry-btn--ghost">${this.#i()}</button>`), this.on(this, "click", () => this.toggle());
}
#i() {
switch (this.#t[this.#e]) {
case "dark":
return "☾";
// Moon
case "ocean":
return "🌊";
// Wave
default:
return "☀";
}
}
toggle() {
this.#e = (this.#e + 1) % this.#t.length;
const t = this.#t[this.#e];
document.documentElement.dataset.ryTheme = t;
const e = this.$('[data-ry-target="button"]');
e && (e.innerHTML = this.#i()), this.emit("theme-change", { theme: t });
}
get theme() {
return this.#t[this.#e] ?? "light";
}
set theme(t) {
const e = this.#t.indexOf(t);
if (e !== -1) {
this.#e = e, document.documentElement.dataset.ryTheme = t;
const s = this.$('[data-ry-target="button"]');
s && (s.innerHTML = this.#i());
}
}
}
customElements.define("ry-theme-toggle", V);
class U extends u {
setup() {
this.hasAttribute("interactive") && (this.hasAttribute("tabindex") || this.setAttribute("tabindex", "0"), this.setAttribute("role", "link"), this.on(this, "click", this.#t), this.on(this, "keydown", this.#e));
}
#t = (t) => {
if (t.target.closest("a, button, ry-button")) return;
const s = this.getAttribute("href");
s && (window.location.href = s), this.emit("click", { originalEvent: t });
};
#e = (t) => {
(t.key === "Enter" || t.key === " ") && (t.preventDefault(), this.click());
};
}
customElements.define("ry-card", U);
class W extends u {
setup() {
this.#t();
const t = this.$("[close]");
t && this.on(t, "click", () => this.dismiss());
}
#t() {
if (this.$('[data-ry-target="content"]')) return;
const t = this.getAttribute("variant") ?? "info", e = this.hasAttribute("dismissible"), s = this.innerHTML;
this.innerHTML = `
<div data-ry-target="content" class="ry-alert__content">${s}</div>
${e ? '<button data-ry-target="close" class="ry-alert__close" close aria-label="Dismiss">×</button>' : ""}
`, this.setAttribute("role", "alert"), this.setAttribute("data-variant", t);
}
dismiss() {
this.state = "closed", setTimeout(() => {
this.remove();
}, 200), this.emit("close");
}
}
customElements.define("ry-alert", W);
class Y extends u {
static get observedAttributes() {
return ["label", "error", "hint"];
}
setup() {
this.#t();
}
attributeChangedCallback(t, e, s) {
e !== s && (t === "error" ? this.#e(s) : t === "hint" ? this.#i(s) : t === "label" && this.#s(s));
}
#t() {
const t = this.$("input, textarea, select");
if (!t) return;
const e = t.id || `ry-field-${Math.random().toString(36).slice(2, 9)}`;
if (t.id = e, !this.$('[data-ry-target="label"]')) {
const a = this.getAttribute("label");
if (a) {
const n = document.createElement("label");
n.setAttribute("data-ry-target", "label"), n.className = "ry-label", n.setAttribute("for", e), n.textContent = a, this.insertBefore(n, t);
}
}
if (!this.$('[data-ry-target="error"]')) {
const a = document.createElement("div");
a.setAttribute("data-ry-target", "error"), a.className = "ry-field__error", a.setAttribute("role", "alert");
const n = this.getAttribute("error");
n && (a.textContent = n), t.insertAdjacentElement("afterend", a);
}
if (!this.$('[data-ry-target="hint"]')) {
const a = document.createElement("div");
a.setAttribute("data-ry-target", "hint"), a.className = "ry-field__hint";
const n = this.getAttribute("hint");
n && (a.textContent = n);
const o = this.$('[data-ry-target="error"]');
o ? o.insertAdjacentElement("afterend", a) : t.insertAdjacentElement("afterend", a);
}
const s = this.$('[data-ry-target="error"]'), i = this.$('[data-ry-target="hint"]'), r = [];
s && (s.id = `${e}-error`, r.push(s.id)), i && (i.id = `${e}-hint`, r.push(i.id)), r.length && t.setAttribute("aria-describedby", r.join(" "));
}
#e(t) {
const e = this.$('[data-ry-target="error"]');
e && (e.textContent = t ?? "");
const s = this.$("input, textarea, select");
s && (t ? s.setAttribute("aria-invalid", "true") : s.removeAttribute("aria-invalid"));
}
#i(t) {
const e = this.$('[data-ry-target="hint"]');
e && (e.textContent = t ?? "");
}
#s(t) {
const e = this.$('[data-ry-target="label"]');
e && (e.textContent = t ?? "");
}
}
customElements.define("ry-field", Y);
let X = 0;
class G extends u {
#t = null;
static observedAttributes = ["checked", "disabled"];
setup() {
this.#e(), this.#t = this.$('input[type="checkbox"]'), this.#t && this.on(this.#t, "change", this.#i), this.on(this, "keydown", this.#s);
}
#e() {
if (this.$('[data-ry-target="track"]')) return;
const t = `ry-switch-${++X}`, e = this.getAttribute("name") ?? "", s = this.hasAttribute("checked"), i = this.hasAttribute("disabled"), r = this.textContent?.trim() ?? "";
this.innerHTML = `
<label data-ry-target="wrapper" class="ry-switch__wrapper" for="${t}">
<input
type="checkbox"
id="${t}"
data-ry-target="input"
class="ry-switch__input"
${e ? `name="${e}"` : ""}
${s ? "checked" : ""}
${i ? "disabled" : ""}
role="switch"
aria-checked="${s}"
>
<span data-ry-target="track" class="ry-switch__track">
<span data-ry-target="thumb" class="ry-switch__thumb"></span>
</span>
${r ? `<span data-ry-target="label" class="ry-switch__label">${r}</span>` : ""}
</label>
`;
}
#i = () => {
if (!this.#t) return;
const t = this.#t.checked;
this.#t.setAttribute("aria-checked", String(t)), t ? this.setAttribute("checked", "") : this.removeAttribute("checked"), this.emit("change", {
value: String(t),
label: t ? "on" : "off"
});
};
#s = (t) => {
(t.key === " " || t.key === "Enter") && (t.preventDefault(), this.#t?.click());
};
get checked() {
return this.#t?.checked ?? !1;
}
set checked(t) {
this.#t && (this.#t.checked = t, this.#t.setAttribute("aria-checked", String(t)), t ? this.setAttribute("checked", "") : this.removeAttribute("checked"));
}
get value() {
return this.#t?.value ?? "";
}
set value(t) {
this.#t && (this.#t.value = t);
}
}
customElements.define("ry-switch", G);
let J = 0;
class Z extends u {
#t = null;
#e = null;
#i = null;
setup() {
this.#i = `ry-tooltip-${++J}`, this.#s();
const t = this.firstElementChild;
t && (t.setAttribute("aria-describedby", this.#i), this.on(t, "mouseenter", this.#r), this.on(t, "mouseleave", this.#o), this.on(t, "focusin", this.#r), this.on(t, "focusout", this.#o)), this.on(document, "keydown", this.#l);
}
#s() {
this.#e = document.createElement("div"), this.#e.id = this.#i, this.#e.setAttribute("data-ry-target", "content"), this.#e.className = "ry-tooltip__content", this.#e.setAttribute("role", "tooltip"), this.#e.textContent = this.getAttribute("content") ?? "", this.appendChild(this.#e);
}
#r = () => {
this.#t && clearTimeout(this.#t), this.#t = setTimeout(() => {
this.#a();
}, 200);
};
#a = () => {
const t = this.getAttribute("position") ?? "top";
this.setAttribute("data-ry-position", t), this.state = "open";
};
#o = () => {
this.#t && (clearTimeout(this.#t), this.#t = null), this.state = "closed";
};
#l = (t) => {
t.key === "Escape" && this.state === "open" && this.#o();
};
teardown() {
this.#t && clearTimeout(this.#t);
}
}
customElements.define("ry-tooltip", Z);
class Q extends u {
#t = null;
#e = 0;
setup() {
this.#i();
const t = this.id;
t && document.querySelectorAll(`ry-button[drawer="${t}"], [drawer="${t}"]`).forEach((s) => {
this.on(s, "click", () => this.open());
}), this.$$("[close]").forEach((s) => {
this.on(s, "click", () => this.close());
});
const e = this.$('[data-ry-target="backdrop"]');
e && this.on(e, "click", () => this.close()), this.on(document, "keydown", this.#s), this.#e = window.innerWidth - document.documentElement.clientWidth, this.hasAttribute("open") ? this.state = "open" : this.hasAttribute("data-ry-state") || (this.state = "closed");
}
#i() {
if (this.$('[data-ry-target="panel"]')) return;
const t = this.getAttribute("side") ?? "left", e = this.innerHTML;
this.innerHTML = `
<div data-ry-target="backdrop" class="ry-drawer__backdrop"></div>
<div data-ry-target="panel" class="ry-drawer__panel" role="dialog" aria-modal="true" data-ry-side="${t}">
<button data-ry-target="close" class="ry-drawer__close" close aria-label="Close">×</button>
<div data-ry-target="content" class="ry-drawer__content">
${e}
</div>
</div>
`, this.setAttribute("data-ry-side", t);
}
#s = (t) => {
t.key === "Escape" && this.state === "open" && this.close();
};
open() {
if (this.state === "open") return;
this.#t = document.activeElement, document.body.setAttribute("data-ry-scroll-lock", ""), document.body.style.setProperty("--ry-scrollbar-width", `${this.#e}px`), this.state = "open";
const t = this.$('[data-ry-target="panel"]');
t && this.nextFrame().then(() => {
this.trapFocus(t);
}), this.emit("open");
}
close() {
this.state !== "closed" && (document.body.removeAttribute("data-ry-scroll-lock"), document.body.style.removeProperty("--ry-scrollbar-width"), this.state = "closed", this.#t instanceof HTMLElement && (this.#t.focus(), this.#t = null), this.emit("close"));
}
toggle() {
this.state === "open" ? this.close() : this.open();
}
}
customElements.define("ry-drawer", Q);
const tt = 4e3;
class m extends u {
#t = null;
static observedAttributes = ["variant", "duration"];
// Static container for programmatic toasts
static #e = null;
static #i() {
return m.#e || (m.#e = document.createElement("div"), m.#e.setAttribute("data-ry-target", "toast-container"), m.#e.className = "ry-toast-container", document.body.appendChild(m.#e)), m.#e;
}
// Static API
static show(t) {
const e = document.createElement("ry-toast");
return e.textContent = t.message, t.variant && e.setAttribute("variant", t.variant), t.duration !== void 0 && e.setAttribute("duration", String(t.duration)), m.#i().appendChild(e), e;
}
static success(t, e) {
return m.show({ message: t, variant: "success", duration: e });
}
static error(t, e) {
return m.show({ message: t, variant: "error", duration: e });
}
static info(t, e) {
return m.show({ message: t, variant: "info", duration: e });
}
static warning(t, e) {
return m.show({ message: t, variant: "warning", duration: e });
}
setup() {
this.#s();
const t = this.$("[close]");
t && this.on(t, "click", () => this.dismiss());
const e = parseInt(this.getAttribute("duration") ?? String(tt), 10);
e > 0 && (this.#t = setTimeout(() => this.dismiss(), e)), requestAnimationFrame(() => {
this.state = "visible";
});
}
#s() {
if (this.$('[data-ry-target="content"]')) return;
const t = this.getAttribute("variant") ?? "info", e = this.textContent?.trim() ?? "", s = this.#r(t);
this.innerHTML = `
<div data-ry-target="icon" class="ry-toast__icon">${s}</div>
<div data-ry-target="content" class="ry-toast__content">${e}</div>
<button data-ry-target="close" class="ry-toast__close" close aria-label="Dismiss">×</button>
`;
}
#r(t) {
const e = {
info: "ℹ",
success: "✓",
warning: "⚠",
error: "✕"
};
return e[t] ?? e.info;
}
dismiss() {
this.#t && (clearTimeout(this.#t), this.#t = null), this.state = "hiding", setTimeout(() => {
this.remove();
}, 300), this.emit("close");
}
teardown() {
this.#t && clearTimeout(this.#t);
}
}
customElements.define("ry-toast", m);
const w = {
// Close / X
close: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18"/><path d="M6 6l12 12"/></svg>',
// Chevron down
"chevron-down": '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>',
// Chevron up
"chevron-up": '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 15l-6-6-6 6"/></svg>',
// Chevron right
"chevron-right": '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>',
// Chevron left
"chevron-left": '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>',
// Check / Checkmark
check: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>',
// Copy
copy: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>',
// Sun (light mode)
sun: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="M4.93 4.93l1.41 1.41"/><path d="M17.66 17.66l1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="M6.34 17.66l-1.41 1.41"/><path d="M19.07 4.93l-1.41 1.41"/></svg>',
// Moon (dark mode)
moon: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>',
// Info
info: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
// Warning / Alert triangle
warning: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>',
// Error / X circle
error: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6"/><path d="M9 9l6 6"/></svg>',
// Success / Check circle
success: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9 12l2 2 4-4"/></svg>',
// Search
search: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>',
// Menu / Hamburger
menu: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h18"/><path d="M3 6h18"/><path d="M3 18h18"/></svg>',
// Plus
plus: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>',
// Minus
minus: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></svg>',
// Settings / Gear
settings: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/></svg>',
// User
user: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
// Heart
heart: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>',
// Star
star: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
// Trash
trash: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>',
// Edit / Pencil
edit: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
// External link
"external-link": '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>',
// Download
download: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
// Upload
upload: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>',
// Folder (closed)
folder: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 2H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/></svg>',
// Folder open
"folder-open": '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 2H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/><path d="M2 10h20"/></svg>',
// File
file: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>',
// Gradient: solid color
"gradient-solid": '<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="4" y="4" width="16" height="16" rx="2"/></svg>',
// Gradient: linear
"gradient-linear": '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="21" x2="21" y2="3"/><line x1="3" y1="15" x2="15" y2="3"/><line x1="9" y1="21" x2="21" y2="9"/></svg>',
// Gradient: radial
"gradient-radial": '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="6.5"/></svg>',
// Shape: circle
"shape-circle": '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/></svg>',
// Shape: ellipse
"shape-ellipse": '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="12" rx="10" ry="6"/></svg>',
// Quote
quote: '<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M4.583 17.321C3.553 16.227 3 15 3 13.011c0-3.5 2.457-6.637 6.03-8.188l.893 1.378c-3.335 1.804-3.987 4.145-4.247 5.621.537-.278 1.24-.375 1.929-.311C9.591 11.69 11 13.17 11 15c0 1.933-1.567 3.5-3.5 3.5-1.073 0-2.099-.49-2.917-1.179zM14.583 17.321C13.553 16.227 13 15 13 13.011c0-3.5 2.457-6.637 6.03-8.188l.893 1.378c-3.335 1.804-3.987 4.145-4.247 5.621.537-.278 1.24-.375 1.929-.311C19.591 11.69 21 13.17 21 15c0 1.933-1.567 3.5-3.5 3.5-1.073 0-2.099-.49-2.917-1.179z"/></svg>'
};
function g(h) {
return w[h] ?? "";
}
function Ut(h, t) {
w[h] = t;
}
function Wt(h) {
Object.assign(w, h);
}
function Yt() {
return Object.keys(w);
}
let et = 0;
class st extends u {
#t = null;
#e = -1;
#i = "";
#s = null;
#r = /* @__PURE__ */ new Set();
// Store options reference
_options = [];
get #a() {
return this.hasAttribute("multiple");
}
static observedAttributes = ["value", "disabled"];
setup() {
this.#t = `ry-select-${++et}`, this.#o(), this.hasAttribute("tabindex") || this.setAttribute("tabindex", "0"), this.on(this, "click", this.#l), this.on(this, "keydown", this.#c), this.on(document, "click", this.#u), this.#a && this.on(this, "ry:remove", this.#n), this.hasAttribute("data-ry-state") || (this.state = "closed");
const t = this.getAttribute("value");
if (t)
if (this.#a)
for (const e of t.split(",").map((s) => s.trim()).filter(Boolean))
this.#p(e);
else
this.value = t;
}
#o() {
const t = [...this.querySelectorAll("ry-option")].map((o) => ({
value: o.getAttribute("value") ?? o.textContent?.trim() ?? "",
label: o.textContent?.trim() ?? "",
disabled: o.hasAttribute("disabled")
})), e = this.getAttribute("placeholder") ?? "Select...", s = this.getAttribute("name") ?? "", i = this.hasAttribute("disabled"), r = this.#a, a = t.map(
(o) => `<option value="${o.value}"${o.disabled ? " disabled" : ""}>${o.label}</option>`
).join(""), n = t.map((o, l) => `
<div data-ry-target="option" class="ry-select__option"
role="option"
id="${this.#t}-option-${l}"
data-value="${o.value}"
${o.disabled ? "data-disabled" : ""}
aria-disabled="${o.disabled}">
${r ? `<span data-ry-target="check" class="ry-select__check">${g("check")}</span>` : ""}
<span class="ry-select__option-label">${o.label}</span>
</div>
`).join("");
if (r) {
const o = this.hasAttribute("clearable");
this.innerHTML = `
<div data-ry-target="trigger" class="ry-select__trigger ry-select__trigger--multi" aria-haspopup="listbox" aria-expanded="false">
<div data-ry-target="tags" class="ry-select__tags"></div>
<span data-ry-target="value" class="ry-select__value">${e}</span>
${o ? `<button data-ry-target="clear" class="ry-select__clear" aria-label="Clear all" type="button">${g("close")}</button>` : ""}
<span data-ry-target="arrow" class="ry-select__arrow">▾</span>
</div>
<div data-ry-target="dropdown" class="ry-select__dropdown" role="listbox" id="${this.#t}-listbox" tabindex="-1" aria-multiselectable="true">
${n}
</div>
<select data-ry-target="native" class="ry-select__native" ${s ? `name="${s}"` : ""} multiple tabindex="-1" aria-hidden="true" ${i ? "disabled" : ""}>
${a}
</select>
`;
const l = this.$('[data-ry-target="clear"]');
l && this.on(l, "click", (c) => {
c.stopPropagation(), this.#M();
});
} else
this.innerHTML = `
<div data-ry-target="trigger" class="ry-select__trigger" aria-haspopup="listbox" aria-expanded="false">
<span data-ry-target="value" class="ry-select__value">${e}</span>
<span data-ry-target="arrow" class="ry-select__arrow">▾</span>
</div>
<div data-ry-target="dropdown" class="ry-select__dropdown" role="listbox" id="${this.#t}-listbox" tabindex="-1">
${n}
</div>
<select data-ry-target="native" class="ry-select__native" ${s ? `name="${s}"` : ""} tabindex="-1" aria-hidden="true" ${i ? "disabled" : ""}>
<option value="">${e}</option>
${a}
</select>
`;
this._options = t;
}
#l = (t) => {
if (this.hasAttribute("disabled")) return;
const e = t.target, s = e.closest('[data-ry-target="option"]');
if (s instanceof HTMLElement && !s.hasAttribute("data-disabled")) {
if (this.#a) {
const r = s.dataset.value ?? "";
this.#p(r);
} else
this.#w(s), this.close();
return;
}
e.closest('[data-ry-target="trigger"]') && this.toggle();
};
#c = (t) => {
if (this.hasAttribute("disabled")) return;
const e = this.state === "open";
switch (t.key) {
case "Enter":
case " ":
if (t.preventDefault(), e && this.#e >= 0) {
const i = this.$$('[data-ry-target="option"]:not([data-disabled])')[this.#e];
if (i)
if (this.#a) {
const r = i.dataset.value ?? "";
this.#p(r);
} else
this.#w(i), this.close();
} else
this.toggle();
break;
case "Escape":
e && (t.preventDefault(), this.close());
break;
case "ArrowDown":
t.preventDefault(), e ? this.#b() : this.open();
break;
case "ArrowUp":
t.preventDefault(), e && this.#f();
break;
case "Home":
e && (t.preventDefault(), this.#A());
break;
case "End":
e && (t.preventDefault(), this.#g());
break;
case "Backspace":
if (this.#a && this.#r.size > 0) {
const s = [...this.#r], i = s[s.length - 1];
i && this.#p(i);
}
break;
default:
t.key.length === 1 && !t.ctrlKey && !t.metaKey && this.#d(t.key);
break;
}
};
#u = (t) => {
const e = t.target;
!this.contains(e) && this.state === "open" && this.close();
};
#n = (t) => {
const e = t.detail.value;
this.#r.has(e) && this.#p(e);
};
#d(t) {
this.#s && clearTimeout(this.#s), this.#i += t.toLowerCase();
const e = this.$$('[data-ry-target="option"]:not([data-disabled])'), s = e.findIndex(
(i) => i.textContent?.trim().toLowerCase().startsWith(this.#i)
);
if (s >= 0) {
if (this.state === "open")
this.#h(s);
else if (!this.#a) {
const i = e[s];
i && this.#w(i);
}
}
this.#s = setTimeout(() => {
this.#i = "";
}, 500);
}
#h(t) {
const e = this.$$('[data-ry-target="option"]:not([data-disabled])');
if (t < 0 || t >= e.length) return;
this.$$('[data-ry-target="option"][data-highlighted]').forEach((r) => {
r.removeAttribute("data-highlighted");
});
const s = e[t];
if (!s) return;
s.setAttribute("data-highlighted", ""), s.scrollIntoView({ block: "nearest" }), this.#e = t;
const i = this.$('[data-ry-target="trigger"]');
i && i.setAttribute("aria-activedescendant", s.id);
}
#b() {
const t = this.$$('[data-ry-target="option"]:not([data-disabled])'), e = Math.min(this.#e + 1, t.length - 1);
this.#h(e);
}
#f() {
const t = Math.max(this.#e - 1, 0);
this.#h(t);
}
#A() {
this.#h(0);
}
#g() {
const t = this.$$('[data-ry-target="option"]:not([data-disabled])');
this.#h(t.length - 1);
}
// --- Multi-select methods ---
#p(t) {
const e = this.getAttribute("max-selections");
if (this.#r.has(t))
this.#r.delete(t);
else {
if (e && this.#r.size >= parseInt(e, 10)) return;
this.#r.add(t);
}
this.#y(), this.#v(), this.#m(), this.setAttribute("value", [...this.#r].join(",")), this.emit("change", { value: this.value, label: "" });
}
#y() {
const t =