$sloprun.dev

Version Range Tester

demo sandbox: allow-scripts · csp: default-src 'none'

sandboxed and isolated in your browser · never enter a real password in a demo

▶ 7 ran · ✓ 0 worked
i ran it — no login needed:
share: preview embed ↗
post it anywhere:
email the card:

0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously

That one line in your package.json — ^1.2.3, ~1.2.3, >=1.2 <2 — is really a filter, and almost nobody can say exactly what it lets through. Type a range here and the demo rewrites it as the plain comparators npm actually uses, then runs every candidate past it live — sorted the way an installer sorts, so 1.2.10 outranks 1.2.3 and beta.11 outranks beta.2 — naming the bound that accepted each version or kept it out. It opens on ^1.2.3 with 1.3.0-rc.1 pinned, which is the case that bites people: the release candidate clears both bounds and still gets filtered out, because a range skips pre-releases unless it names one itself. Try ^0.2.3 to watch the caret change its mind below 1.0.0, flip the pre-releases switch to see the floors pick up npm's hidden -0, then add a version of your own at the bottom and see where it lands.

Embed this demo — put a runnable demo in your blog post

Paste this where you write. It renders a live, runnable demo inline anywhere raw HTML / iframes are allowed — Ghost, WordPress, Notion, Discourse, your own site.

<iframe src="https://sloprun.dev/embed/semver-explainer" width="100%" height="640" loading="lazy" allow="" style="border:1px solid #E3E2DC;border-radius:10px" title="Version Range Tester — a runnable demo on sloprun.dev"></iframe>
preview ↗

On Medium and dev.to the plain link becomes a rich preview card that links back here — they don't run third-party iframes, so paste the URL there and the reader clicks through to run it. https://sloprun.dev/p/semver-explainer

Source

the code you see is the code that just ran raw ↗
Source — the code you see is the code that just ran 1263 lines 54.3 KB index.html
demos/semver-explainer/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Version Range Tester — sloprun.dev</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
   Identity: instrument-panel. Machine facts in mono; human words in sans.
   Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
  --bg: #F6F6F3;          /* neutral paper, not cream */
  --surface: #FFFFFF;
  --ink: #1A1D21;
  --muted: #5A6068;
  --line: #E3E2DC;
  --accent: #C05600;      /* phosphor amber, darkened for light bg */
  --accent-soft: #FFF3E6;
  --run: #1A7F37;         /* earned green */
  --run-soft: #E7F4EA;
  --danger: #C93C37;
  --radius: 10px;
  --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
    --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
    --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
  }
}
:root[data-theme="dark"] {
  --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
  --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
  --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }

/* derived tints (kept out of the verbatim token block, themed the same three ways) */
:root { --danger-soft: rgba(201, 60, 55, .14); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --danger-soft: rgba(244, 112, 103, .18); }
}
:root[data-theme="dark"] { --danger-soft: rgba(244, 112, 103, .18); }

/* ---- shell ---- */
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
body { font-size: 16px; line-height: 1.55; -webkit-text-size-adjust: 100%; overflow-x: hidden; }
.wrap { max-width: 1060px; margin: 0 auto; padding: 26px 18px 36px; }
h1 { font-size: 1.5rem; line-height: 1.2; margin: 0 0 7px; letter-spacing: -.015em; }
.lede { margin: 0; color: var(--muted); font-size: .95rem; max-width: 68ch; }
.lede b { color: var(--ink); font-weight: 600; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
code, .mono { font-family: var(--font-mono); }

.k-lab {
  font-family: var(--font-mono); font-size: .66rem; letter-spacing: .11em;
  text-transform: uppercase; color: var(--muted);
}
.btn {
  font-family: var(--font-mono); font-size: .73rem; letter-spacing: .02em;
  background: var(--bg); color: var(--ink); border: 1px solid var(--line);
  border-radius: 999px; padding: 7px 12px; min-height: 32px; cursor: pointer;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.btn.ghost { background: transparent; color: var(--muted); }
.btn.ghost:hover { color: var(--accent); }

.panel {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px; margin-top: 18px;
}

/* ---- range input ---- */
.rowlab { display: flex; align-items: baseline; gap: 10px; margin-bottom: 7px; flex-wrap: wrap; }
.lamp {
  font-family: var(--font-mono); font-size: .66rem; letter-spacing: .11em;
  text-transform: uppercase; color: var(--muted); display: inline-flex; align-items: center; gap: 6px;
}
.lamp::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: var(--muted); flex: none;
}
.lamp[data-state="ok"] { color: var(--muted); }
.lamp[data-state="ok"]::before { background: var(--accent); }
.lamp[data-state="bad"] { color: var(--danger); }
.lamp[data-state="bad"]::before { background: var(--danger); }

.rangebox {
  display: flex; align-items: stretch; gap: 0;
  border: 1px solid var(--line); border-radius: 9px; background: var(--bg); overflow: hidden;
}
.rangebox:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.rangebox .sig {
  font-family: var(--font-mono); font-size: .9rem; color: var(--muted);
  padding: 12px 0 12px 13px; user-select: none; flex: none;
}
#range {
  flex: 1 1 auto; min-width: 0; border: 0; background: transparent; color: var(--ink);
  font-family: var(--font-mono); font-size: 1rem; letter-spacing: 0;
  padding: 12px 12px 12px 8px;
}
#range:focus { outline: none; }
#range::placeholder { color: var(--muted); opacity: 1; }
.rangebox.bad { border-color: var(--danger); }
.rangebox.bad:focus-within { box-shadow: 0 0 0 3px var(--danger-soft); }

.err { margin: 8px 0 0; color: var(--danger); font-size: .85rem; }
.err code { background: transparent; }

