slippy-cli 0.1.0

AI Linter for Rust projects
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Slippy Lint List</title>
  <style>
    :root {
      --bg: #1a1a2e;
      --surface: #16213e;
      --surface-hover: #1a2745;
      --border: #2a3a5c;
      --text: #e0e0e0;
      --text-muted: #8892a4;
      --accent: #e2b714;
      --code-bg: #0f1629;
      --link: #7eb8da;
      --tag-warn-bg: #433a10;
      --tag-warn-text: #e2b714;
    }

    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      background: var(--bg);
      color: var(--text);
      line-height: 1.6;
      min-height: 100vh;
    }

    header {
      border-bottom: 1px solid var(--border);
      padding: 1.5rem 2rem;
      display: flex;
      align-items: center;
      gap: 1rem;
      flex-wrap: wrap;
    }

    header h1 {
      font-size: 1.4rem;
      font-weight: 700;
      color: var(--accent);
      white-space: nowrap;
    }

    header h1 span {
      color: var(--text);
      font-weight: 400;
    }

    .search-box {
      flex: 1;
      min-width: 200px;
      max-width: 420px;
      position: relative;
    }

    .search-box input {
      width: 100%;
      padding: 0.5rem 0.75rem 0.5rem 2.2rem;
      border-radius: 6px;
      border: 1px solid var(--border);
      background: var(--surface);
      color: var(--text);
      font-size: 0.9rem;
      outline: none;
      transition: border-color 0.15s;
    }

    .search-box input:focus {
      border-color: var(--accent);
    }

    .search-box::before {
      content: "\1F50D";
      position: absolute;
      left: 0.65rem;
      top: 50%;
      transform: translateY(-50%);
      font-size: 0.85rem;
      opacity: 0.5;
    }

    .count {
      color: var(--text-muted);
      font-size: 0.85rem;
      margin-left: auto;
      white-space: nowrap;
    }

    main {
      padding: 1.5rem 2rem 3rem;
      max-width: 960px;
      margin: 0 auto;
    }

    .lint-card {
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: 8px;
      margin-bottom: 1rem;
      transition: border-color 0.15s;
    }

    .lint-card:target {
      border-color: var(--accent);
    }

    .lint-header {
      display: flex;
      align-items: center;
      gap: 0.75rem;
      padding: 0.85rem 1.15rem;
      cursor: pointer;
      user-select: none;
      flex-wrap: wrap;
    }

    .lint-header:hover {
      background: var(--surface-hover);
      border-radius: 8px;
    }

    .lint-name {
      font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
      font-size: 0.95rem;
      font-weight: 600;
      color: var(--link);
      text-decoration: none;
    }

    .lint-name:hover {
      text-decoration: underline;
    }

    .tag {
      font-size: 0.7rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.04em;
      padding: 0.15em 0.55em;
      border-radius: 4px;
      background: var(--tag-warn-bg);
      color: var(--tag-warn-text);
    }

    .lint-desc {
      color: var(--text-muted);
      font-size: 0.85rem;
      flex: 1;
      min-width: 200px;
    }

    .chevron {
      margin-left: auto;
      color: var(--text-muted);
      font-size: 0.8rem;
      transition: transform 0.2s;
      flex-shrink: 0;
    }

    .lint-card.open .chevron {
      transform: rotate(90deg);
    }

    .lint-body {
      display: none;
      padding: 0 1.15rem 1.15rem;
      border-top: 1px solid var(--border);
    }

    .lint-card.open .lint-body {
      display: block;
    }

    .lint-body h3 {
      font-size: 0.95rem;
      font-weight: 600;
      margin: 1rem 0 0.4rem;
      color: var(--text);
    }

    .lint-body h3:first-child {
      margin-top: 0.75rem;
    }

    .lint-body p,
    .lint-body li {
      font-size: 0.88rem;
      color: var(--text-muted);
      margin-bottom: 0.4rem;
    }

    .lint-body ul {
      padding-left: 1.3rem;
    }

    .lint-body pre {
      background: var(--code-bg);
      border: 1px solid var(--border);
      border-radius: 6px;
      padding: 0.85rem 1rem;
      overflow-x: auto;
      margin: 0.5rem 0;
      font-size: 0.82rem;
      line-height: 1.55;
    }

    .lint-body code {
      font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
      font-size: 0.82rem;
    }

    .lint-body p code,
    .lint-body li code {
      background: var(--code-bg);
      padding: 0.15em 0.4em;
      border-radius: 3px;
      font-size: 0.82rem;
    }

    .lint-body .explain-link {
      display: inline-block;
      margin-top: 0.5rem;
      font-size: 0.82rem;
      color: var(--text-muted);
      font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
    }

    .no-results {
      text-align: center;
      color: var(--text-muted);
      padding: 3rem 0;
      display: none;
      font-size: 0.95rem;
    }

    .loading {
      text-align: center;
      color: var(--text-muted);
      padding: 3rem 0;
      font-size: 0.95rem;
    }

    @media (max-width: 640px) {
      header {
        padding: 1rem;
      }

      main {
        padding: 1rem;
      }

      .search-box {
        max-width: none;
      }
    }
  </style>
