Menu
Public repository / master
AkurAI-Framework / site/frontend/app.js
Latest change 2e9c26e6274a8310cc2db17fb9390a7af4ab0913 - feat(site): live component previews + copyable curl install widget by Ólafur Búi Ólafsson
// Native ES module — no bundler. Pings the framework's health endpoint.
const el = document.getElementById("health");
if (el) {
fetch("/api/health", { headers: { accept: "application/json" } })
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`status ${r.status}`))))
.then((d) => {
el.textContent = `server up · ${d.framework} ${d.version}`;
el.classList.add("ok");
})
.catch((e) => (el.textContent = `server unreachable: ${e.message}`));
}
// Realtime demo — only runs on /realtime, where the panels exist.
if (document.getElementById("sse-count")) initRealtime();
function initRealtime() {
startSse();
startWs();
}
// Server-Sent Events: native EventSource consuming /api/events.
function startSse() {
const count = document.getElementById("sse-count");
const log = document.getElementById("sse-log");
const status = document.getElementById("sse-status");
const es = new EventSource("/api/events");
es.addEventListener("open", () => setStatus(status, true, "connected"));
es.addEventListener("error", () => setStatus(status, false, "reconnecting…"));
es.addEventListener("tick", (e) => {
count.textContent = e.data;
logLine(log, `tick → ${e.data}`);
});
}
// WebSocket: native WebSocket against /api/ws, echoing what you send.
function startWs() {
const log = document.getElementById("ws-log");
const status = document.getElementById("ws-status");
const form = document.getElementById("ws-form");
const input = document.getElementById("ws-input");
const scheme = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${scheme}://${location.host}/api/ws`);
ws.addEventListener("open", () => setStatus(status, true, "connected"));
ws.addEventListener("close", () => setStatus(status, false, "closed"));
ws.addEventListener("error", () => setStatus(status, false, "error"));
ws.addEventListener("message", (e) => logLine(log, `echo → ${e.data}`, "in"));
form.addEventListener("submit", (event) => {
event.preventDefault();
const msg = input.value.trim();
if (!msg || ws.readyState !== WebSocket.OPEN) return;
ws.send(msg);
logLine(log, `sent → ${msg}`, "out");
input.value = "";
});
}
function setStatus(el, ok, text) {
el.textContent = text;
el.classList.toggle("ok", ok);
}
// Prepend a line to a log list, capping its length so it never grows forever.
function logLine(el, text, kind) {
const li = document.createElement("li");
li.textContent = text;
if (kind) li.className = `rt-${kind}`;
el.prepend(li);
while (el.children.length > 8) el.lastChild.remove();
}
// ── copy-to-clipboard widgets ──
// Generic: any [data-clipboard] container with a [data-copy-btn] button copies
// the container's data-clipboard string. Used by the landing-page install block.
(function copyInit() {
"use strict";
function fallbackCopy(text) {
var ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "absolute";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
var ok = false;
try {
ok = document.execCommand("copy");
} catch (e) {
ok = false;
}
document.body.removeChild(ta);
return ok;
}
function flash(btn) {
if (btn.dataset.copyBusy === "1") return;
btn.dataset.copyBusy = "1";
var original = btn.textContent;
btn.textContent = "Copied!";
btn.classList.add("is-copied");
setTimeout(function () {
btn.textContent = original;
btn.classList.remove("is-copied");
delete btn.dataset.copyBusy;
}, 1500);
}
function bind(widget) {
var btn = widget.querySelector("[data-copy-btn]");
if (!btn) return;
btn.addEventListener("click", function () {
var text = widget.getAttribute("data-clipboard") || "";
if (!text) return;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(
function () { flash(btn); },
function () { if (fallbackCopy(text)) flash(btn); }
);
} else if (fallbackCopy(text)) {
flash(btn);
}
});
}
function copyStart() {
document.querySelectorAll("[data-clipboard]").forEach(bind);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", copyStart);
} else {
copyStart();
}
})();
// ── components: actions ──
/* Actions — progressive enhancement only. Native <details> handles open/close. */
(function actionsInit() {
// Button group: single-select view switcher (aria-pressed + is-active).
var actionsGroups = document.querySelectorAll('.btn-group[role="group"]');
actionsGroups.forEach(function (actionsGroup) {
actionsGroup.addEventListener('click', function (e) {
var actionsBtn = e.target.closest('.btn');
if (!actionsBtn || !actionsGroup.contains(actionsBtn)) return;
actionsGroup.querySelectorAll('.btn').forEach(function (b) {
var active = b === actionsBtn;
b.classList.toggle('is-active', active);
b.setAttribute('aria-pressed', String(active));
});
});
});
// Dropdowns: close on outside click, Esc, and menu-item activation.
var actionsDropdowns = document.querySelectorAll('details.dropdown');
if (!actionsDropdowns.length) return;
function actionsCloseAll(except) {
actionsDropdowns.forEach(function (d) {
if (d !== except) d.removeAttribute('open');
});
}
actionsDropdowns.forEach(function (actionsD) {
actionsD.addEventListener('toggle', function () {
if (actionsD.open) actionsCloseAll(actionsD);
});
var actionsMenu = actionsD.querySelector('.menu');
if (actionsMenu) {
actionsMenu.addEventListener('click', function (e) {
if (e.target.closest('.menu-item')) actionsD.removeAttribute('open');
});
}
});
document.addEventListener('click', function (e) {
if (!e.target.closest('details.dropdown')) actionsCloseAll(null);
});
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
actionsDropdowns.forEach(function (d) {
if (d.open) {
d.removeAttribute('open');
var s = d.querySelector('summary');
if (s) s.focus();
}
});
});
})();
// ── components: forms ──
// Forms extras — progressive enhancement only. Components work without this.
(function () {
"use strict";
// Search: wire the clear (×) button to empty + refocus its input.
function formsInitSearch(root) {
root.querySelectorAll(".search").forEach(function (wrap) {
var input = wrap.querySelector('input[type="search"]');
var clear = wrap.querySelector(".search-clear");
if (!input || !clear) return;
clear.addEventListener("click", function () {
input.value = "";
input.focus();
input.dispatchEvent(new Event("input", { bubbles: true }));
});
});
}
// Dropzone: toggle .is-dragover on real drag events. Native input still does the upload.
function formsInitDropzone(root) {
root.querySelectorAll(".dropzone").forEach(function (zone) {
var input = zone.querySelector(".dropzone-input");
if (!input) return;
["dragenter", "dragover"].forEach(function (evt) {
zone.addEventListener(evt, function (e) {
e.preventDefault();
zone.classList.add("is-dragover");
});
});
["dragleave", "dragend", "drop"].forEach(function (evt) {
zone.addEventListener(evt, function (e) {
if (evt === "dragleave" && zone.contains(e.relatedTarget)) return;
zone.classList.remove("is-dragover");
});
});
zone.addEventListener("drop", function (e) {
e.preventDefault();
if (e.dataTransfer && e.dataTransfer.files.length) {
input.files = e.dataTransfer.files;
input.dispatchEvent(new Event("change", { bubbles: true }));
}
});
});
}
function formsInit() {
var root = document;
formsInitSearch(root);
formsInitDropzone(root);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", formsInit);
} else {
formsInit();
}
})();
// ── components: data ──
/* Data display — code-block copy button */
(function dataDisplayInit() {
"use strict";
var buttons = document.querySelectorAll("[data-display-copy]");
if (!buttons.length) return;
buttons.forEach(function dataDisplayBind(btn) {
btn.addEventListener("click", function dataDisplayCopy() {
var block = btn.closest(".code-block");
var code = block && block.querySelector("code");
if (!code || !navigator.clipboard) return;
navigator.clipboard.writeText(code.textContent).then(function dataDisplayDone() {
var original = btn.textContent;
btn.textContent = "Copied";
setTimeout(function dataDisplayReset() { btn.textContent = original; }, 1500);
}).catch(function dataDisplayFail() { /* clipboard denied — silent */ });
});
});
})();
// ── components: feedback ──
// ============================================================
// Feedback components — toast spawning + dismiss wiring
// No-build native ES module. No global leakage (IIFE).
// ============================================================
(function () {
"use strict";
var feedbackIcons = {
success: "✓", // ✓
info: "i",
warning: "!",
danger: "✕" // ✕
};
var feedbackTitles = {
success: "Success",
info: "Heads up",
warning: "Warning",
danger: "Error"
};
function feedbackRemoveToast(toast) {
if (!toast || !toast.parentNode) return;
toast.classList.add("toast-leaving");
var done = function () { if (toast.parentNode) toast.parentNode.removeChild(toast); };
// Fall back to immediate removal if animations are off/unsupported.
var handled = false;
toast.addEventListener("animationend", function () { handled = true; done(); }, { once: true });
setTimeout(function () { if (!handled) done(); }, 300);
}
function feedbackToast(opts) {
opts = opts || {};
var region = document.getElementById("feedback-toast-region");
if (!region) return;
var variant = opts.variant || "info";
var title = opts.title || feedbackTitles[variant] || feedbackTitles.info;
var text = opts.text || "";
var duration = typeof opts.duration === "number" ? opts.duration : 4000;
var toast = document.createElement("div");
toast.className = "toast toast-" + variant;
toast.setAttribute("role", "status");
var icon = document.createElement("span");
icon.className = "toast-icon";
icon.setAttribute("aria-hidden", "true");
icon.textContent = feedbackIcons[variant] || feedbackIcons.info;
var titleEl = document.createElement("span");
titleEl.className = "toast-title";
titleEl.textContent = title;
var close = document.createElement("button");
close.type = "button";
close.className = "toast-close";
close.setAttribute("aria-label", "Dismiss notification");
close.innerHTML = "×";
var textEl = document.createElement("span");
textEl.className = "toast-text";
textEl.textContent = text;
toast.appendChild(icon);
toast.appendChild(titleEl);
toast.appendChild(close);
toast.appendChild(textEl);
region.appendChild(toast);
var timer = null;
var dismiss = function () {
if (timer) clearTimeout(timer);
feedbackRemoveToast(toast);
};
close.addEventListener("click", dismiss);
if (duration > 0) timer = setTimeout(dismiss, duration);
return toast;
}
function feedbackInit() {
// Demo trigger — only bind if present.
var btn = document.getElementById("toast-demo-btn");
if (btn) {
var variants = ["success", "info", "warning", "danger"];
var i = 0;
var samples = {
success: "Your changes were saved successfully.",
info: "A new version is available.",
warning: "Storage is at 92% capacity.",
danger: "The connection was lost. Try again."
};
btn.addEventListener("click", function () {
var v = variants[i % variants.length];
i++;
feedbackToast({ variant: v, text: samples[v] });
});
}
// Static toast-close buttons (reference examples).
var staticCloses = document.querySelectorAll(".toast-region .toast-close, .comp-cat .toast > .toast-close");
Array.prototype.forEach.call(document.querySelectorAll(".comp-cat .toast .toast-close"), function (c) {
c.addEventListener("click", function () {
var t = c.closest(".toast");
if (t) feedbackRemoveToast(t);
});
});
void staticCloses;
// Dismissible banners.
Array.prototype.forEach.call(document.querySelectorAll("[data-feedback-dismissible] .banner-close"), function (c) {
c.addEventListener("click", function () {
var b = c.closest(".banner");
if (b && b.parentNode) b.parentNode.removeChild(b);
});
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", feedbackInit);
} else {
feedbackInit();
}
})();
// ── components: nav ──
/* ============================================================
Navigation — Tabs interactivity (roving tabindex + arrows)
No-build IIFE. Initializes every .tabs group present in DOM.
============================================================ */
(function navTabs() {
"use strict";
var groups = document.querySelectorAll(".tabs");
if (!groups.length) return;
Array.prototype.forEach.call(groups, function (tablist) {
var tabs = Array.prototype.slice.call(
tablist.querySelectorAll('[role="tab"]')
);
if (!tabs.length) return;
function panelFor(tab) {
var id = tab.getAttribute("aria-controls");
return id ? document.getElementById(id) : null;
}
function select(tab, focus) {
tabs.forEach(function (t) {
var on = t === tab;
t.setAttribute("aria-selected", on ? "true" : "false");
t.tabIndex = on ? 0 : -1;
var panel = panelFor(t);
if (panel) panel.hidden = !on;
});
if (focus) tab.focus();
}
tablist.addEventListener("click", function (e) {
var tab = e.target.closest('[role="tab"]');
if (tab && tabs.indexOf(tab) !== -1) select(tab, false);
});
tablist.addEventListener("keydown", function (e) {
var idx = tabs.indexOf(document.activeElement);
if (idx === -1) return;
var next = null;
switch (e.key) {
case "ArrowRight":
case "ArrowDown":
next = tabs[(idx + 1) % tabs.length];
break;
case "ArrowLeft":
case "ArrowUp":
next = tabs[(idx - 1 + tabs.length) % tabs.length];
break;
case "Home":
next = tabs[0];
break;
case "End":
next = tabs[tabs.length - 1];
break;
default:
return;
}
e.preventDefault();
select(next, true);
});
// Normalize initial state from markup (first selected, or index 0).
var initial =
tabs.filter(function (t) {
return t.getAttribute("aria-selected") === "true";
})[0] || tabs[0];
select(initial, false);
});
})();
// ── components: overlays ──
/* ============================================================
Overlays — Modal + Drawer behavior. Tiny, dependency-free.
Native <dialog> handles focus trap / Esc / backdrop for the
modal; the drawer gets manual Esc + focus management.
============================================================ */
(function () {
"use strict";
/* ---------- Modal: native <dialog> ---------- */
function overlaysInitModals() {
document.querySelectorAll("[data-open-modal]").forEach(function (trigger) {
var dialog = document.getElementById(trigger.getAttribute("data-open-modal"));
if (!dialog || typeof dialog.showModal !== "function") return;
trigger.addEventListener("click", function () {
dialog.showModal();
});
dialog.querySelectorAll("[data-close-modal]").forEach(function (btn) {
btn.addEventListener("click", function () {
dialog.close();
});
});
});
}
/* ---------- Drawer: manual open/close ---------- */
function overlaysInitDrawers() {
document.querySelectorAll("[data-open-drawer]").forEach(function (trigger) {
var drawer = document.getElementById(trigger.getAttribute("data-open-drawer"));
if (!drawer) return;
var lastFocused = null;
function open() {
lastFocused = document.activeElement;
drawer.classList.add("is-open");
drawer.setAttribute("aria-hidden", "false");
var focusable = drawer.querySelector(
"button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])"
);
if (focusable) focusable.focus();
document.addEventListener("keydown", onKey);
}
function close() {
drawer.classList.remove("is-open");
drawer.setAttribute("aria-hidden", "true");
document.removeEventListener("keydown", onKey);
if (lastFocused && typeof lastFocused.focus === "function") lastFocused.focus();
}
function onKey(e) {
if (e.key === "Escape") {
e.preventDefault();
close();
}
}
trigger.addEventListener("click", open);
drawer.querySelectorAll("[data-close-drawer]").forEach(function (btn) {
btn.addEventListener("click", close);
});
});
}
function overlaysInit() {
overlaysInitModals();
overlaysInitDrawers();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", overlaysInit);
} else {
overlaysInit();
}
})();