nido-core 0.0.0-alpha.0

Pre-release metadata crate. Real Nido core types and traits ship at >=0.1.0.
Documentation
const COMPARE_STORAGE_KEY = "nido-groundwork-compare";

function loadCompare() {
  try {
    const raw = localStorage.getItem(COMPARE_STORAGE_KEY);
    if (!raw) return new Set();
    const parsed = JSON.parse(raw);
    if (!Array.isArray(parsed)) return new Set();
    return new Set(parsed.filter((role) => typeof role === "string"));
  } catch {
    localStorage.removeItem(COMPARE_STORAGE_KEY);
    return new Set();
  }
}

const state = {
  capabilities: [],
  query: "",
  layer: "all",
  gate: "all",
  adoption: "all",
  sort: "role",
  selectedRole: "",
  compare: loadCompare(),
};

const els = {
  search: document.querySelector("#search"),
  layerFilters: document.querySelector("#layerFilters"),
  gateButtons: [...document.querySelectorAll("[data-gate]")],
  adoptionButtons: [...document.querySelectorAll("[data-adoption]")],
  sort: document.querySelector("#sort"),
  clear: document.querySelector("#clearFilters"),
  catalog: document.querySelector("#catalogList"),
  details: document.querySelector("#details"),
  compare: document.querySelector("#compareList"),
  total: document.querySelector("[data-total-count]"),
  gated: document.querySelector("[data-gated-count]"),
  layers: document.querySelector("[data-layer-count]"),
  visible: document.querySelector("[data-visible-count]"),
  standard: document.querySelector("[data-standard-count]"),
  review: document.querySelector("[data-review-count]"),
};

function text(value) {
  return document.createTextNode(value);
}

function element(tag, attrs = {}, children = []) {
  const node = document.createElement(tag);
  for (const [name, value] of Object.entries(attrs)) {
    if (value === false || value === null || value === undefined) continue;
    if (name === "class") node.className = value;
    else if (name === "dataset") {
      for (const [key, dataValue] of Object.entries(value)) node.dataset[key] = dataValue;
    } else if (name.startsWith("on") && typeof value === "function") {
      node.addEventListener(name.slice(2).toLowerCase(), value);
    } else if (value === true) {
      node.setAttribute(name, "");
    } else {
      node.setAttribute(name, value);
    }
  }
  for (const child of children) {
    node.append(child instanceof Node ? child : text(String(child)));
  }
  return node;
}

function titleize(value) {
  return value
    .replaceAll("_", " ")
    .replaceAll("-", " ")
    .replace(/\b[a-z]/g, (letter) => letter.toUpperCase());
}

function matches(capability) {
  const haystack = `${capability.role} ${capability.layer} ${capability.summary}`.toLowerCase();
  return (
    (!state.query || haystack.includes(state.query.toLowerCase())) &&
    (state.layer === "all" || capability.layer === state.layer) &&
    (state.gate === "all" || capability.release_gate === state.gate) &&
    (state.adoption === "all" || capability.adoption_status === state.adoption)
  );
}

function sorted(items) {
  const collator = new Intl.Collator("en");
  return [...items].sort((a, b) => {
    if (state.sort === "layer") {
      return collator.compare(a.layer, b.layer) || collator.compare(a.role, b.role);
    }
    if (state.sort === "gate") {
      return collator.compare(a.release_gate, b.release_gate) || collator.compare(a.role, b.role);
    }
    return collator.compare(a.role, b.role);
  });
}

function filtered() {
  return sorted(state.capabilities.filter(matches));
}

function setPressed(buttons, attr, value) {
  for (const button of buttons) {
    button.setAttribute("aria-pressed", button.dataset[attr] === value ? "true" : "false");
  }
}

function syncUrl() {
  const params = new URLSearchParams();
  if (state.query) params.set("q", state.query);
  if (state.layer !== "all") params.set("layer", state.layer);
  if (state.gate !== "all") params.set("gate", state.gate);
  if (state.adoption !== "all") params.set("adoption", state.adoption);
  if (state.selectedRole) params.set("role", state.selectedRole);
  const query = params.toString();
  history.replaceState(null, "", query ? `?${query}` : location.pathname);
}