.presets { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.chip {
  font-family: var(--font-mono); font-size: .72rem; letter-spacing: 0;
  border: 1px solid var(--line); border-radius: 999px; padding: 6px 11px; min-height: 30px;
  background: var(--bg); color: var(--ink); cursor: pointer;
  display: inline-flex; align-items: center;
}
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.chip[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }

/* ---- expansion strip ---- */
.expand { margin-top: 14px; display: flex; flex-direction: column; gap: 7px; }
.clause {
  display: flex; flex-wrap: wrap; align-items: center; gap: 7px;
  background: var(--bg); border: 1px solid var(--line); border-radius: 9px; padding: 9px 11px;
}
.clause .cno { font-family: var(--font-mono); font-size: .66rem; letter-spacing: .11em; color: var(--muted); flex: none; }
.cmp {
  font-family: var(--font-mono); font-size: .78rem;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-bottom-width: 2px; border-bottom-color: var(--accent);
  border-radius: 7px; padding: 5px 9px; min-height: 30px; cursor: help;
  overflow-wrap: anywhere; text-align: left;
}
.cmp:hover, .cmp[data-on="1"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.cmp:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.orbar { display: flex; align-items: center; gap: 9px; }
.orbar .k-lab { flex: none; }
.orbar i { flex: 1 1 auto; height: 1px; background: var(--line); display: block; }
.cmp .z { opacity: .5; }
.nob {
  font-family: var(--font-mono); font-size: .78rem; color: var(--muted);
  border: 1px dashed var(--line); border-radius: 7px; padding: 5px 9px; min-height: 30px;
  display: inline-flex; align-items: center;
}
.zeronote { margin: 9px 0 0; font-size: .84rem; color: var(--muted); }
.zeronote code { color: var(--ink); }
.plain { margin: 11px 0 0; font-size: .92rem; }
.plain b { color: var(--accent); font-weight: 600; }

/* ---- stats ---- */
.stats {
  display: flex; flex-wrap: wrap; gap: 10px 22px; align-items: flex-end;
  margin-top: 18px; padding: 12px 14px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
}
.stat { display: flex; flex-direction: column; gap: 3px; }
.stat .val { font-family: var(--font-mono); font-size: 1.02rem; line-height: 1.2; }
.stat .val.run { color: var(--run); }
.stat .val.acc { color: var(--accent); }
.stat .val.off { color: var(--muted); }
.stats .spacer { flex: 1 1 auto; }

/* ---- main split ---- */
.main { display: flex; flex-direction: column; gap: 16px; margin-top: 16px; }
.why { order: -1; }
@media (min-width: 900px) {
  .main { display: grid; grid-template-columns: minmax(0,1fr) 330px; align-items: start; }
  .why { order: 0; position: sticky; top: 16px; }
}

/* ---- why panel ---- */
.why {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px;
}
.whyhead { display: flex; align-items: center; gap: 8px; justify-content: space-between; margin-bottom: 9px; }
.wv { font-family: var(--font-mono); font-size: 1.02rem; overflow-wrap: anywhere; margin: 0 0 2px; }
.verdict {
  font-family: var(--font-mono); font-size: .66rem; letter-spacing: .11em; text-transform: uppercase;
  border: 1px solid var(--line); border-radius: 999px; padding: 3px 9px; color: var(--muted); flex: none;
}
.verdict[data-v="yes"] { color: var(--run); border-color: var(--run); background: var(--run-soft); }
.checks { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; gap: 0; }
.checks li {
  display: flex; align-items: baseline; gap: 9px; padding: 6px 0;
  border-top: 1px dashed var(--line); font-family: var(--font-mono); font-size: .78rem;
}
.checks li:first-child { border-top: 0; }
.checks .cw { flex: 1 1 auto; overflow-wrap: anywhere; color: var(--ink); }
.checks .cs { flex: none; letter-spacing: .08em; font-size: .68rem; text-transform: uppercase; color: var(--muted); }
.checks li[data-p="1"] .cs { color: var(--run); }
.checks li[data-p="rule"] .cw { color: var(--muted); }
.say { margin: 11px 0 0; font-size: .89rem; color: var(--ink); }
.say .m { font-family: var(--font-mono); font-size: .82rem; }
.hint { color: var(--muted); font-size: .89rem; margin: 0; }
.neigh {
  margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--line);
  font-family: var(--font-mono); font-size: .72rem; color: var(--muted); overflow-wrap: anywhere;
}
.neigh b { color: var(--ink); font-weight: 400; }
.pintag { font-family: var(--font-mono); font-size: .62rem; letter-spacing: .1em; text-transform: uppercase; color: var(--accent); }

/* ---- grid ---- */
.gridpanel {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px;
}
.gridhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; margin-bottom: 4px; }
.gridhead h2 { font-size: .95rem; margin: 0; }
.gridhead .k-lab { margin-left: auto; }
.grp { margin-top: 13px; }
.grphead { display: flex; align-items: baseline; gap: 9px; margin-bottom: 6px; }
.grphead .gname { font-family: var(--font-mono); font-size: .72rem; color: var(--ink); letter-spacing: .04em; }
.grphead i { flex: 1 1 auto; height: 1px; background: var(--line); display: block; }
.grphead .gcount { font-family: var(--font-mono); font-size: .66rem; color: var(--muted); flex: none; }
.cells { display: grid; grid-template-columns: repeat(auto-fill, minmax(108px, 1fr)); gap: 7px; }
.cell {
  position: relative; text-align: left; cursor: pointer;
  background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
  padding: 8px 9px 9px; min-height: 46px; color: var(--muted);
  font-family: var(--font-mono); font-size: .74rem; line-height: 1.3;
  overflow-wrap: anywhere; display: flex; align-items: flex-start; gap: 6px;
}
.cell .num { flex: 1 1 auto; }
.cell .mk { flex: none; font-size: .8rem; line-height: 1.2; color: var(--muted); opacity: .5; }
.cell[data-m="1"] { background: var(--run-soft); border-color: var(--run); color: var(--ink); }
.cell[data-m="1"] .mk { color: var(--run); opacity: 1; }
.cell[data-m="?"] { opacity: .45; }
.cell:hover { border-color: var(--accent); }
.cell:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cell[data-pin="1"] { box-shadow: inset 0 0 0 2px var(--accent); }
.cell .pick {
  display: block; margin-top: 3px; font-size: .6rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent);
}
.legend { display: flex; flex-wrap: wrap; gap: 8px 16px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.legend span { font-family: var(--font-mono); font-size: .66rem; color: var(--muted); display: inline-flex; align-items: center; gap: 6px; }
.legend i { width: 11px; height: 11px; border-radius: 3px; border: 1px solid var(--line); background: var(--bg); display: block; }
.legend i.on { background: var(--run-soft); border-color: var(--run); }
.note { margin: 10px 0 0; font-size: .85rem; color: var(--muted); }

/* ---- add row ---- */
.addrow { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 14px; align-items: center; }
#addv {
  flex: 1 1 150px; min-width: 0; font-family: var(--font-mono); font-size: 16px;
  background: var(--bg); color: var(--ink); border: 1px solid var(--line);
  border-radius: 8px; padding: 8px 10px; min-height: 34px;
}
#addv:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
#addv::placeholder { color: var(--muted); opacity: 1; }
.addmsg { font-family: var(--font-mono); font-size: .7rem; color: var(--muted); flex-basis: 100%; }
.addmsg[data-s="bad"] { color: var(--danger); }
.addmsg[data-s="ok"] { color: var(--run); }

footer { margin-top: 24px; font-family: var(--font-mono); font-size: .7rem; color: var(--muted); }

