(function () {
"use strict";
function initDropdowns() {
const dropdowns = document.querySelectorAll("[data-rio-dropdown]");
if (!dropdowns.length) return;
dropdowns.forEach((dd) => {
const toggle = dd.querySelector(".rio-dropdown-toggle");
if (!toggle) return;
toggle.addEventListener("click", (e) => {
e.stopPropagation();
const open = dd.classList.toggle("is-open");
toggle.setAttribute("aria-expanded", String(open));
});
});
document.addEventListener("click", (e) => {
dropdowns.forEach((dd) => {
if (dd.classList.contains("is-open") && !dd.contains(e.target)) {
dd.classList.remove("is-open");
const t = dd.querySelector(".rio-dropdown-toggle");
if (t) t.setAttribute("aria-expanded", "false");
}
});
});
document.addEventListener("keydown", (e) => {
if (e.key !== "Escape") return;
dropdowns.forEach((dd) => {
if (!dd.classList.contains("is-open")) return;
dd.classList.remove("is-open");
const t = dd.querySelector(".rio-dropdown-toggle");
if (t) {
t.setAttribute("aria-expanded", "false");
t.focus();
}
});
});
}
function initBulkSelect() {
const form = document.querySelector("[data-rio-bulk]");
if (!form) return;
const all = form.querySelector("[data-rio-bulk-all]");
const idsInput = form.querySelector("[data-rio-bulk-ids]");
const countEl = form.querySelector("[data-rio-bulk-count]");
const clearBtn = form.querySelector("[data-rio-bulk-clear]");
const rows = Array.from(form.querySelectorAll("[data-rio-bulk-row]"));
if (!rows.length) return;
function refresh() {
const checked = rows.filter((r) => r.checked);
const count = checked.length;
idsInput.value = checked.map((r) => r.value).join(",");
if (countEl) countEl.textContent = String(count);
form.classList.toggle("is-active", count > 0);
rows.forEach((r) => {
const tr = r.closest("tr");
if (tr) tr.classList.toggle("is-selected", r.checked);
});
if (all) {
all.checked = count > 0 && count === rows.length;
all.indeterminate = count > 0 && count < rows.length;
}
}
rows.forEach((r) => r.addEventListener("change", refresh));
if (all) {
all.addEventListener("change", () => {
rows.forEach((r) => { r.checked = all.checked; });
refresh();
});
}
if (clearBtn) {
clearBtn.addEventListener("click", () => {
rows.forEach((r) => { r.checked = false; });
refresh();
});
}
form.addEventListener("submit", (e) => {
if (!idsInput.value) e.preventDefault();
});
refresh();
}
function initRowActions() {
const menus = Array.from(document.querySelectorAll("[data-rio-row-actions]"));
if (!menus.length) return;
let openMenu = null;
function placeMenu(details) {
const toggle = details.querySelector(".rio-row-actions__toggle");
const panel = details.querySelector(".rio-row-actions__menu");
if (!toggle || !panel) return;
panel.classList.add("is-floating");
const btn = toggle.getBoundingClientRect();
const panelRect = panel.getBoundingClientRect();
const gap = 4;
const margin = 8;
let top = btn.bottom + gap;
let left = btn.right - panelRect.width;
if (top + panelRect.height > window.innerHeight - margin) {
top = btn.top - panelRect.height - gap;
}
if (left < margin) left = margin;
const maxLeft = window.innerWidth - panelRect.width - margin;
if (left > maxLeft) left = maxLeft;
panel.style.top = `${Math.max(margin, top)}px`;
panel.style.left = `${left}px`;
}
function closeMenu() {
if (!openMenu) return;
const details = openMenu;
openMenu = null;
details.removeAttribute("open");
const panel = details.querySelector(".rio-row-actions__menu");
if (panel) {
panel.classList.remove("is-floating");
panel.style.top = "";
panel.style.left = "";
}
const toggle = details.querySelector(".rio-row-actions__toggle");
if (toggle) toggle.setAttribute("aria-expanded", "false");
}
function openOne(details) {
if (openMenu && openMenu !== details) closeMenu();
details.setAttribute("open", "");
openMenu = details;
const toggle = details.querySelector(".rio-row-actions__toggle");
if (toggle) toggle.setAttribute("aria-expanded", "true");
placeMenu(details);
}
menus.forEach((details) => {
const toggle = details.querySelector(".rio-row-actions__toggle");
if (!toggle) return;
toggle.setAttribute("aria-expanded", "false");
toggle.addEventListener("click", (e) => {
e.preventDefault();
if (details.hasAttribute("open")) closeMenu();
else openOne(details);
});
details.addEventListener("keydown", (e) => {
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
const items = Array.from(
details.querySelectorAll(".rio-row-actions__item")
);
if (!items.length) return;
e.preventDefault();
const current = items.indexOf(document.activeElement);
const step = e.key === "ArrowDown" ? 1 : -1;
const next = (current + step + items.length) % items.length;
items[next].focus();
});
});
document.addEventListener("click", (e) => {
if (!openMenu) return;
if (!openMenu.contains(e.target)) closeMenu();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && openMenu) {
const toggle = openMenu.querySelector(".rio-row-actions__toggle");
closeMenu();
if (toggle) toggle.focus();
}
});
window.addEventListener("scroll", closeMenu, true);
window.addEventListener("resize", closeMenu);
}
function initFkAutocomplete() {
const widgets = document.querySelectorAll("[data-rio-fk-autocomplete]");
widgets.forEach((widget) => {
const lookupUrl = widget.getAttribute("data-rio-fk-lookup-url");
if (!lookupUrl) return;
const search = widget.querySelector("[data-rio-fk-search]");
const idInput = widget.querySelector("[data-rio-fk-id]");
const results = widget.querySelector("[data-rio-fk-results]");
if (!search || !idInput || !results) return;
let debounce = 0;
let lastTerm = "";
function hideResults() {
results.setAttribute("hidden", "");
results.innerHTML = "";
}
function render(items) {
results.innerHTML = "";
if (!items.length) {
const empty = document.createElement("li");
empty.className = "rio-fk-autocomplete-empty";
empty.textContent = "No matches";
results.appendChild(empty);
} else {
items.forEach((item) => {
const li = document.createElement("li");
li.className = "rio-fk-autocomplete-result";
li.textContent = item.label;
li.setAttribute("role", "option");
li.setAttribute("data-id", String(item.id));
li.addEventListener("mousedown", (e) => {
e.preventDefault();
idInput.value = String(item.id);
search.value = item.label;
hideResults();
});
results.appendChild(li);
});
}
results.removeAttribute("hidden");
}
async function fetchResults(term) {
try {
const url = lookupUrl + "?q=" + encodeURIComponent(term);
const resp = await fetch(url, {
headers: { Accept: "application/json" },
credentials: "same-origin",
});
if (!resp.ok) return;
const items = await resp.json();
if (Array.isArray(items)) render(items);
} catch (_e) {
}
}
search.addEventListener("input", () => {
if (search.value !== lastTerm) idInput.value = "";
lastTerm = search.value;
window.clearTimeout(debounce);
const term = search.value.trim();
debounce = window.setTimeout(() => fetchResults(term), 250);
});
search.addEventListener("focus", () => {
if (search.value.trim().length > 0) {
window.clearTimeout(debounce);
debounce = window.setTimeout(() => fetchResults(search.value.trim()), 50);
}
});
search.addEventListener("blur", () => {
window.setTimeout(hideResults, 120);
});
search.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
hideResults();
}
});
});
}
function initSearchPalette() {
const trigger = document.querySelector("[data-rio-search-trigger]");
const palette = document.querySelector("[data-rio-search-palette]");
if (!palette) return;
const dialog = palette.querySelector("[data-rio-search-palette-dialog]");
const input = palette.querySelector("[data-rio-search-palette-input]");
const list = palette.querySelector("[data-rio-search-palette-results]");
if (!dialog || !input || !list) return;
let debounceTimer = 0;
let selectedIndex = -1;
let resultItems = [];
let optionIdCounter = 0;
let groupIdCounter = 0;
function open() {
palette.setAttribute("aria-hidden", "false");
input.value = "";
list.innerHTML = "";
selectedIndex = -1;
resultItems = [];
input.removeAttribute("aria-activedescendant");
setTimeout(() => input.focus(), 0);
}
function close() {
if (palette.getAttribute("aria-hidden") === "true") return;
palette.setAttribute("aria-hidden", "true");
window.clearTimeout(debounceTimer);
input.removeAttribute("aria-activedescendant");
if (trigger) trigger.focus();
}
function isOpen() {
return palette.getAttribute("aria-hidden") === "false";
}
function setSelected(idx) {
if (resultItems.length === 0) {
selectedIndex = -1;
input.removeAttribute("aria-activedescendant");
return;
}
const n = resultItems.length;
selectedIndex = ((idx % n) + n) % n;
resultItems.forEach((el, i) => {
el.classList.toggle("is-selected", i === selectedIndex);
});
resultItems[selectedIndex].scrollIntoView({ block: "nearest" });
input.setAttribute("aria-activedescendant", resultItems[selectedIndex].id);
}
function render(results) {
list.innerHTML = "";
resultItems = [];
selectedIndex = -1;
input.removeAttribute("aria-activedescendant");
optionIdCounter = 0;
groupIdCounter = 0;
if (!results.length) {
const empty = document.createElement("li");
empty.className = "rio-search-palette__empty";
empty.textContent = "No results.";
list.appendChild(empty);
return;
}
const groups = new Map();
results.forEach((r) => {
if (!groups.has(r.model_label)) groups.set(r.model_label, []);
groups.get(r.model_label).push(r);
});
groups.forEach((rows, label) => {
const group = document.createElement("li");
group.className = "rio-search-palette__group";
const headingId = `rio-search-palette__group-${groupIdCounter++}`;
group.setAttribute("role", "group");
group.setAttribute("aria-labelledby", headingId);
const heading = document.createElement("span");
heading.className = "rio-search-palette__group-label";
heading.id = headingId;
heading.textContent = label;
group.appendChild(heading);
rows.forEach((r) => {
const a = document.createElement("a");
a.className = "rio-search-palette__result";
a.href = r.url;
a.id = `rio-search-palette__option-${optionIdCounter++}`;
a.setAttribute("role", "option");
a.setAttribute("tabindex", "-1");
const text = document.createElement("span");
text.className = "rio-search-palette__result-label";
text.textContent = r.label;
a.appendChild(text);
group.appendChild(a);
resultItems.push(a);
});
list.appendChild(group);
});
setSelected(0);
}
async function fetchResults(term) {
try {
const url = "/admin/_search?q=" + encodeURIComponent(term);
const resp = await fetch(url, {
headers: { Accept: "application/json" },
credentials: "same-origin",
});
if (!resp.ok) return;
const body = await resp.json();
if (body && Array.isArray(body.results)) render(body.results);
} catch (_e) {
}
}
if (trigger) trigger.addEventListener("click", open);
palette.addEventListener("click", (e) => {
if (e.target === palette) close();
});
input.addEventListener("input", () => {
window.clearTimeout(debounceTimer);
const term = input.value.trim();
if (term.length < 2) {
list.innerHTML = "";
resultItems = [];
selectedIndex = -1;
return;
}
debounceTimer = window.setTimeout(() => fetchResults(term), 200);
});
input.addEventListener("keydown", (e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
setSelected(selectedIndex + 1);
} else if (e.key === "ArrowUp") {
e.preventDefault();
setSelected(selectedIndex - 1);
} else if (e.key === "Tab") {
e.preventDefault();
if (resultItems.length > 0) {
setSelected(selectedIndex + (e.shiftKey ? -1 : 1));
}
} else if (e.key === "Enter") {
if (selectedIndex >= 0 && resultItems[selectedIndex]) {
e.preventDefault();
window.location.href = resultItems[selectedIndex].href;
}
}
});
function focusInOtherTextInput(target) {
if (!(target instanceof Element)) return false;
if (target === input) return false;
return target.matches("input, textarea, [contenteditable], [contenteditable='true']");
}
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && isOpen()) {
e.preventDefault();
close();
return;
}
const isCmdK = (e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K");
if (isCmdK) {
if (focusInOtherTextInput(e.target)) return;
e.preventDefault();
if (isOpen()) close();
else open();
}
});
}
function syncThemeIcons(theme) {
const moon = document.querySelector(".rio-theme-moon");
const sun = document.querySelector(".rio-theme-sun");
if (!moon || !sun) return;
const dark = theme === "dark";
moon.style.display = dark ? "none" : "";
sun.style.display = dark ? "" : "none";
}
function initConsole() {
const themeBtn = document.getElementById("themeToggle");
if (themeBtn) {
themeBtn.addEventListener("click", () => {
const next =
document.documentElement.getAttribute("data-theme") === "dark"
? "light"
: "dark";
document.documentElement.setAttribute("data-theme", next);
try { localStorage.setItem("rio-theme", next); } catch (e) {}
syncThemeIcons(next);
});
}
syncThemeIcons(document.documentElement.getAttribute("data-theme") || "light");
const rail = document.getElementById("rail");
const railBtn = document.getElementById("railToggle");
if (rail && railBtn) {
try {
if (localStorage.getItem("rio-rail-open") === "0") {
rail.classList.remove("rio-rail--open");
railBtn.setAttribute("aria-expanded", "false");
} else {
rail.classList.add("rio-rail--open");
railBtn.setAttribute("aria-expanded", "true");
}
} catch (e) {}
railBtn.addEventListener("click", () => {
const open = rail.classList.toggle("rio-rail--open");
railBtn.setAttribute("aria-expanded", String(open));
try { localStorage.setItem("rio-rail-open", open ? "1" : "0"); } catch (e) {}
});
}
}
function initViewDesigner() {
const form = document.querySelector("[data-rio-vd]");
if (!form) return;
const list = form.querySelector(".rio-vd-rows");
if (!list) return;
function renumber() {
Array.from(list.querySelectorAll("[data-rio-vd-row]")).forEach((row, i) => {
const prio = row.querySelector("[data-rio-vd-priority]");
if (prio) prio.value = String(i * 10);
});
}
list.addEventListener("click", (e) => {
const up = e.target.closest("[data-rio-vd-up]");
const down = e.target.closest("[data-rio-vd-down]");
if (!up && !down) return;
e.preventDefault();
const row = e.target.closest("[data-rio-vd-row]");
if (!row) return;
if (up && row.previousElementSibling) {
list.insertBefore(row, row.previousElementSibling);
} else if (down && row.nextElementSibling) {
list.insertBefore(row.nextElementSibling, row);
}
renumber();
});
list.addEventListener("change", (e) => {
const sel = e.target.closest("[data-rio-vd-role]");
if (!sel) return;
const row = sel.closest("[data-rio-vd-row]");
const dot = row && row.querySelector("[data-rio-vd-dot]");
if (dot) dot.className = "rio-vd-dot rio-vd-dot--" + sel.value;
});
}
function initBranding() {
const root = document.querySelector("[data-rio-branding]");
if (!root) return;
const color = root.querySelector("[data-rio-brand-color]");
const hex = root.querySelector("[data-rio-brand-hex]");
const preview = document.querySelector("[data-rio-brand-preview]");
const cmd = document.querySelector("[data-rio-brand-cmd]");
const rust = document.querySelector("[data-rio-brand-rust]");
const clamp = (n) => Math.max(0, Math.min(255, n));
const parse = (h) => {
const m = /^#?([0-9a-f]{6})$/i.exec((h || "").trim());
if (!m) return null;
const i = parseInt(m[1], 16);
return [(i >> 16) & 255, (i >> 8) & 255, i & 255];
};
const toHex = (rgb) => "#" + rgb.map((x) => clamp(Math.round(x)).toString(16).padStart(2, "0")).join("");
const darken = (rgb, f) => rgb.map((c) => c * (1 - f));
function apply(h) {
const rgb = parse(h);
if (!rgb) return;
const hh = toHex(rgb);
const hov = toHex(darken(rgb, 0.15));
const act = toHex(darken(rgb, 0.28));
if (preview) {
const s = preview.style;
s.setProperty("--rio-rust", hh);
s.setProperty("--rio-rust-hover", hov);
s.setProperty("--rio-rust-solid", hh);
s.setProperty("--rio-rust-solid-hover", hov);
s.setProperty("--rio-rust-solid-active", act);
s.setProperty("--rio-accent", hh);
s.setProperty("--rio-rust-tint", `rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.10)`);
s.setProperty("--rio-rust-tint-2", `rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.18)`);
}
if (cmd) cmd.textContent = `rustio-admin theme generate --brand '${hh}' --out generated/tokens.css`;
if (rust) rust.textContent = `Admin::new().accent_color("${hh}")`;
}
function sync(from) {
const rgb = parse(from.value);
if (!rgb) return;
const hh = toHex(rgb);
if (color) color.value = hh;
if (hex) hex.value = hh;
apply(hh);
}
if (color) color.addEventListener("input", () => sync(color));
if (hex) hex.addEventListener("input", () => { if (parse(hex.value)) sync(hex); });
root.querySelectorAll("[data-hex]").forEach((btn) => {
btn.addEventListener("click", () => {
const h = btn.getAttribute("data-hex");
if (hex) hex.value = h;
if (color) color.value = h;
apply(h);
});
});
apply((hex && hex.value) || "#B84318");
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
initConsole();
initDropdowns();
initRowActions();
initBulkSelect();
initFkAutocomplete();
initSearchPalette();
initViewDesigner();
initBranding();
});
} else {
initConsole();
initDropdowns();
initRowActions();
initBulkSelect();
initFkAutocomplete();
initSearchPalette();
initViewDesigner();
initBranding();
}
})();