function renderSummary(items) {
  const total = state.capabilities.length;
  const gated = state.capabilities.filter((item) => item.release_gate === "license-review").length;
  const layers = new Set(state.capabilities.map((item) => item.layer)).size;
  els.total.textContent = total;
  els.gated.textContent = gated;
  els.layers.textContent = layers;
  els.visible.textContent = items.length;
  els.standard.textContent = items.filter((item) => item.release_gate === "standard").length;
  els.review.textContent = items.filter((item) => item.release_gate === "license-review").length;
}

function renderLayerFilters() {
  const layers = ["all", ...new Set(state.capabilities.map((item) => item.layer))];
  els.layerFilters.replaceChildren(
    ...layers.map((layer) =>
      element(
        "button",
        {
          type: "button",
          "data-layer": layer,
          "aria-pressed": state.layer === layer ? "true" : "false",
          onclick: () => {
            state.layer = layer;
            render();
          },
        },
        [layer === "all" ? "All" : titleize(layer)],
      ),
    ),
  );
}

function badge(className, value, data = {}) {
  return element("span", { class: className, dataset: data }, [titleize(value)]);
}

function renderCatalog(items) {
  if (!items.length) {
    els.catalog.replaceChildren(element("div", { class: "empty-state" }, ["No matching capabilities."]));
    return;
  }

  els.catalog.replaceChildren(
    ...items.map((capability) =>
      element(
        "article",
        {
          role: "listitem",
          class: "capability-item",
        },
        [
          element(
            "button",
            {
              type: "button",
              class: "capability-card",
              "aria-current": state.selectedRole === capability.role ? "true" : "false",
              "data-role": capability.role,
              onclick: () => {
                state.selectedRole = capability.role;
                render();
              },
            },
            [
              element("div", { class: "card-topline" }, [
                element("strong", { class: "role-name" }, [capability.role]),
              ]),
              element("div", { class: "badge-row" }, [
                badge("layer-badge", capability.layer),
                badge("gate-badge", capability.release_gate, { gate: capability.release_gate }),
                badge("adoption-badge", capability.adoption_status),
              ]),
              element("div", { class: "card-summary" }, [capability.summary]),
            ],
          ),
        ],
      ),
    ),
  );
}

function renderCompare() {
  const selected = [...state.compare]
    .map((role) => state.capabilities.find((item) => item.role === role))
    .filter(Boolean);

  if (!selected.length) {
    els.compare.replaceChildren(element("div", { class: "compare-empty" }, ["No roles pinned."]));
    return;
  }

  els.compare.replaceChildren(
    element("table", { class: "compare-table" }, [
      element("thead", {}, [
        element("tr", {}, [
          element("th", { scope: "col" }, ["Role"]),
          element("th", { scope: "col" }, ["Layer"]),
          element("th", { scope: "col" }, ["Adoption"]),
          element("th", { scope: "col" }, ["Gate"]),
          element("th", { scope: "col" }, [""]),
        ]),
      ]),
      element(
        "tbody",
        {},
        selected.map((capability) =>
          element("tr", { class: "compare-row" }, [
            element("th", { scope: "row" }, [capability.role]),
            element("td", {}, [titleize(capability.layer)]),
            element("td", {}, [titleize(capability.adoption_status)]),
            element("td", {}, [titleize(capability.release_gate)]),
            element("td", {}, [
              element(
                "button",
                {
                  type: "button",
                  class: "ghost-button",
                  "aria-label": `Remove ${capability.role}`,
                  onclick: () => {
                    state.compare.delete(capability.role);
                    persistCompare();
                    render();
                  },
                },
                ["Remove"],
              ),
            ]),
          ]),
        ),
      ),
    ]),
  );
}

async function copyCommand(command, button) {
  try {
    await navigator.clipboard.writeText(command);
    button.textContent = "Copied";
    setTimeout(() => (button.textContent = "Copy"), 1300);
  } catch {
    button.textContent = "Select";
  }
}