@media (prefers-reduced-motion: no-preference) {
  .btn, .chip, .cmp, .cell, .verdict, .checks li { transition: background-color .13s ease, border-color .13s ease, color .13s ease, box-shadow .13s ease, opacity .13s ease; }
  @keyframes flash { 0% { box-shadow: 0 0 0 3px var(--accent-soft); border-color: var(--accent); } 100% { box-shadow: 0 0 0 0 transparent; } }
  .cell.fresh { animation: flash .9s ease-out 1; }
}
@media (max-width: 400px) {
  .wrap { padding: 20px 13px 30px; }
  .cells { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
}
</style>
</head>
<body>
<main class="wrap">
  <h1>Version Range Tester</h1>
  <p class="lede">The line you write in <b>package.json</b> is a filter. Type one below and every candidate version
    says whether it gets through — and <b>which part of the range</b> let it in or kept it out.</p>

  <section class="panel" aria-labelledby="rl">
    <div class="rowlab">
      <span class="k-lab" id="rl">version range</span>
      <span class="lamp" id="lamp" data-state="ok">parsed</span>
    </div>
    <div class="rangebox" id="rangebox">
      <span class="sig" aria-hidden="true">"pkg":</span>
      <input id="range" type="text" value="^1.2.3" spellcheck="false" autocomplete="off"
             autocapitalize="off" aria-describedby="rl expand-desc" placeholder="e.g. ^1.2.3">
    </div>
    <p class="err" id="err" role="status" hidden></p>

    <div class="presets" id="presets" role="group" aria-label="Example ranges"></div>

    <div class="expand" id="expand" aria-describedby="expand-desc"></div>
    <p class="zeronote" id="zeronote" hidden></p>
    <p class="plain" id="plain"></p>
    <p class="sr-only" id="expand-desc">The range is rewritten as plain comparators. Point at any comparator for an explanation.</p>
  </section>

  <div class="stats">
    <div class="stat">
      <span class="k-lab">versions accepted</span>
      <span class="val run" id="s-count" aria-live="polite">0 / 0</span>
    </div>
    <div class="stat">
      <span class="k-lab">an installer picks</span>
      <span class="val acc" id="s-pick">—</span>
    </div>
    <div class="stat">
      <span class="k-lab">pre-releases</span>
      <button type="button" class="btn" id="pretog" aria-pressed="false"><span class="sr-only">pre-releases </span><span id="pretogtxt">excluded</span></button>
    </div>
    <div class="spacer"></div>
    <div class="stat">
      <button type="button" class="btn ghost" id="reset">reset</button>
    </div>
  </div>

  <div class="main">
    <section class="gridpanel" aria-labelledby="gh">
      <div class="gridhead">
        <h2 id="gh">Candidate versions</h2>
        <span class="k-lab">precedence order · oldest first</span>
      </div>
      <div id="groups"></div>
      <div class="legend">
        <span><i class="on" aria-hidden="true"></i> accepted by the range</span>
        <span><i aria-hidden="true"></i> filtered out</span>
        <span>arrow keys move · click to pin</span>
      </div>
      <p class="note">Sorted the way an installer sorts: numbers compare as numbers, so
        <code>1.2.10</code> is newer than <code>1.2.3</code> and <code>beta.11</code> is newer than
        <code>beta.2</code>. A pre-release always ranks below the release it leads to.</p>
      <div class="addrow">
        <label class="k-lab" for="addv">add a version</label>
        <input id="addv" type="text" placeholder="1.4.0-rc.2" spellcheck="false" autocomplete="off" autocapitalize="off">
        <button type="button" class="btn" id="addbtn">add</button>
        <span class="addmsg" id="addmsg" role="status"></span>
      </div>
    </section>

    <section class="why" id="why" aria-label="Explanation"></section>
  </div>

  <footer>demo · sloprun.dev</footer>
</main>

<script>
(function () {
  "use strict";
  var $ = function (id) { return document.getElementById(id); };
  function esc(s) {
    return String(s).replace(/[&<>"']/g, function (c) {
      return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
    });
  }

/* ===== semver core (start) ===== */
  var FULLRE = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
  var PARTRE = /^v?(\d+|[xX*])(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;

  /* a full, valid version — nothing less is accepted as a candidate */
  function parseVersion(s) {
    var m = FULLRE.exec(String(s).trim());
    if (!m) return null;
    return {
      major: +m[1], minor: +m[2], patch: +m[3],
      pre: m[4] ? m[4].split(".") : [],
      build: m[5] || ""
    };
  }

  function isNum(id) { return /^[0-9]+$/.test(id); }

  /* one pre-release identifier against another: numbers numerically, text as text,
     and a number always ranks below text */
  function cmpIdent(a, b) {
    var na = isNum(a), nb = isNum(b);
    if (na && nb) { var x = +a, y = +b; return x < y ? -1 : x > y ? 1 : 0; }
    if (na) return -1;
    if (nb) return 1;
    return a < b ? -1 : a > b ? 1 : 0;
  }

  function cmpPre(a, b) {
    if (!a.length && !b.length) return 0;
    if (!a.length) return 1;   /* a release outranks any pre-release of itself */
    if (!b.length) return -1;
    var n = Math.min(a.length, b.length);
    for (var i = 0; i < n; i++) {
      var c = cmpIdent(a[i], b[i]);
      if (c) return c;
    }
    return a.length < b.length ? -1 : a.length > b.length ? 1 : 0;  /* fewer fields = lower */
  }

  /* build metadata is ignored for precedence, per spec */
  function compare(a, b) {
    if (a.major !== b.major) return a.major < b.major ? -1 : 1;
    if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
    if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
    return cmpPre(a.pre, b.pre);
  }

  function fmtV(v) {
    return v.major + "." + v.minor + "." + v.patch + (v.pre.length ? "-" + v.pre.join(".") : "") +
      (v.build ? "+" + v.build : "");
  }

  function isXtok(t) { return t === undefined || t === "" || t === "x" || t === "X" || t === "*"; }

  /* a possibly-partial version used inside a range: 1.2.x, 1.2, 1, * */
  function parsePartial(s) {
    if (s === "" || s === "*" || s === "x" || s === "X") return { major: null, minor: null, patch: null, pre: [] };
    var m = PARTRE.exec(s);
    if (!m) return null;
    /* an x swallows everything to its right; a number after one is a typo, not a range */
    if (isXtok(m[1]) && !isXtok(m[2])) return null;
    if (isXtok(m[2]) && !isXtok(m[3])) return null;
    function num(t) { return isXtok(t) ? null : +t; }
    var maj = num(m[1]), min = num(m[2]), pat = num(m[3]);
    if (maj === null) return { major: null, minor: null, patch: null, pre: [] };
    var pre = m[4] ? m[4].split(".") : [];
    for (var i = 0; i < pre.length; i++) if (!/^[0-9A-Za-z-]+$/.test(pre[i])) return null;
    if (pre.length && (min === null || pat === null)) return null;
    return { major: maj, minor: min, patch: pat, pre: pre };
  }

  function V(ma, mi, pa, pre) { return { major: ma, minor: mi, patch: pa, pre: pre || [], build: "" }; }

  /* A comparator. `synth` marks a "-0" this tool added rather than one the author typed,
     so it can be rendered quietly and explained. */
  function C(op, v, synth) {
    var full = op + fmtV(v);
    var zero = !!synth && v.pre.length === 1 && v.pre[0] === "0";
    return { op: op, ver: v, text: full, head: zero ? full.slice(0, -2) : full, tail: zero ? "-0" : "" };
  }
  /* the lowest possible pre-release tag: the floor of a version, below -alpha, below -0.0 */
  function floorTag(inc) { return inc ? ["0"] : []; }

  /* one whitespace-free piece of a range -> a list of plain comparators.
     Ceilings always carry -0 so a pre-release of the ceiling stays outside; floors carry
     one only when pre-releases are switched on. This is exactly what npm does. */
  function expandSimple(tok, inc) {
    var m = /^(\^|~>|~|>=|<=|>|<|=)?(.*)$/.exec(tok);
    var op = m[1] || "", rest = m[2];
    if (op === "~>") op = "~";
    if (op && rest === "") return null;   /* an operator with nothing after it is not a range */
    var p = parsePartial(rest);
    if (!p) return null;
    var Z = floorTag(inc), z0 = ["0"];
    var any = p.major === null;
    var noMinor = !any && p.minor === null;
    var noPatch = !any && !noMinor && p.patch === null;
    var exact = V(p.major, p.minor, p.patch, p.pre);
    /* the same version as a floor: a written pre-release wins, otherwise the tag rides along */
    var zsyn = !p.pre.length, zex = zsyn ? V(p.major, p.minor, p.patch, Z) : exact;
    var ANY = [C(">=", V(0, 0, 0, Z), true)];
    var NONE = [C("<", V(0, 0, 0, z0), true)];
    var kind, cmps;

    if (op === "^") {
      kind = "caret";
      if (any) cmps = ANY;
      else if (noMinor) cmps = [C(">=", V(p.major, 0, 0, Z), true), C("<", V(p.major + 1, 0, 0, z0), true)];
      else if (noPatch) cmps = p.major > 0
        ? [C(">=", V(p.major, p.minor, 0, Z), true), C("<", V(p.major + 1, 0, 0, z0), true)]
        : [C(">=", V(0, p.minor, 0, Z), true), C("<", V(0, p.minor + 1, 0, z0), true)];
      else if (p.major > 0) cmps = [C(">=", exact), C("<", V(p.major + 1, 0, 0, z0), true)];
      /* below 1.0.0 the caret floor picks up the tag as well, exactly where npm puts it */
      else if (p.minor > 0) cmps = [C(">=", zex, zsyn), C("<", V(0, p.minor + 1, 0, z0), true)];
      else cmps = [C(">=", zex, zsyn), C("<", V(0, 0, p.patch + 1, z0), true)];
    } else if (op === "~") {
      /* npm never lowers a tilde floor onto -0, even with pre-releases included */
      kind = "tilde";
      if (any) cmps = ANY;
      else if (noMinor) cmps = [C(">=", V(p.major, 0, 0, []), true), C("<", V(p.major + 1, 0, 0, z0), true)];
      else if (noPatch) cmps = [C(">=", V(p.major, p.minor, 0, []), true), C("<", V(p.major, p.minor + 1, 0, z0), true)];
      else cmps = [C(">=", exact), C("<", V(p.major, p.minor + 1, 0, z0), true)];
    } else if (op === "" || op === "=") {
      kind = any ? "any" : (noMinor || noPatch) ? "xrange" : "exact";
      if (any) cmps = ANY;
      else if (noMinor) cmps = [C(">=", V(p.major, 0, 0, Z), true), C("<", V(p.major + 1, 0, 0, z0), true)];
      else if (noPatch) cmps = [C(">=", V(p.major, p.minor, 0, Z), true), C("<", V(p.major, p.minor + 1, 0, z0), true)];
      else cmps = [C("=", exact)];
    } else if (op === ">") {
      kind = "op";
      if (any) cmps = NONE;                                    /* nothing is greater than everything */
      else if (noMinor) cmps = [C(">=", V(p.major + 1, 0, 0, Z), true)];
      else if (noPatch) cmps = [C(">=", V(p.major, p.minor + 1, 0, Z), true)];
      else cmps = [C(">", exact)];
    } else if (op === ">=") {
      kind = "op";
      if (any) cmps = ANY;
      else if (noMinor) cmps = [C(">=", V(p.major, 0, 0, Z), true)];
      else if (noPatch) cmps = [C(">=", V(p.major, p.minor, 0, Z), true)];
      else cmps = [C(">=", exact)];
    } else if (op === "<") {
      kind = "op";
      if (any) cmps = NONE;
      else if (noMinor) cmps = [C("<", V(p.major, 0, 0, z0), true)];
      else if (noPatch) cmps = [C("<", V(p.major, p.minor, 0, z0), true)];
      else cmps = [C("<", exact)];
    } else { /* <= */
      kind = "op";
      if (any) cmps = ANY;
      else if (noMinor) cmps = [C("<", V(p.major + 1, 0, 0, z0), true)];
      else if (noPatch) cmps = [C("<", V(p.major, p.minor + 1, 0, z0), true)];
      else cmps = [C("<=", exact)];
    }
    return { src: tok, kind: kind, op: op, part: p, cmps: cmps };
  }

  function expandHyphen(loTok, hiTok, inc) {
    var lo = parsePartial(loTok), hi = parsePartial(hiTok);
    if (!lo || !hi) return null;
    var Z = floorTag(inc), z0 = ["0"], cmps = [];
    if (lo.major === null) { /* open lower end */ }
    else if (lo.minor === null) cmps.push(C(">=", V(lo.major, 0, 0, Z), true));
    else if (lo.patch === null) cmps.push(C(">=", V(lo.major, lo.minor, 0, Z), true));
    else if (lo.pre.length) cmps.push(C(">=", V(lo.major, lo.minor, lo.patch, lo.pre)));
    else cmps.push(C(">=", V(lo.major, lo.minor, lo.patch, Z), true));
    if (hi.major === null) { /* open upper end */ }
    else if (hi.minor === null) cmps.push(C("<", V(hi.major + 1, 0, 0, z0), true));
    else if (hi.patch === null) cmps.push(C("<", V(hi.major, hi.minor + 1, 0, z0), true));
    else if (hi.pre.length) cmps.push(C("<=", V(hi.major, hi.minor, hi.patch, hi.pre)));
    else if (inc) cmps.push(C("<", V(hi.major, hi.minor, hi.patch + 1, z0), true));
    else cmps.push(C("<=", V(hi.major, hi.minor, hi.patch, [])));
    if (!cmps.length) cmps.push(C(">=", V(0, 0, 0, Z), true));
    return { src: loTok + " - " + hiTok, kind: "hyphen", op: "-", part: lo, cmps: cmps };
  }

  /* full range string -> { ok, clauses:[{tokens, cmps}] } or { ok:false, error, token } */
  function parseRange(input, inc) {
    var src = String(input).trim();
    var parts = src.split("||");
    var clauses = [];
    for (var ci = 0; ci < parts.length; ci++) {
      var s = parts[ci].trim().replace(/\s+/g, " ").replace(/([<>]=?|~>?|\^|=)\s+/g, "$1");
      var tokens = [];
      if (s === "") {
        tokens.push(expandSimple("*", inc));
      } else {
        var raw = s.split(" ");
        for (var i = 0; i < raw.length; i++) {
          var t = raw[i];
          if (t === "-") {
            if (!tokens.length || i + 1 >= raw.length) {
              return { ok: false, error: "A dash range needs a version on each side, like 1.2.3 - 2.0.0.", token: "-" };
            }
            var prev = tokens.pop();
            var h = expandHyphen(prev.src, raw[i + 1], inc);
            if (!h) return { ok: false, error: "Could not read the dash range.", token: prev.src + " - " + raw[i + 1] };
            tokens.push(h);
            i++;
            continue;
          }
          var e = expandSimple(t, inc);
          if (!e) return { ok: false, error: explainBadToken(t), token: t };
          tokens.push(e);
        }
      }
      var flat = [];
      for (var k = 0; k < tokens.length; k++) {
        tokens[k].cmps = tokens[k].cmps.filter(function (c) { return !isFloor(c, inc); });
        flat = flat.concat(tokens[k].cmps);
      }
      clauses.push({ tokens: tokens, cmps: flat });
    }
    /* a clause with nothing left to check takes everything, and npm lets it swallow
       the alternatives beside it */
    for (var q = 0; q < clauses.length; q++) {
      if (!clauses[q].cmps.length) { clauses = [clauses[q]]; break; }
    }
    return { ok: true, src: src, clauses: clauses };
  }

  /* ">=0.0.0" rules nothing out, so npm deletes it outright — which quietly matters,
     because a bound that is gone cannot be the one that lets a pre-release through */
  function isFloor(c, inc) {
    if (c.op !== ">=" || c.ver.major || c.ver.minor || c.ver.patch) return false;
    return inc ? (c.ver.pre.length === 1 && c.ver.pre[0] === "0") : !c.ver.pre.length;
  }

  function explainBadToken(t) {
    var body = t.replace(/^(\^|~>|~|>=|<=|>|<|=)/, "");
    if (/[xX*]\.\d/.test(body)) return "“" + t + "” puts a number after an x. An x already means “anything here”, so nothing can follow it.";
    if (/^\d+(\.\d+){3,}/.test(body)) return "“" + t + "” has too many numbers — a version is exactly three: major.minor.patch.";
    if (/,/.test(t)) return "Commas are not separators here. Use a space for “and”, or || for “or”.";
    if (/^[<>=~^]+$/.test(t)) return "“" + t + "” is an operator with nothing after it.";
    if (/^\d+\.\d+\.\d+-$/.test(body)) return "“" + t + "” ends in a dash with no pre-release name after it.";
    return "“" + t + "” is not a version or a range operator.";
  }

  function cmpTest(v, c) {
    var r = compare(v, c.ver);
    switch (c.op) {
      case "=": return r === 0;
      case ">": return r > 0;
      case ">=": return r >= 0;
      case "<": return r < 0;
      case "<=": return r <= 0;
    }
    return false;
  }

  /* the pre-release rule: a pre-release only slips through a comparator set when some
     comparator in that same set names a pre-release on the identical x.y.z */
  function preAllowed(cmps, v) {
    for (var i = 0; i < cmps.length; i++) {
      var cv = cmps[i].ver;
      if (cv.pre.length && cv.major === v.major && cv.minor === v.minor && cv.patch === v.patch) return true;
    }
    return false;
  }

  function testClause(clause, v, includePre) {
    var lines = [], passes = 0;
    for (var i = 0; i < clause.cmps.length; i++) {
      var ok = cmpTest(v, clause.cmps[i]);
      if (ok) passes++;
      lines.push({ c: clause.cmps[i], pass: ok });
    }
    var all = passes === clause.cmps.length;
    var blocked = false;
    if (all && v.pre.length && !includePre && !preAllowed(clause.cmps, v)) blocked = true;
    return { match: all && !blocked, lines: lines, passes: passes, preBlocked: blocked };
  }

  function testVersion(v, range, includePre) {
    var best = null;
    for (var i = 0; i < range.clauses.length; i++) {
      var r = testClause(range.clauses[i], v, includePre);
      r.clause = i;
      if (r.match) return r;
      if (!best || r.passes > best.passes || (r.preBlocked && !best.preBlocked)) best = r;
    }
    return best;
  }
/* ===== semver core (end) ===== */

  /* ---- plain-language notes ---- */
  /* the human-readable version behind a comparator: the quiet -0 is a mechanism, not a number */
  function baseV(c) { return c.tail ? c.ver.major + "." + c.ver.minor + "." + c.ver.patch : fmtV(c.ver); }

  function boundsPhrase(cmps) {
    var lo = null, hi = null, eq = null;
    for (var i = 0; i < cmps.length; i++) {
      var c = cmps[i];
      if (c.op === "=") eq = c;
      else if (c.op === ">=" || c.op === ">") lo = c;
      else hi = c;
    }
    if (!cmps.length) return "anything — there is no bound left to clear.";
    if (eq) return "only " + fmtV(eq.ver) + " itself.";
    /* >* and <* expand to <0.0.0-0, which nothing can satisfy */
    if (!lo && hi && hi.op === "<" && hi.ver.major === 0 && hi.ver.minor === 0 && hi.ver.patch === 0 && hi.tail) {
      return "nothing at all — nothing sits below 0.0.0.";
    }
    var a = lo ? (lo.op === ">" ? "anything after " + fmtV(lo.ver)
                 : "from " + baseV(lo) + (lo.tail ? " and its pre-releases" : "")) : "anything";
    if (!hi) return a + " upward, with no ceiling.";
    var b = hi.op === "<=" ? " through " + fmtV(hi.ver) + ", inclusive." : " up to but not including " + baseV(hi) + ".";
    return a + b;
  }

  /* comparator as markup: the added -0 rides along dimmed, so the eye reads the number */
  function cmpHTML(c) {
    return esc(c.head) + (c.tail ? '<span class="z">' + c.tail + "</span>" : "");
  }

  function tokenNote(t) {
    var p = t.part, b = boundsPhrase(t.cmps);
    if (t.kind === "caret") {
      if (p.major === null) return "A caret on * is just *: " + b;
      if (p.major > 0) return "The caret pins the leading number and lets everything after it move: " + b;
      if (p.minor > 0 || p.minor === null) return "Below 1.0.0 the caret gets cautious — the first non-zero number is treated as the breaking one, so it pins the minor instead: " + b;
      return "At 0.0.x every patch is fair game to break things, so the caret pins all three: " + b;
    }
    if (t.kind === "tilde") {
      if (p.minor === null) return "A tilde with only a major number behaves like a caret: " + b;
      return "The tilde lets the last number move and freezes the rest: " + b;
    }
    if (t.kind === "hyphen") return "A dash range is inclusive on the left and, when the right side is complete, on the right too: " + b;
    if (t.kind === "xrange") return "An x (or a missing number) means “any value here”: " + b;
    if (t.kind === "any") return "A bare * has no bound to write down at all, so npm writes none. Anything gets through — except pre-releases, which need a bound naming one before they are let in.";
    if (t.kind === "exact") return "A version with no operator is a pin: " + b;
    return "Read literally: " + b;
  }

  function clauseSentence(clause) {
    return boundsPhrase(clause.cmps).replace(/^./, function (c) { return c.toUpperCase(); });
  }

  /* ---- state ---- */
  var DEFAULTS = [
    "0.1.0", "0.2.3", "0.2.9", "0.3.0",
    "1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-alpha.beta", "1.0.0-beta",
    "1.0.0-beta.2", "1.0.0-beta.11", "1.0.0-rc.1", "1.0.0",
    "1.0.4", "1.1.0", "1.2.2", "1.2.3", "1.2.4", "1.2.10",
    "1.3.0-rc.1", "1.3.0", "1.9.9",
    "2.0.0-rc.1", "2.0.0", "2.5.1", "3.0.0"
  ];
  var PRESETS = [
    "^1.2.3", "~1.2.3", ">=1.2 <2", "1.2.3 - 2.0.0", "1.x",
    "^0.2.3", "1.2.3 || >=2.5.0", "^2.0.0-rc.1", "*"
  ];

  var cands = [];        /* parsed, sorted ascending */
  var results = [];      /* per-candidate test result, aligned with cands */
  var range = null;
  var includePre = false;
  var pin = null;        /* {t:"v", i} | {t:"c", ci, ti} */
  var hover = null;
  var cells = [];
  var activeIdx = 0;

  function setCands(list) {
    var seen = {}, out = [];
    for (var i = 0; i < list.length; i++) {
      var v = typeof list[i] === "string" ? parseVersion(list[i]) : list[i];
      if (!v) continue;
      var key = fmtV(v);
      if (seen[key]) continue;
      seen[key] = 1;
      out.push(v);
    }
    out.sort(function (a, b) { var c = compare(a, b); return c !== 0 ? c : (fmtV(a) < fmtV(b) ? -1 : 1); });
    cands = out;
  }

  /* ---- range panel ---- */
  function renderRange() {
    var box = $("rangebox"), lamp = $("lamp"), err = $("err");
    var exp = $("expand"), plain = $("plain");
    exp.innerHTML = ""; 
    if (!range.ok) {
      box.classList.add("bad");
      lamp.dataset.state = "bad";
      lamp.textContent = "cannot read";
      err.hidden = false;
      err.textContent = range.error;
      plain.textContent = "";
      return;
    }
    box.classList.remove("bad");
    lamp.dataset.state = "ok";
    var n = 0;
    for (var q = 0; q < range.clauses.length; q++) n += range.clauses[q].cmps.length;
    lamp.textContent = range.clauses.length > 1
      ? range.clauses.length + " alternatives · " + n + " bounds"
      : n === 0 ? "no bounds" : n + (n === 1 ? " bound" : " bounds");
    err.hidden = true;

    for (var ci = 0; ci < range.clauses.length; ci++) {
      if (ci > 0) {
        var bar = document.createElement("div");
        bar.className = "orbar";
        bar.innerHTML = '<i></i><span class="k-lab">or</span><i></i>';
        exp.appendChild(bar);
      }
      var cl = document.createElement("div");
      cl.className = "clause";
      var lab = document.createElement("span");
      lab.className = "cno";
      lab.textContent = "accepts";
      cl.appendChild(lab);
      var cs = range.clauses[ci].cmps;
      if (!cs.length) {
        var nob = document.createElement("span");
        nob.className = "nob";
        nob.textContent = "any version";
        cl.appendChild(nob);
      }
      for (var i = 0; i < cs.length; i++) {
        if (i > 0) {
          var and = document.createElement("span");
          and.className = "k-lab";
          and.textContent = "and";
          cl.appendChild(and);
        }
        var b = document.createElement("button");
        b.type = "button";
        b.className = "cmp";
        b.innerHTML = cmpHTML(cs[i]);
        b.dataset.ci = ci;
        b.dataset.i = i;
        b.setAttribute("aria-label", "Comparator " + cs[i].text + ". Show what it allows.");
        cl.appendChild(b);
      }
      exp.appendChild(cl);
    }

    var ceil = null, floor = null;
    for (var z = 0; z < range.clauses.length; z++) {
      var zs = range.clauses[z].cmps;
      for (var y = 0; y < zs.length; y++) {
        if (!zs[y].tail) continue;
        if (zs[y].op === ">=" || zs[y].op === ">") { if (!floor) floor = zs[y]; }
        else if (!ceil) ceil = zs[y];
      }
    }
    var zn = $("zeronote");
    if (ceil || floor) {
      zn.hidden = false;
      zn.innerHTML = "That dim <code>-0</code> is npm's own handwriting, not a version anyone published: it is the lowest pre-release tag that can exist. " +
        (ceil ? "A ceiling of <code>" + esc(ceil.text) + "</code> therefore means “not even " + esc(baseV(ceil)) + "'s own release candidates.” " : "") +
        (floor ? "A floor of <code>" + esc(floor.text) + "</code> means “start underneath every pre-release of " + esc(baseV(floor)) + "” — floors pick the tag up only while pre-releases are switched on." : "");
    } else {
      zn.hidden = true;
      zn.textContent = "";
    }

    var sent = [];
    for (var s = 0; s < range.clauses.length; s++) sent.push(clauseSentence(range.clauses[s]));
    plain.innerHTML = "<b>In words:</b> " + esc(sent.join(" — or — "));
  }

  /* ---- grid ---- */
  function buildGrid() {
    var host = $("groups");
    host.innerHTML = "";
    cells = [];
    var i = 0;
    while (i < cands.length) {
      var maj = cands[i].major, start = i;
      while (i < cands.length && cands[i].major === maj) i++;
      var g = document.createElement("div");
      g.className = "grp";
      var head = document.createElement("div");
      head.className = "grphead";
      head.innerHTML = '<span class="gname">' + maj + '.x</span><i></i>' +
        '<span class="gcount" data-from="' + start + '" data-to="' + i + '"></span>';
      g.appendChild(head);
      var wrap = document.createElement("div");
      wrap.className = "cells";
      for (var k = start; k < i; k++) {
        var b = document.createElement("button");
        b.type = "button";
        b.className = "cell";
        b.dataset.i = k;
        b.tabIndex = -1;
        b.innerHTML = '<span class="num">' + esc(fmtV(cands[k])) + '</span><span class="mk" aria-hidden="true">·</span>';
        wrap.appendChild(b);
        cells.push(b);
      }
      g.appendChild(wrap);
      host.appendChild(g);
    }
    if (activeIdx >= cells.length) activeIdx = 0;
    if (cells.length) cells[activeIdx].tabIndex = 0;
  }

  function paint() {
    results = [];
    var matches = 0, pick = -1;
    for (var i = 0; i < cands.length; i++) {
      var r = range.ok ? testVersion(cands[i], range, includePre) : null;
      results.push(r);
      if (r && r.match) { matches++; pick = i; }
    }
    for (var k = 0; k < cells.length; k++) {
      var idx = +cells[k].dataset.i;
      var m = results[idx];
      var lbl = fmtV(cands[idx]);
      if (!range.ok) {
        cells[k].dataset.m = "?";
        cells[k].setAttribute("aria-label", lbl + " — range not readable");
      } else {
        cells[k].dataset.m = m.match ? "1" : "0";
        cells[k].setAttribute("aria-label", lbl + (m.match ? " — accepted" : " — filtered out"));
      }
      cells[k].querySelector(".mk").textContent = (range.ok && m.match) ? "✓" : "·";
      var old = cells[k].querySelector(".pick");
      if (old) old.remove();
      if (idx === pick) {
        var t = document.createElement("span");
        t.className = "pick";
        t.textContent = "installed";
        cells[k].querySelector(".num").appendChild(t);
        cells[k].setAttribute("aria-label", lbl + " — accepted, and the highest match, so this is the one installed");
      }
      cells[k].dataset.pin = (pin && pin.t === "v" && pin.i === idx) ? "1" : "0";
    }
    var counters = document.querySelectorAll(".gcount");
    for (var c = 0; c < counters.length; c++) {
      var from = +counters[c].dataset.from, to = +counters[c].dataset.to, hit = 0;
      for (var j = from; j < to; j++) if (results[j] && results[j].match) hit++;
      counters[c].textContent = range.ok ? hit + "/" + (to - from) : "—";
    }
    $("s-count").textContent = range.ok ? matches + " / " + cands.length : "— / " + cands.length;
    $("s-count").className = "val " + (range.ok && matches ? "run" : "off");
    $("s-pick").textContent = pick >= 0 ? fmtV(cands[pick]) : (range.ok ? "nothing" : "—");
    $("s-pick").className = "val " + (pick >= 0 ? "acc" : "off");
    renderWhy();
  }

  /* ---- why panel ---- */
  function whyDefault() {
    return '<p class="k-lab" style="margin:0 0 6px">reading</p>' +
      '<p class="hint">Point at any version in the grid, or at a comparator above, and the reasoning shows up here. ' +
      'Click to keep it pinned.</p>';
  }

  function whyVersion(idx, pinned) {
    var v = cands[idx], r = results[idx];
    var h = '<div class="whyhead"><p class="wv">' + esc(fmtV(v)) + '</p>';
    if (!range.ok) {
      h += '<span class="verdict">unknown</span></div><p class="hint">Fix the range above and this fills in.</p>';
      return h;
    }
    h += '<span class="verdict" data-v="' + (r.match ? "yes" : "no") + '">' + (r.match ? "accepted" : "filtered out") + "</span></div>";
    if (range.clauses.length > 1) {
      h += '<p class="k-lab" style="margin:0 0 2px">' + (r.match ? "matched on" : "closest") + " alternative " + (r.clause + 1) + "</p>";
    }
    h += '<ul class="checks">';
    if (!r.lines.length) {
      h += '<li data-p="1"><span class="cw">no bounds to clear</span><span class="cs">✓ true</span></li>';
    }
    for (var i = 0; i < r.lines.length; i++) {
      h += '<li data-p="' + (r.lines[i].pass ? "1" : "0") + '"><span class="cw">' + esc(fmtV(v)) + " " +
        cmpHTML(r.lines[i].c) + '</span><span class="cs">' + (r.lines[i].pass ? "✓ true" : "✕ false") + "</span></li>";
    }
    if (v.pre.length) {
      /* the rule only gets a say once every bound has already passed */
      var allBounds = r.passes === r.lines.length;
      var state, word;
      if (!allBounds) { state = "rule"; word = "not reached"; }
      else if (r.preBlocked) { state = "rule"; word = "✕ blocks"; }
      else if (includePre) { state = "1"; word = "✓ turned off"; }
      else { state = "1"; word = "✓ waived"; }
      h += '<li data-p="' + state + '"><span class="cw">pre-release rule</span><span class="cs">' + word + "</span></li>";
    }
    h += "</ul>";

    var say;
    if (r.match) {
      if (!r.lines.length) say = "This range writes down no bounds at all, so there is nothing for a version to fail.";
      else if (!v.pre.length) say = "Every bound is satisfied.";
      else if (includePre) say = "Every bound is satisfied, and pre-releases are switched on, so nothing holds it back.";
      else say = "Every bound is satisfied, and the range names a pre-release on this exact <span class=\"m\">" +
        v.major + "." + v.minor + "." + v.patch + "</span>, so this pre-release is allowed through.";
    } else if (r.preBlocked) {
      say = (r.lines.length ? "It clears every bound, but it" : "It") +
        " is a pre-release. A range skips pre-releases unless the range itself " +
        "mentions one on the same <span class=\"m\">" + v.major + "." + v.minor + "." + v.patch +
        "</span>. Flip <span class=\"m\">pre-releases</span> to included above, or name it yourself with " +
        "<span class=\"m\">>=" + esc(fmtV(v)) + "</span>.";
    } else {
      var fail = null;
      for (var f = 0; f < r.lines.length; f++) if (!r.lines[f].pass) { fail = r.lines[f].c; break; }
      var why = "";
      if (fail) {
        if (fail.op === "<" || fail.op === "<=") why = "it is at or past the upper bound";
        else if (fail.op === ">" || fail.op === ">=") why = "it is older than the lower bound";
        else why = "the range pins one exact version and this is not it";
      }
      say = "Fails <span class=\"m\">" + esc(fail ? fail.text : "") + "</span> — " + why + ".";
    }
    h += '<p class="say">' + say + "</p>";

    var prev = idx > 0 ? fmtV(cands[idx - 1]) : null;
    var next = idx < cands.length - 1 ? fmtV(cands[idx + 1]) : null;
    h += '<div class="neigh">' + (prev ? esc(prev) + " &lt; " : "") + "<b>" + esc(fmtV(v)) + "</b>" +
      (next ? " &lt; " + esc(next) : "") + "</div>";
    if (v.pre.length) {
      h += '<p class="note" style="font-size:.8rem">Pre-release fields are compared one at a time, left to right. ' +
        'A number always ranks below a word, and a shorter list ranks below a longer one that starts the same.</p>';
    }
    if (pinned) h += '<p class="pintag" style="margin:8px 0 0">pinned · click again to release</p>';
    return h;
  }

  function whyCmp(ci, i, pinned) {
    var c = range.clauses[ci].cmps[i];
    var tok = null, seen = 0;
    var toks = range.clauses[ci].tokens;
    for (var t = 0; t < toks.length; t++) {
      if (i < seen + toks[t].cmps.length) { tok = toks[t]; break; }
      seen += toks[t].cmps.length;
    }
    var pass = 0;
    for (var k = 0; k < cands.length; k++) if (cmpTest(cands[k], c)) pass++;
    var h = '<div class="whyhead"><p class="wv">' + cmpHTML(c) + '</p><span class="verdict">one bound</span></div>';
    h += '<p class="k-lab" style="margin:0 0 4px">written as</p><p class="wv" style="font-size:.86rem">' + esc(tok ? tok.src : "") + "</p>";
    h += '<p class="say">' + esc(tokenNote(tok || { kind: "op", part: {}, cmps: [c] })) + "</p>";
    if (c.tail) {
      h += '<p class="say" style="color:var(--muted)">Nobody wrote the <span class="m">-0</span> — npm adds it. ' +
        (c.op === ">=" || c.op === ">"
          ? "It drops this floor underneath every pre-release of <span class=\"m\">" + esc(baseV(c)) + "</span>."
          : "It pulls this ceiling underneath <span class=\"m\">" + esc(baseV(c)) + "</span>'s own pre-releases, so a release candidate for the next version cannot sneak in below the line.") + "</p>";
    }
    h += '<ul class="checks"><li data-p="1"><span class="cw">candidates clearing this bound alone</span>' +
      '<span class="cs">' + pass + "/" + cands.length + "</span></li>";
    if (range.clauses[ci].cmps.length > 1) {
      h += '<li data-p="0"><span class="cw">bounds combined with it</span><span class="cs">' +
        (range.clauses[ci].cmps.length - 1) + "</span></li>";
    }
    h += "</ul>";
    if (pinned) h += '<p class="pintag" style="margin:8px 0 0">pinned · click again to release</p>';
    return h;
  }

  function renderWhy() {
    var target = hover || pin;
    var pinned = !hover && !!pin;
    var el = $("why");
    /* put out any lit comparator first, so an early return can't leave one stuck on */
    var on = document.querySelectorAll('.cmp[data-on="1"]');
    for (var z = 0; z < on.length; z++) on[z].dataset.on = "0";
    if (!target) { el.innerHTML = whyDefault(); return; }
    if (target.t === "v") {
      if (target.i >= cands.length) { el.innerHTML = whyDefault(); return; }
      el.innerHTML = whyVersion(target.i, pinned);
    } else {
      if (!range.ok || !range.clauses[target.ci] || !range.clauses[target.ci].cmps[target.ti]) {
        el.innerHTML = whyDefault();
        return;
      }
      el.innerHTML = whyCmp(target.ci, target.ti, pinned);
    }
    if (target.t === "c") {
      var b = document.querySelector('.cmp[data-ci="' + target.ci + '"][data-i="' + target.ti + '"]');
      if (b) b.dataset.on = "1";
    }
  }

  /* ---- apply ---- */
  /* the pre-release switch changes how the range EXPANDS, not just how it is tested,
     so every re-test goes back through the expander with the flag */
  function applyRange(src) {
    range = parseRange(src, includePre);
    if (pin && pin.t === "c" &&
        !(range.ok && range.clauses[pin.ci] && range.clauses[pin.ci].cmps[pin.ti])) pin = null;
    renderRange();
    paint();
    var chips = $("presets").querySelectorAll(".chip");
    for (var i = 0; i < chips.length; i++) {
      chips[i].setAttribute("aria-pressed", chips[i].dataset.r === src.trim() ? "true" : "false");
    }
  }
  function reapply() { applyRange($("range").value); }

  /* ---- wiring ---- */
  function buildPresets() {
    var host = $("presets");
    for (var i = 0; i < PRESETS.length; i++) {
      var b = document.createElement("button");
      b.type = "button";
      b.className = "chip";
      b.dataset.r = PRESETS[i];
      b.textContent = PRESETS[i];
      b.setAttribute("aria-pressed", "false");
      host.appendChild(b);
    }
    host.addEventListener("click", function (e) {
      var b = e.target.closest(".chip");
      if (!b) return;
      $("range").value = b.dataset.r;
      applyRange(b.dataset.r, true);
    });
  }

  $("range").addEventListener("input", function () { applyRange(this.value); });

  function setPreTog() {
    $("pretog").setAttribute("aria-pressed", includePre ? "true" : "false");
    $("pretogtxt").textContent = includePre ? "included" : "excluded";
  }
  $("pretog").addEventListener("click", function () {
    includePre = !includePre;
    setPreTog();
    reapply();
  });

  $("reset").addEventListener("click", function () {
    setCands(DEFAULTS);
    includePre = false;
    setPreTog();
    $("range").value = "^1.2.3";
    $("addv").value = "";
    setAddMsg("", "");
    pin = { t: "v", i: indexOfVersion("1.3.0-rc.1") };
    hover = null;
    activeIdx = 0;
    buildGrid();
    applyRange("^1.2.3");
  });

  function indexOfVersion(s) {
    var v = parseVersion(s);
    if (!v) return -1;
    for (var i = 0; i < cands.length; i++) if (compare(cands[i], v) === 0 && cands[i].build === v.build) return i;
    return -1;
  }

  function setAddMsg(txt, state) {
    var el = $("addmsg");
    el.textContent = txt;
    if (state) el.dataset.s = state; else el.removeAttribute("data-s");
  }

  function addVersion() {
    var raw = $("addv").value.trim();
    if (!raw) { setAddMsg("Type a version first.", "bad"); return; }
    var v = parseVersion(raw);
    if (!v) {
      var m = /^v?(\d+)(\.\d+)?$/.exec(raw);
      setAddMsg(m ? "Give all three numbers, like " + raw.replace(/^v/, "") + (m[2] ? ".0" : ".0.0") + "."
                  : "“" + raw + "” is not a valid version.", "bad");
      return;
    }
    var existing = indexOfVersion(fmtV(v));
    if (existing >= 0) {
      setAddMsg(fmtV(v) + " is already in the grid.", "");
      revealCandidate(existing);
      return;
    }
    var list = cands.slice();
    list.push(v);
    setCands(list);
    buildGrid();
    paint();
    var idx = indexOfVersion(fmtV(v));
    setAddMsg("added " + fmtV(v) + " · " + (results[idx] && results[idx].match ? "accepted" : "filtered out"),
      results[idx] && results[idx].match ? "ok" : "");
    $("addv").value = "";
    revealCandidate(idx);
  }

  /* show a version without stealing focus — the typist stays in the add field */
  function revealCandidate(idx) {
    for (var i = 0; i < cells.length; i++) {
      if (+cells[i].dataset.i === idx) {
        setActive(i);
        pin = { t: "v", i: idx };
        hover = null;
        paint();
        cells[i].classList.remove("fresh");
        void cells[i].offsetWidth;
        cells[i].classList.add("fresh");
        if (cells[i].scrollIntoView) cells[i].scrollIntoView({ block: "nearest", inline: "nearest" });
        return;
      }
    }
  }

  $("addbtn").addEventListener("click", addVersion);
  $("addv").addEventListener("keydown", function (e) {
    if (e.key === "Enter") { e.preventDefault(); addVersion(); }
  });

  function setActive(k) {
    if (!cells.length) return;
    if (cells[activeIdx]) cells[activeIdx].tabIndex = -1;
    activeIdx = Math.max(0, Math.min(cells.length - 1, k));
    cells[activeIdx].tabIndex = 0;
  }

  /* grid interaction: hover previews, click pins, focus pins */
  var groups = $("groups");
  groups.addEventListener("mouseover", function (e) {
    var c = e.target.closest(".cell");
    if (!c) return;
    hover = { t: "v", i: +c.dataset.i };
    renderWhy();
  });
  groups.addEventListener("mouseleave", function () { hover = null; renderWhy(); });
  groups.addEventListener("click", function (e) {
    var c = e.target.closest(".cell");
    if (!c) return;
    var i = +c.dataset.i;
    pin = (pin && pin.t === "v" && pin.i === i) ? null : { t: "v", i: i };
    hover = null;
    setActive(cells.indexOf(c));
    paint();
  });
  groups.addEventListener("focusin", function (e) {
    var c = e.target.closest(".cell");
    if (!c) return;
    setActive(cells.indexOf(c));
    pin = { t: "v", i: +c.dataset.i };
    hover = null;
    paint();
  });
  groups.addEventListener("keydown", function (e) {
    var c = e.target.closest(".cell");
    if (!c) return;
    var k = cells.indexOf(c), next = -1;
    if (e.key === "ArrowRight") next = k + 1;
    else if (e.key === "ArrowLeft") next = k - 1;
    else if (e.key === "Home") next = 0;
    else if (e.key === "End") next = cells.length - 1;
    else if (e.key === "ArrowDown" || e.key === "ArrowUp") next = rowJump(k, e.key === "ArrowDown" ? 1 : -1);
    else return;
    if (next < 0 || next >= cells.length) { e.preventDefault(); return; }
    e.preventDefault();
    setActive(next);
    cells[next].focus();
  });

  /* move to the visually nearest cell one row up or down */
  function rowJump(k, dir) {
    var cur = cells[k], top = cur.offsetTop, left = cur.offsetLeft;
    var best = -1, bestTop = null, bestDx = Infinity;
    for (var i = 0; i < cells.length; i++) {
      var t = cells[i].offsetTop;
      if (dir > 0 ? t <= top : t >= top) continue;
      if (bestTop === null || (dir > 0 ? t < bestTop : t > bestTop)) { bestTop = t; best = -1; bestDx = Infinity; }
      if (t !== bestTop) continue;
      var dx = Math.abs(cells[i].offsetLeft - left);
      if (dx < bestDx) { bestDx = dx; best = i; }
    }
    return best;
  }

  /* comparator chips */
  $("expand").addEventListener("mouseover", function (e) {
    var b = e.target.closest(".cmp");
    if (!b) return;
    hover = { t: "c", ci: +b.dataset.ci, ti: +b.dataset.i };
    renderWhy();
  });
  $("expand").addEventListener("mouseleave", function () { hover = null; renderWhy(); });
  $("expand").addEventListener("focusin", function (e) {
    var b = e.target.closest(".cmp");
    if (!b) return;
    pin = { t: "c", ci: +b.dataset.ci, ti: +b.dataset.i };
    hover = null;
    paint();
  });
  $("expand").addEventListener("click", function (e) {
    var b = e.target.closest(".cmp");
    if (!b) return;
    var same = pin && pin.t === "c" && pin.ci === +b.dataset.ci && pin.ti === +b.dataset.i;
    pin = same ? null : { t: "c", ci: +b.dataset.ci, ti: +b.dataset.i };
    hover = null;
    paint();
  });

  /* platform theme */
  window.addEventListener("message", function (e) {
    var d = e.data;
    if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
      document.documentElement.setAttribute("data-theme", d.theme);
    }
  });

  /* boot */
  buildPresets();
  setCands(DEFAULTS);
  buildGrid();
  pin = { t: "v", i: indexOfVersion("1.3.0-rc.1") };
  applyRange("^1.2.3");
})();
</script>
</body>
</html>

source-visible by construction · nothing is published here without its code

Comments

0 total

No comments yet. If you ran it, say what happened.

Report this post

Goes straight to the moderation queue. Enough independent reports and the post is suspended automatically until a human looks.

what is wrong