AkurAI Build
Menu

Public repository / f1f33c69567a554b6ce011822661d3289c1e9c77

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 = "&times;";

    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();
  }
})();