function persistCompare() {
  localStorage.setItem(COMPARE_STORAGE_KEY, JSON.stringify([...state.compare]));
}

function renderDetails(items) {
  const visibleSelection = items.find((item) => item.role === state.selectedRole);
  const capability =
    visibleSelection ||
    items[0] ||
    state.capabilities.find((item) => item.role === state.selectedRole) ||
    state.capabilities[0];

  if (!capability) {
    els.details.replaceChildren();
    return;
  }

  state.selectedRole = capability.role;
  const command = `nido ansible show-groundwork ${capability.role} --json`;
  const pinned = state.compare.has(capability.role);
  const copyButton = element(
    "button",
    {
      type: "button",
      class: "detail-action",
      onclick: (event) => copyCommand(command, event.currentTarget),
    },
    ["Copy"],
  );
  const pinButton = element(
    "button",
    {
      type: "button",
      class: "detail-action",
      onclick: () => {
        if (state.compare.has(capability.role)) state.compare.delete(capability.role);
        else state.compare.add(capability.role);
        persistCompare();
        render();
      },
    },
    [pinned ? "Unpin" : "Pin"],
  );

  els.details.replaceChildren(
    element("div", { class: "detail-kicker" }, ["Selected capability"]),
    element("h1", { class: "detail-title" }, [capability.role]),
    element("p", { class: "detail-summary" }, [capability.summary]),
    element("div", { class: "detail-meta" }, [
      element("div", {}, [element("span", {}, ["Layer"]), element("span", {}, [titleize(capability.layer)])]),
      element("div", {}, [
        element("span", {}, ["Adoption"]),
        element("span", {}, [titleize(capability.adoption_status)]),
      ]),
      element("div", {}, [
        element("span", {}, ["Release gate"]),
        element("span", {}, [titleize(capability.release_gate)]),
      ]),
    ]),
    element("div", { class: "detail-actions" }, [pinButton, copyButton]),
    element("div", { class: "command-box" }, [element("code", {}, [command])]),
  );
}

function render() {
  const items = filtered();
  renderLayerFilters();
  renderSummary(items);
  renderCatalog(items);
  renderDetails(items);
  renderCompare();
  setPressed(els.gateButtons, "gate", state.gate);
  setPressed(els.adoptionButtons, "adoption", state.adoption);
  els.search.value = state.query;
  els.sort.value = state.sort;
  syncUrl();
}

function hydrateFromUrl() {
  const params = new URLSearchParams(location.search);
  state.query = params.get("q") || "";
  state.layer = params.get("layer") || "all";
  state.gate = params.get("gate") || "all";
  state.adoption = params.get("adoption") || "all";
  state.selectedRole = params.get("role") || "";
}

async function boot() {
  hydrateFromUrl();
  const response = await fetch("./groundwork.json", { cache: "no-store" });
  if (!response.ok) throw new Error(`catalog fetch failed: ${response.status}`);
  const payload = await response.json();
  state.capabilities = payload.capabilities;
  render();

  if ("serviceWorker" in navigator) {
    navigator.serviceWorker.register("./sw.js").catch(() => {});
  }
}

els.search.addEventListener("input", (event) => {
  state.query = event.currentTarget.value;
  render();
});

for (const button of els.gateButtons) {
  button.addEventListener("click", () => {
    state.gate = button.dataset.gate;
    render();
  });
}

for (const button of els.adoptionButtons) {
  button.addEventListener("click", () => {
    state.adoption = button.dataset.adoption;
    render();
  });
}

els.sort.addEventListener("change", (event) => {
  state.sort = event.currentTarget.value;
  render();
});

els.clear.addEventListener("click", () => {
  state.query = "";
  state.layer = "all";
  state.gate = "all";
  state.adoption = "all";
  render();
});

boot().catch((error) => {
  els.catalog.replaceChildren(
    element("div", { class: "empty-state" }, [`Catalog unavailable: ${error.message}`]),
  );
});