| 1 | <!-- 🤖 Public contributors index. Fully self-contained: inline CSS/JS, no |
| 2 | external requests except ./contributors.json (relative) — avatars ride |
| 3 | inside the JSON as data URIs. Like carbon.json, the JSON is COMMITTED |
| 4 | data (web/status/contributors.json, refreshed by tools/contributors.mjs), |
| 5 | republished as-is by deploy/status-publish.sh. --> |
| 6 | <!doctype html> |
| 7 | <html lang="en"> |
| 8 | <head> |
| 9 | <meta charset="utf-8" /> |
| 10 | <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| 11 | <meta name="color-scheme" content="light dark" /> |
| 12 | <title>Eleven — contributors</title> |
| 13 | <link rel="icon" type="image/png" href="./icon-192.png" /> |
| 14 | <link rel="stylesheet" href="./status.css" /> |
| 15 | <style> |
| 16 | /* Contributors-only: the identity block (avatar / AI mark, name, kind, bio) |
| 17 | and the wider first column. Shared .bars supplies the container, rows, bar |
| 18 | mark, and count; everything else shared lives in status.css. */ |
| 19 | .bars li { grid-template-columns: minmax(180px, 260px) 1fr 5em; padding: 12px 16px; } |
| 20 | .who { display: flex; gap: 10px; align-items: flex-start; min-width: 0; } |
| 21 | .who img, .who .ai-mark { |
| 22 | width: 36px; height: 36px; border-radius: 50%; flex: none; |
| 23 | border: 1px solid var(--border); |
| 24 | } |
| 25 | .who .ai-mark { |
| 26 | display: inline-flex; align-items: center; justify-content: center; |
| 27 | background: var(--plane); color: var(--ink-2); |
| 28 | font-size: 11px; font-weight: 650; letter-spacing: 0.04em; |
| 29 | } |
| 30 | .who .name { font-size: 14px; font-weight: 600; overflow-wrap: anywhere; } |
| 31 | .who .name a { color: inherit; text-decoration: none; } |
| 32 | .who .name a:hover { text-decoration: underline; } |
| 33 | .who .kind { font-size: 12px; color: var(--ink-2); font-weight: 400; margin-left: 6px; } |
| 34 | .who .bio { display: block; font-size: 12px; font-weight: 400; color: var(--ink-2); margin-top: 1px; } |
| 35 | </style> |
| 36 | </head> |
| 37 | <body> |
| 38 | <div class="wrap"> |
| 39 | <header data-title="Contributors"></header> |
| 40 | <nav class="status-nav" aria-label="Status"></nav> |
| 41 | <script src="./status-nav.js"></script> |
| 42 | <p class="lede">Who builds Eleven — humans and AI models on the same metric. |
| 43 | This app is AI-written, so the models appear in the index exactly as the |
| 44 | humans do: <span id="metric">—</span>.</p> |
| 45 | <p class="asof">As of <time id="asof" datetime="">—</time></p> |
| 46 | |
| 47 | <p id="error" role="alert">Couldn’t load the contributors index. It may be mid-publish — this page retries every minute.</p> |
| 48 | |
| 49 | <section aria-labelledby="h-list"> |
| 50 | <h2 id="h-list">By commits</h2> |
| 51 | <ul class="bars" id="list"><li><span class="who"><span class="name">Loading…</span></span></li></ul> |
| 52 | </section> |
| 53 | |
| 54 | <footer> |
| 55 | <p>Refreshed from GitHub by tools/contributors.mjs and committed to the |
| 56 | repo. Contributors own their row: edit your bio (or hide yourself) in |
| 57 | web/status/contributors.json by PR.</p> |
| 58 | </footer> |
| 59 | </div> |
| 60 | |
| 61 | <script> |
| 62 | // contract: the fields this page reads from ./contributors.json — kept in sync |
| 63 | // with web/status/contributors.json and checked by test/status-contract.test.mjs. |
| 64 | const FIELDS = [ |
| 65 | "updated_at", "metric", |
| 66 | "contributors[].id", "contributors[].kind", "contributors[].name", |
| 67 | "contributors[].bio", "contributors[].commits", "contributors[].url", |
| 68 | "contributors[].avatar", |
| 69 | ]; |
| 70 | |
| 71 | const $ = (id) => document.getElementById(id); |
| 72 | |
| 73 | // All rendering is textContent / createElement — never innerHTML of fetched |
| 74 | // data, even though the feed is our own. |
| 75 | function render(d) { |
| 76 | $("asof").dateTime = d.updated_at; |
| 77 | $("asof").textContent = new Date(d.updated_at).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }); |
| 78 | $("metric").textContent = d.metric; |
| 79 | |
| 80 | const list = $("list"); |
| 81 | list.replaceChildren(); |
| 82 | const rows = (d.contributors || []).filter((c) => !c.hidden); |
| 83 | const max = Math.max(1, ...rows.map((c) => c.commits)); |
| 84 | for (const c of rows) { |
| 85 | const li = document.createElement("li"); |
| 86 | |
| 87 | const who = document.createElement("span"); |
| 88 | who.className = "who"; |
| 89 | if (c.kind === "human" && c.avatar) { |
| 90 | const img = document.createElement("img"); |
| 91 | img.src = c.avatar; |
| 92 | img.alt = ""; |
| 93 | who.append(img); |
| 94 | } else { |
| 95 | const mark = document.createElement("span"); |
| 96 | mark.className = "ai-mark"; |
| 97 | mark.textContent = "AI"; |
| 98 | mark.setAttribute("aria-hidden", "true"); |
| 99 | who.append(mark); |
| 100 | } |
| 101 | const id = document.createElement("span"); |
| 102 | const name = document.createElement("span"); |
| 103 | name.className = "name"; |
| 104 | if (c.url) { |
| 105 | const a = document.createElement("a"); |
| 106 | a.href = c.url; |
| 107 | a.textContent = c.name; |
| 108 | name.append(a); |
| 109 | } else { |
| 110 | name.textContent = c.name; |
| 111 | } |
| 112 | const kind = document.createElement("span"); |
| 113 | kind.className = "kind"; |
| 114 | kind.textContent = c.kind === "ai" ? "AI model" : "@" + c.id; |
| 115 | name.append(kind); |
| 116 | id.append(name); |
| 117 | if (c.bio) { |
| 118 | const bio = document.createElement("span"); |
| 119 | bio.className = "bio"; |
| 120 | bio.textContent = c.bio; |
| 121 | name.append(bio); |
| 122 | } |
| 123 | who.append(id); |
| 124 | |
| 125 | const track = document.createElement("span"); |
| 126 | track.className = "track"; |
| 127 | track.setAttribute("aria-hidden", "true"); |
| 128 | const bar = document.createElement("span"); |
| 129 | bar.className = "bar"; |
| 130 | bar.style.width = (100 * c.commits / max) + "%"; |
| 131 | track.append(bar); |
| 132 | |
| 133 | const count = document.createElement("span"); |
| 134 | count.className = "count"; |
| 135 | count.textContent = c.commits.toLocaleString(); |
| 136 | |
| 137 | li.append(who, track, count); |
| 138 | list.append(li); |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | async function refresh() { |
| 143 | try { |
| 144 | const res = await fetch("./contributors.json", { cache: "no-cache" }); |
| 145 | if (!res.ok) throw new Error("HTTP " + res.status); |
| 146 | render(await res.json()); |
| 147 | $("error").classList.remove("show"); |
| 148 | } catch (e) { |
| 149 | // Keep the previous render (no skeleton flash); surface the failure inline. |
| 150 | $("error").classList.add("show"); |
| 151 | } |
| 152 | } |
| 153 | refresh(); |
| 154 | setInterval(refresh, 60 * 1000); |
| 155 | void FIELDS; |
| 156 | </script> |
| 157 | </body> |
| 158 | </html> |
| 159 | |