</head>

<body>
  <header>
    <h1>slippy <span>lint list</span></h1>
    <div class="search-box">
      <input type="text" id="search" placeholder="Filter lints…" autocomplete="off" autofocus>
    </div>
    <span class="count" id="count"></span>
  </header>

  <main id="lints">
    <div class="loading">Loading lints…</div>
  </main>
  <div class="no-results" id="no-results">No lints match your search.</div>

  <script src="marked.min.js"></script>
  <script>
    let LINTS = [];

    function escapeHtml(s) {
      return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
    }

    function render() {
      const query = document.getElementById("search").value.toLowerCase().replace(/::/g, "_").replace(/-/g, "_");
      const container = document.getElementById("lints");
      const noResults = document.getElementById("no-results");
      container.innerHTML = "";
      let shown = 0;

      for (const lint of LINTS) {
        const matchesId = lint.id.includes(query);
        const matchesDesc = lint.description.toLowerCase().includes(query);
        if (!matchesId && !matchesDesc) continue;
        shown++;

        const card = document.createElement("div");
        card.className = "lint-card";
        card.id = lint.id;

        if (location.hash === "#" + lint.id) card.classList.add("open");

        card.innerHTML = `
      <div class="lint-header" role="button" tabindex="0">
        <a class="lint-name" href="#${lint.id}">slippy::${lint.id}</a>
        <span class="tag">${lint.level}</span>
        <span class="lint-desc">${escapeHtml(lint.description)}</span>
        <span class="chevron">&#9654;</span>
      </div>
      <div class="lint-body">
        ${marked.parse(lint.explanation)}
        <span class="explain-link">cargo slippy --explain ${lint.id}</span>
      </div>
    `;

        const header = card.querySelector(".lint-header");
        header.addEventListener("click", (e) => {
          if (e.target.classList.contains("lint-name")) return;
          card.classList.toggle("open");
        });
        header.addEventListener("keydown", (e) => {
          if (e.key === "Enter" || e.key === " ") {
            e.preventDefault();
            card.classList.toggle("open");
          }
        });

        container.appendChild(card);
      }

      document.getElementById("count").textContent = shown + " lint" + (shown !== 1 ? "s" : "");
      noResults.style.display = shown === 0 ? "block" : "none";
    }

    fetch("lints.json")
      .then(r => r.json())
      .then(data => {
        LINTS = data;
        render();
        document.getElementById("search").addEventListener("input", render);
        window.addEventListener("hashchange", render);
        if (location.hash) {
          const el = document.getElementById(location.hash.slice(1));
          if (el) { el.classList.add("open"); el.scrollIntoView({ behavior: "smooth" }); }
        }
      })
      .catch(() => {
        document.getElementById("lints").innerHTML =
          '<div class="loading">Failed to load lint data.</div>';
      });
  </script>
</body>

</html>