the front page shows people in two columns in their svfe colours, with a commons and the last 30 days of pushes
Signed-off-by: dawn <90008@klbr.net>
755 unmodified linesExpand all756757758759760760761762763764417 unmodified linesExpand all755 unmodified linesExpand all );
let first_html = std::fs::read_to_string(first_dir.join("www/index.html")).unwrap(); assert!( first_html.contains("<h1>knot<span>.</span>phase0<span>.</span>test</h1>"), first_html.contains("<h1 class=\"host\">knot<span>.</span>phase0<span>.</span>test</h1>"), "{first_html}" ); assert!( first_html.contains("<link rel=\"icon\" href=\"/favicon.svg\" type=\"image/svg+xml\">"),417 unmodified linesExpand all227 unmodified linesExpand all228229230231232233234235236237238239240241242243244245246247248249250251252253254255256227 unmodified linesExpand all }}
impl Put for OwnerHandles {}
pub(crate) struct IndexPulse;
impl Record for IndexPulse { type Area = OutputRoot; const NAME: &'static str = "index page push counts"; const VERSION: u32 = 1; const DURABILITY: Durability = Durability::Rename; type Key = (); type Value = crate::index_page::Counted;
fn relative((): &Self::Key) -> PathBuf { PathBuf::from("index-pulse.json") }
fn encode(value: &Self::Value) -> anyhow::Result<Vec<u8>> { encode_json(Self::VERSION, value) }
fn decode(bytes: &[u8]) -> anyhow::Result<Self::Value> { decode_json(Self::VERSION, Self::NAME, bytes) }}
impl Put for IndexPulse {}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122:root{--bg:#fdf7fa;--card:#fff;--text:#3b2f36;--muted:#9a8691;--accent:#d9628f;--soft:#fbe3ee;--border:#f1d5e2;--lav:#8a6fd0;--teal:#3f9c8c;--amber:#c08a2c;--sky:#4f8fc4;--field-shade:color-mix(in srgb,var(--accent) 12%,transparent);color-scheme:light dark}@media (prefers-color-scheme:dark){:root{--bg:#1b1519;--card:#241c21;--text:#f1e4ea;--muted:#a8919d;--accent:#f28bb5;--soft:#3a2531;--border:#3d2c36;--lav:#b6a4f0;--teal:#5fbfae;--amber:#e3b45f;--sky:#7fb3e0;--field-shade:color-mix(in srgb,#000 18%,transparent)}}*{box-sizing:border-box}[hidden]{display:none!important}html.js{scrollbar-width:none}html.js::-webkit-scrollbar{display:none}body{margin:0;min-height:100vh;background:var(--bg);color:var(--text);font:14px/1.6 ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace;-webkit-font-smoothing:antialiased}main{max-width:74rem;margin:0 auto;padding:2rem clamp(1rem,4vw,2.5rem)}a{color:var(--accent)}ul{list-style:none;margin:0;padding:0}button{font:inherit;cursor:pointer;border:0;padding:0;background:none;color:var(--muted)}mark{background:var(--soft);color:var(--accent)}
/* the ribbon */.bar{display:flex;align-items:stretch;border:1px solid var(--border);background:var(--card);min-height:2.1rem}.bar>*{display:flex;align-items:center;padding:0 .8rem;border-right:1px solid var(--border);white-space:nowrap}.bar .host{margin:0;background:var(--accent);color:var(--bg);font-weight:700;letter-spacing:-.01em;font-size:1.02rem}.bar .face{display:block;line-height:2.1rem;color:var(--muted);font-size:.84rem;background:var(--soft)}.bar .face a{color:var(--accent);text-decoration:none}.bar .face a:hover{text-decoration:underline}.bar .sp{flex:1;padding:0;border-right:0}.pulse{align-items:flex-end;gap:1px;padding:.35rem .7rem 0}.pulse i{flex:none;width:3px;background:var(--accent);opacity:.75}.pulse i.z{height:1px;background:var(--border);opacity:1}.pulse i:hover{opacity:1;background:var(--text)}.bar .search{border:1px solid var(--border);margin:3px;padding:0 .3rem 0 .5rem;gap:.45rem;color:var(--muted);font-size:.84rem;cursor:text;background:var(--card);box-shadow:inset 0 1px 0 var(--field-shade)}.search .mg{flex:none;width:1em;height:1em}.search input{font:inherit;color:var(--text);background:none;border:0;outline:none;width:10rem;cursor:text}.search input::placeholder{color:var(--muted);opacity:.85}.search input::-webkit-search-cancel-button{display:none}.search kbd{flex:none;font:inherit;font-size:.8em;line-height:1;padding:.12rem .35rem;border:1px solid var(--border);color:var(--muted);background:var(--card)}.search:focus-within kbd,.search:has(input:not(:placeholder-shown)) kbd{visibility:hidden}.bar .search:hover{border-color:color-mix(in srgb,var(--accent) 45%,var(--border))}.bar .search:focus-within{border-color:var(--accent);color:var(--accent)}@supports (corner-shape:bevel){.bar{border-top-left-radius:7px;border-bottom-right-radius:7px;corner-shape:bevel;overflow:clip}.bar .search{border-bottom-right-radius:4px;corner-shape:bevel}}@supports not (corner-shape:bevel){.bar{position:relative;clip-path:polygon(7px 0,100% 0,100% calc(100% - 7px),calc(100% - 7px) 100%,0 100%,0 7px)}.bar::before,.bar::after{content:"";position:absolute;z-index:6;width:7px;height:7px;pointer-events:none}.bar::before{left:-1px;top:-1px;background:linear-gradient(to bottom right,transparent 50%,var(--border) 0 calc(50% + 1.3px),transparent 0)}.bar::after{right:-1px;bottom:-1px;background:linear-gradient(to top left,transparent 50%,var(--border) 0 calc(50% + 1.3px),transparent 0)}.bar .search{position:relative;clip-path:polygon(0 0,100% 0,100% calc(100% - 4px),calc(100% - 4px) 100%,0 100%)}.bar .search::after{content:"";position:absolute;right:-1px;bottom:-1px;width:4px;height:4px;pointer-events:none;background:linear-gradient(to top left,transparent 50%,var(--sfl,var(--border)) 0 calc(50% + 1.3px),transparent 0)}.bar .search:hover{--sfl:color-mix(in srgb,var(--accent) 45%,var(--border))}.bar .search:focus-within{--sfl:var(--accent)}}
/* people, two columns */#people{display:flex;gap:3.5rem;align-items:flex-start;margin-top:1.6rem}#people,#commons,main>footer,#empty{padding-left:1.25rem;padding-right:1.25rem}.mc{flex:1 1 0;min-width:0}.person{margin:0 0 1.9rem}.u-chk{position:absolute;opacity:0;pointer-events:none;width:0;height:0}.ph{display:inline-flex;align-items:stretch;max-width:100%;border:1px solid var(--border);font-size:.86rem}.ph a{padding:0 .55rem;background:color-mix(in srgb,var(--line) 16%,var(--card));color:var(--line);font-weight:700;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.ph a:hover{background:var(--line);color:var(--bg)}.ph span{flex:none;padding:0 .5rem;color:var(--muted);border-left:1px solid var(--border);background:var(--card)}.ph span:first-child{border-left:0;background:var(--soft);color:var(--accent);font-weight:700}
/* rows hang off the owner's branch line */.repos{margin:.3rem 0 0 .35rem;position:relative}.repos li{position:relative;padding:.16rem 0 .16rem 1.5rem}.repos li:hover,.repos li:focus-within{z-index:20}.repos li::before{content:"";position:absolute;left:.35rem;top:0;bottom:0;border-left:1px solid var(--line,var(--accent));opacity:.45}.repos li::after{content:"";position:absolute;left:.35rem;top:calc(.16rem + .8em);width:.75rem;border-top:1px solid var(--line,var(--accent));opacity:.45}.repos li:last-child::before,.repos li.last::before,main:not(.searching) .u-chk:not(:checked)~.repos li.fold-last::before{bottom:auto;height:calc(.16rem + .8em)}.commons li{--line:var(--accent)}.row{position:relative;display:flex;align-items:baseline;gap:.5rem;min-width:0}.own{flex:none;color:var(--line)}.repos a{color:var(--text);text-decoration:none;white-space:nowrap}.repos a:hover{color:var(--accent)}.desc{color:var(--muted);font-size:.8rem;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.age{flex:none;margin-left:auto;font-size:.74rem;color:var(--muted);padding-left:.4rem}.pop{position:absolute;left:1.5rem;top:calc(100% + .1rem);z-index:30;max-width:min(26rem,calc(100vw - 3rem));padding:.42rem .65rem;background:var(--card);color:var(--text);border:1px solid var(--border);border-left:3px solid var(--line,var(--accent));font-size:.78rem;line-height:1.45;white-space:normal;pointer-events:none;opacity:0;transition:opacity .12s}.repos li:hover .pop,.repos li:focus-within .pop{opacity:1}.clone{position:absolute;right:0;top:50%;transform:translateY(-50%);font-size:.8rem;opacity:0;padding:0 .3rem 0 .4rem;border:1px solid transparent;background:var(--bg);transition:opacity .12s}.clone svg{display:block;width:1.05em;height:1.05em}.repos li:hover .clone,.repos li:focus-within .clone{opacity:.8}.clone:hover{color:var(--accent);border-color:var(--border);opacity:1}.repos li:hover .age,.repos li:focus-within .age{visibility:hidden}
/* the fold: a faint line and a dither band from the branch line to the ages */main:not(.searching) .u-chk:not(:checked)~.repos li.more{display:none}.repos.folds{padding-bottom:12px}.repos.folds::before{content:"";position:absolute;left:.35rem;right:0;bottom:0;height:1px;background:var(--border);pointer-events:none}.repos.folds::after{content:"";position:absolute;left:.35rem;right:0;bottom:0;height:12px;background:color-mix(in srgb,var(--line,var(--accent)) 55%,var(--border));-webkit-mask:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%228%22%20height%3D%2212%22%20shape-rendering%3D%22crispEdges%22%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%224%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%224%22%20y%3D%224%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%226%22%20y%3D%226%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%228%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%224%22%20y%3D%228%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%222%22%20y%3D%2210%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%226%22%20y%3D%2210%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3C%2Fsvg%3E") 0 0/8px 12px repeat-x;mask:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%228%22%20height%3D%2212%22%20shape-rendering%3D%22crispEdges%22%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%224%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%224%22%20y%3D%224%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%226%22%20y%3D%226%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%228%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%224%22%20y%3D%228%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%222%22%20y%3D%2210%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3Crect%20x%3D%226%22%20y%3D%2210%22%20width%3D%222%22%20height%3D%222%22%2F%3E%3C%2Fsvg%3E") 0 0/8px 12px repeat-x;pointer-events:none}.u-chk:checked~.repos.folds{padding-bottom:0}.u-chk:checked~.repos.folds::before,.u-chk:checked~.repos.folds::after,main.searching .repos.folds::before,main.searching .repos.folds::after{display:none}main.searching .repos.folds{padding-bottom:0}.unwind{display:inline-flex;cursor:pointer;margin:.3rem 0 0 1.85rem;font-size:.78rem;color:var(--line,var(--accent));user-select:none}.unwind:hover{text-decoration:underline}.u-chk:checked~.unwind .a,.u-chk:not(:checked)~.unwind .b,main.searching .unwind{display:none}
/* the commons: everyone with a repo or two, in one band */#commons{margin-top:.6rem}#commons .repos{columns:2;column-gap:3.85rem}#commons .repos li{break-inside:avoid}#commons .row{overflow:hidden}#commons .repos a{overflow:hidden;text-overflow:ellipsis;min-width:0;flex:0 0 auto;max-width:62%}#commons .desc{flex:1 1 0}
#empty{color:var(--muted);margin:1rem 0}footer{margin-top:1rem;font-size:.78rem;color:var(--muted);display:flex;justify-content:space-between}
/* the page's own scrollbar, drawn over the page */#th{position:fixed;right:2px;width:3px;background:color-mix(in srgb,var(--text) 22%,var(--border));opacity:0;transition:opacity .25s,width .12s;z-index:60}#th.on{opacity:1}#th:hover,#th.drag{width:5px;background:var(--accent);opacity:1}
@media (hover:none){.pop{display:none}}@media (max-width:640px){main{padding:1.2rem .8rem}#people{flex-direction:column;align-items:stretch;gap:0}.mc{display:contents}#people,#commons,main>footer,#empty{padding-left:0;padding-right:0}#commons .repos{columns:1}.bar{flex-wrap:wrap}.bar .face{order:3;width:100%;border-top:1px solid var(--border);border-right:0;padding:.2rem .8rem;line-height:1.6}.bar .pulse{order:4;width:100%;border-right:0;border-top:1px solid var(--border);padding:.3rem .8rem 0;height:1.6rem}.pulse i{flex:1 1 0}.bar .sp{display:none}.bar .search{flex:1;min-width:0}.search input{width:100%;min-width:0}.search kbd{display:none}.repos li .age{visibility:visible!important}.clone{position:static;transform:none;background:none;padding:0;opacity:.6}.repos.folds::before,.repos.folds::after{right:var(--cw,0)}}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 const k = document.getElementById("kaomoji"); if (k) k.textContent = FACES[Math.floor(Math.random() * FACES.length)]; function copy(b) { const was = b.innerHTML; const done = () => { b.textContent = "♡"; setTimeout(() => (b.innerHTML = was), 1400); }; if (navigator.clipboard) return navigator.clipboard.writeText(b.dataset.clone).then(done); const t = Object.assign(document.createElement("textarea"), { value: b.dataset.clone }); document.body.append(t); t.select(); document.execCommand("copy"); t.remove(); done(); } const main = document.querySelector("main"), people = document.getElementById("people"); const q = document.getElementById("q"); const items = [...document.querySelectorAll("[data-q]")]; const sections = [...people.querySelectorAll(".person")]; function threads() { document.querySelectorAll(".repos").forEach(u => { const vis = [...u.children].filter(l => !l.hidden && getComputedStyle(l).display !== "none"); u.querySelectorAll("li").forEach(l => l.classList.remove("last")); if (vis.length) vis[vis.length - 1].classList.add("last"); }); } function hl(el, term) { el.querySelectorAll("[data-t], .desc").forEach(n => { const s = n.dataset.t || (n.dataset.t = n.textContent); if (!term) { n.textContent = s; return; } const i = s.toLowerCase().indexOf(term); if (i < 0) { n.textContent = s; return; } n.innerHTML = ""; n.append(s.slice(0, i)); const m = document.createElement("mark"); m.textContent = s.slice(i, i + term.length); n.append(m, s.slice(i + term.length)); }); } // the server split the columns by row counts, this one by drawn heights function layout() { const cols = [...people.children]; if (cols.length < 2 || getComputedStyle(cols[0]).display === "contents") return; const heights = cols.map(() => 0); sections.forEach(s => { const i = heights.indexOf(Math.min(...heights)); cols[i].append(s); if (!s.hidden) heights[i] += s.offsetHeight + 30; }); } function apply() { const t = q.value.trim().toLowerCase(); let n = 0; items.forEach(el => { const on = !t || el.dataset.q.includes(t); el.hidden = !on; if (on) { n++; hl(el, t); } }); document.querySelectorAll("[data-group]").forEach(g => { g.hidden = !g.querySelector("[data-q]:not([hidden])"); }); main.classList.toggle("searching", !!t); document.getElementById("empty").hidden = n > 0 || !items.length; threads(); layout(); } q.addEventListener("input", apply); addEventListener("keydown", e => { if (e.key === "/" && !e.target.matches("input, textarea") && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); q.focus(); q.select(); } else if (e.key === "Escape" && document.activeElement === q) { q.value = ""; apply(); q.blur(); } }); main.addEventListener("change", e => { if (e.target.matches(".u-chk")) layout(); }); addEventListener("resize", layout);
const ages = [...document.querySelectorAll(".age[data-at]")]; function age(s) { const h = 3600, d = 24 * h; if (s < h) return Math.max(1, Math.floor(s / 60)) + "m"; if (s < d) return Math.floor(s / h) + "h"; if (s < 7 * d) return Math.floor(s / d) + "d"; if (s < 30 * d) return Math.floor(s / (7 * d)) + "w"; if (s < 365 * d) return Math.floor(s / (30 * d)) + "mo"; return Math.floor(s / (365 * d)) + "y"; } function refreshAges() { const now = Date.now() / 1000; ages.forEach(a => { a.textContent = age(Math.max(0, now - a.dataset.at)); }); } refreshAges(); setInterval(refreshAges, 60000);
// on phones the copy button sits after the age, and the fold stops short of it function fit() { const r = document.querySelector(".repos .row"), a = r && r.querySelector(".age"); if (!a) return; const w = r.getBoundingClientRect().right - a.getBoundingClientRect().right; document.documentElement.style.setProperty("--cw", Math.max(0, w) + "px"); } addEventListener("resize", fit); fit();
(() => { const th = Object.assign(document.createElement("div"), { id: "th" }); document.body.append(th); let fade, drag = null; const se = document.scrollingElement; const place = () => { const ch = innerHeight, sh = se.scrollHeight; if (sh <= ch + 2) { th.style.display = "none"; return; } th.style.display = ""; const h = Math.max(28, ch * ch / sh); th.style.height = h + "px"; th.style.top = 2 + se.scrollTop / (sh - ch) * (ch - h - 4) + "px"; }; const flash = () => { th.classList.add("on"); clearTimeout(fade); fade = setTimeout(() => { if (!drag) th.classList.remove("on"); }, 900); }; addEventListener("scroll", () => { place(); flash(); }); addEventListener("resize", place); new ResizeObserver(place).observe(document.body); th.addEventListener("mousedown", e => { e.preventDefault(); drag = { y: e.clientY, top: se.scrollTop, k: (se.scrollHeight - innerHeight) / (innerHeight - th.offsetHeight - 4) }; th.classList.add("drag", "on"); }); addEventListener("mousemove", e => { if (drag) se.scrollTop = drag.top + (e.clientY - drag.y) * drag.k; else if (e.clientX > innerWidth - 14) flash(); }); addEventListener("mouseup", () => { if (!drag) return; drag = null; th.classList.remove("drag"); flash(); }); place(); })();24 unmodified lines25262728293031323334353637383940414211 unmodified lines545556575658596061626364656667686970717273747576777879134 unmodified linesExpand all21421521621721821922022122222322422522622722822923023123223341 unmodified lines27527627727827928028128228325 unmodified lines30931031131229931331431531631731831932032154 unmodified lines3763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855 unmodified lines59159259359459559659759859960060160260360460560660760860961061138338438538638738838939061261361461561661761861962039262162262339439539639739839940040140240340440540640740840941041141241341441541641762462541962662742142242342442542642742862862963063143043143263263363463543443543643743843963663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967044167167267367467523 unmodified lines6997007017024734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156167037047057067077086206216226236247097106266277117127137147157166326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896907177187197207211 unmodified line24 unmodified lines
mod actor_profile;mod handles;pub mod og;mod pulse;#[cfg(test)]mod tests;
pub use handles::sync_handles;pub(crate) use handles::{ StoredHandles, cached_handles, cached_owner_labels, served_digest, verify_unseen,};use handles::{read_activity, read_descriptions};pub(crate) use pulse::Counted;
pub const KAOMOJI: &[&str] = &[ "(˶ᵔ ᵕ ᵔ˶)", "( ´ ▽ ` )ノ",11 unmodified lines "₍ᐢ.ˬ.ᐢ₎", "(✿◠‿◠)",];
const COPY_SVG: &str = r#"<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M11 5V3.5A1.5 1.5 0 0 0 9.5 2h-6A1.5 1.5 0 0 0 2 3.5v6A1.5 1.5 0 0 0 3.5 11H5"/></svg>"#;const COPY_SVG: &str = r#"<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="5.25" y="5.25" width="8.5" height="8.5"/><path d="M10.75 3V2.25H2.25V10.75H3"/></svg>"#;const STYLE: &str = include_str!("index.css");const SCRIPT: &str = include_str!("index.js");
#[derive(Debug, Clone, PartialEq, Eq)]pub struct PersonSection { pub label: String, pub did: String, pub repos: Vec<ServedRepo>,}
#[derive(Debug, Clone, PartialEq, Eq)]pub struct ServedRepo { pub did: String, pub name: String, pub description: Option<String>, /// unix seconds of the newest commit its public refs name pub active_at: Option<i64>,}
pub fn kaomoji_for(generation_id: &str) -> &'static str { let mut hash: u64 = 0xcbf2_9ce4_8422_2325;134 unmodified linesExpand all kaomoji: kaomoji_for(generation_id), generation: &generation_id[..generation_id.len().min(16)], }, ); let now = chrono::Utc::now(); let pushes = pulse::read(&config.output_root, now.date_naive()); let html = render( &public_url, &manifest.hostname, generation_id, §ions, &page, &Moment { now: now.timestamp(), pushes, }, ); let og_image = og::render(&page.og_title, &page.og_subtitle, &page.og_description)?; let www = build_dir.join("www"); for (name, bytes) in [41 unmodified lines ServedRepo { did: entry.repo_did.clone(), name, description: descriptions.get(&entry.repo_did).cloned(), active_at, }, )); } let mut sections: Vec<(Option<i64>, String, &str, Vec<ServedRepo>)> = by_owner25 unmodified lines .then_with(|| a.2.cmp(b.2)) }); sections .into_iter() .map(|(_, label, _, repos)| PersonSection { label, repos }) .map(|(_, label, did, repos)| PersonSection { label, did: did.to_string(), repos, }) .collect()}
fn escape(text: &str) -> String {54 unmodified lines } out}
/// when the page is drawn: ages count from `now`, and `pushes` fill the header's strip#[derive(Debug, Default, Clone, PartialEq, Eq)]pub struct Moment { /// unix seconds, 0 draws no ages pub now: i64, /// oldest first, empty draws no strip pub pushes: Vec<(chrono::NaiveDate, u32)>,}
/// owners with this many repos or fewer share the commons, once someone has a section of their ownconst COMMONS_MAX: usize = 2;
const HUES: [&str; 5] = ["--accent", "--lav", "--teal", "--amber", "--sky"];
/// svfe's `authorColour`, so a person has one colour on both sites: fnv-1a from one past its/// offset basis, which keeps the owners on the colours the design settledpub fn hue(did: &str) -> &'static str { let mut hash: u32 = 0x811c_9dc6; for byte in did.bytes() { hash ^= u32::from(byte); hash = hash.wrapping_mul(0x0100_0193); } HUES[hash as usize % HUES.len()]}
/// the age a row shows, the page's script recounts it from `data-at`pub fn age(now: i64, at: i64) -> String { const HOUR: i64 = 3600; const DAY: i64 = 24 * HOUR; let secs = (now - at).max(0); let (count, unit) = match secs { s if s < HOUR => ((s / 60).max(1), "m"), s if s < DAY => (s / HOUR, "h"), s if s < 7 * DAY => (s / DAY, "d"), s if s < 30 * DAY => (s / (7 * DAY), "w"), s if s < 365 * DAY => (s / (30 * DAY), "mo"), s => (s / (365 * DAY), "y"), }; format!("{count}{unit}")}
/// which of two columns each section starts in, the shorter one so far taking the next. the/// page's script redoes this as sections unwind or filterfn columns(sections: &[&PersonSection]) -> [Vec<usize>; 2] { let mut columns: [Vec<usize>; 2] = Default::default(); let mut heights = [0; 2]; for (idx, section) in sections.iter().enumerate() { let rows = section.repos.len().min(UNWIND_THRESHOLD); let fold = if section.repos.len() > UNWIND_THRESHOLD { 3 } else { 0 }; let shorter = usize::from(heights[1] < heights[0]); columns[shorter].push(idx); heights[shorter] += 5 + 2 * rows + fold; } columns}
fn render_pushes(pushes: &[(chrono::NaiveDate, u32)]) -> String { if pushes.is_empty() { return String::new(); } let most = pushes .iter() .map(|(_, count)| *count) .max() .unwrap_or(0) .max(1); let mut out = String::from( "<span class=\"pulse\" role=\"img\" aria-label=\"pushes per day, the last 30 days\">", ); for (day, count) in pushes { let when = day.format("%b %-d").to_string().to_lowercase(); let noun = if *count == 1 { "push" } else { "pushes" }; if *count == 0 { out.push_str(&format!("<i class=\"z\" title=\"{when}: 0 pushes\"></i>")); } else { let height = 2 + (u64::from(*count) * 14).div_ceil(u64::from(most)); out.push_str(&format!( "<i style=\"height:{height}px\" title=\"{when}: {count} {noun}\"></i>" )); } } out.push_str("</span>"); out}
/// a label is a handle, or the did when the handle didn't verifyfn handle_mark(label: &str) -> &'static str { if label.starts_with("did:") { "" } else { "@" }}
struct Rows<'a> { links: &'a str, base: &'a str, now: i64,}
impl Rows<'_> { /// `owner` is set in the commons, where each row names whose it is fn row( &self, out: &mut String, label: &str, repo: &ServedRepo, idx: usize, count: usize, owner: Option<&str>, ) { let label_esc = escape(label); let name = escape(&repo.name); let git_url = format!("{}/{}", self.base, escape(&repo.did)); let class = if idx >= UNWIND_THRESHOLD { " class=\"more\"" } else if count > UNWIND_THRESHOLD && idx + 1 == UNWIND_THRESHOLD { " class=\"fold-last\"" } else { "" }; let query = escape(&format!( "{} {} {}", label.to_ascii_lowercase(), repo.name.to_ascii_lowercase(), repo.description .as_deref() .unwrap_or_default() .to_ascii_lowercase() )); let line = owner.map_or(String::new(), |did| { format!(" style=\"--line:var({})\"", hue(did)) }); out.push_str(&format!( "\n <li{class} data-q=\"{query}\"{line}><div class=\"row\">" )); if owner.is_some() { let at = handle_mark(label); out.push_str(&format!("<span class=\"own\">{at}{label_esc}/</span>")); } out.push_str(&format!( "<a href=\"{}/{label_esc}/{name}\" data-t=\"{name}\">{name}</a>", self.links )); if let Some(desc) = &repo.description { out.push_str(&format!("<div class=\"desc\">{}</div>", escape(desc))); } if let Some(at) = repo.active_at.filter(|_| self.now > 0) { out.push_str(&format!( "<span class=\"age\" data-at=\"{at}\">{}</span>", age(self.now, at) )); } if let Some(desc) = repo .description .as_deref() .filter(|desc| desc.chars().count() > POPOVER_CHAR_THRESHOLD) { out.push_str(&format!( "<span class=\"pop\" role=\"tooltip\">{}</span>", escape(desc) )); } out.push_str(&format!( "<button class=\"clone\" type=\"button\" title=\"{git_url}\" aria-label=\"copy git url\" data-clone=\"{git_url}\" onclick=\"copy(this)\">{COPY_SVG}</button></div></li>" )); }
fn fold(out: &mut String, id: &str, count: usize) { if count > UNWIND_THRESHOLD { let rest = count - UNWIND_THRESHOLD; out.push_str(&format!( "\n <label class=\"unwind\" for=\"u-{id}\"><span class=\"a\">{rest} more ⌄</span><span class=\"b\">wind up ⌃</span></label>" )); } }
fn repos_open(out: &mut String, id: &str, count: usize) { if count > UNWIND_THRESHOLD { out.push_str(&format!( "\n <input class=\"u-chk\" type=\"checkbox\" id=\"u-{id}\">" )); } }
fn list_open(out: &mut String, count: usize) { let folds = if count > UNWIND_THRESHOLD { " folds" } else { "" }; out.push_str(&format!("\n <ul class=\"repos{folds}\">")); }}
pub fn render( public_url: &url::Url, hostname: &str, generation_id: &str, sections: &[PersonSection], page: &Page, moment: &Moment,) -> String { let title_esc = escape(&page.title); let description_esc = escape(&page.description); let subtitle_html = render_links(&page.subtitle);5 unmodified lines let links_esc = escape(&page.links); let h1_inner = render_hostname(hostname); let kaomoji = escape(kaomoji_for(generation_id)); let total_repos: usize = sections.iter().map(|s| s.repos.len()).sum(); let pulse_html = render_pushes(&moment.pushes); let rows = Rows { links: &links_esc, base: &base_esc, now: moment.now, };
let (own, shared): (Vec<&PersonSection>, Vec<&PersonSection>) = sections .iter() .partition(|section| section.repos.len() > COMMONS_MAX); let (own, shared) = if own.is_empty() { (shared, Vec::new()) } else { (own, shared) };
let mut people_html = String::new(); for (sec_idx, section) in sections.iter().enumerate() { let owner = escape(§ion.label); let count = section.repos.len(); let has_more = count > UNWIND_THRESHOLD; people_html.push_str(&format!( "\n <section class=\"person\" id=\"{owner}\" data-group>" )); if has_more { for column in columns(&own) { people_html.push_str("\n <div class=\"mc\">"); for idx in column { let section = own[idx]; let owner = escape(§ion.label); let at = handle_mark(§ion.label); let count = section.repos.len(); let id = idx.to_string(); people_html.push_str(&format!( "\n <input class=\"u-chk\" type=\"checkbox\" id=\"u-{sec_idx}\">" "\n <section class=\"person\" id=\"{owner}\" data-group style=\"--line:var({});order:{idx}\">", hue(§ion.did) )); } people_html.push_str(&format!( "\n <header><a href=\"{links_esc}/{owner}\">{owner}</a><span>{count}</span></header>\n <ul class=\"repos\">" )); for (idx, repo) in section.repos.iter().enumerate() { let name = escape(&repo.name); let did = escape(&repo.did); let git_url = format!("{base_esc}/{did}"); let cls = if idx >= UNWIND_THRESHOLD { " class=\"more\"" } else if has_more && idx + 1 == UNWIND_THRESHOLD { " class=\"fold-last\"" } else { "" }; let query = escape(&format!( "{} {} {}", section.label.to_ascii_lowercase(), repo.name.to_ascii_lowercase(), repo.description .as_deref() .unwrap_or_default() .to_ascii_lowercase() )); Rows::repos_open(&mut people_html, &id, count); people_html.push_str(&format!( "\n <li{cls} data-q=\"{query}\"><div class=\"row\"><a href=\"{links_esc}/{owner}/{name}\" data-t=\"{name}\">{name}</a>" "\n <header class=\"ph\"><a href=\"{links_esc}/{owner}\">{at}{owner}</a><span>{count}</span></header>" )); if let Some(desc) = &repo.description { let desc_esc = escape(desc); people_html.push_str(&format!("\n <div class=\"desc\">{desc_esc}</div>")); if desc.chars().count() > POPOVER_CHAR_THRESHOLD { people_html.push_str(&format!( "\n <span class=\"pop\" role=\"tooltip\">{desc_esc}</span>" )); } Rows::list_open(&mut people_html, count); for (row_idx, repo) in section.repos.iter().enumerate() { rows.row(&mut people_html, §ion.label, repo, row_idx, count, None); } people_html.push_str(&format!( "\n <button class=\"clone\" type=\"button\" title=\"{git_url}\" aria-label=\"copy git url\" data-clone=\"{git_url}\" onclick=\"copy(this)\">{COPY_SVG}</button></div></li>" )); people_html.push_str("\n </ul>"); Rows::fold(&mut people_html, &id, count); people_html.push_str("\n </section>"); } people_html.push_str("\n </ul>"); if has_more { let rest = count - UNWIND_THRESHOLD; people_html.push_str(&format!( "\n <label class=\"unwind\" for=\"u-{sec_idx}\"><span class=\"a\">{rest} more ⌄</span><span class=\"b\">wind up ⌃</span></label>" )); people_html.push_str("\n </div>"); }
let mut commons_html = String::new(); if !shared.is_empty() { let mut shared_rows: Vec<(&PersonSection, &ServedRepo)> = shared .iter() .flat_map(|section| section.repos.iter().map(move |repo| (*section, repo))) .collect(); shared_rows.sort_by(|(_, a), (_, b)| b.active_at.cmp(&a.active_at)); let count = shared_rows.len(); let people = if shared.len() == 1 { "person" } else { "people" }; commons_html.push_str("\n <section class=\"person commons\" id=\"commons\" data-group>"); Rows::repos_open(&mut commons_html, "c", count); commons_html.push_str(&format!( "\n <header class=\"ph\"><span>commons</span><span>{} {people}</span></header>", shared.len() )); Rows::list_open(&mut commons_html, count); for (row_idx, (section, repo)) in shared_rows.iter().enumerate() { rows.row( &mut commons_html, §ion.label, repo, row_idx, count, Some(§ion.did), ); } commons_html.push_str("\n </ul>"); Rows::fold(&mut commons_html, "c", count); people_html.push_str("\n </section>"); commons_html.push_str("\n </section>"); }
let faces_js: String = KAOMOJI .iter()23 unmodified lines<meta property="og:image:alt" content="{og_image_alt_esc}"><meta name="twitter:card" content="summary_large_image"><meta name="theme-color" content="{THEME_COLOR_LIGHT}" media="(prefers-color-scheme: light)"><meta name="theme-color" content="{THEME_COLOR_DARK}" media="(prefers-color-scheme: dark)"><style> :root {{ --bg: #fdf7fa; --card: #ffffff; --text: #3b2f36; --muted: #9a8691; --accent: #d9628f; --soft: #fbe3ee; --border: #f1d5e2; color-scheme: light dark; }} @media (prefers-color-scheme: dark) {{ :root {{ --bg: #1b1519; --card: #241c21; --text: #f1e4ea; --muted: #a8919d; --accent: #f28bb5; --soft: #3a2531; --border: #3d2c36; }} }} * {{ box-sizing: border-box; }} [hidden] {{ display: none !important; }} body {{ margin: 0; min-height: 100vh; background: var(--bg); color: var(--text); font: 14.5px/1.55 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; -webkit-font-smoothing: antialiased; }} main {{ width: 100%; max-width: 72rem; margin: 0 auto; padding: 2.6rem clamp(1rem, 4vw, 2.5rem) 2rem; }} .top {{ display: flex; flex-wrap: wrap; align-items: baseline; gap: .35rem .75rem; }} h1 {{ font-size: 1.55rem; margin: 0; letter-spacing: -0.02em; }} h1 span {{ color: var(--accent); }} .face {{ font-size: .88rem; color: var(--muted); opacity: .82; }} .face a {{ color: var(--accent); text-decoration: none; }} .face a:hover {{ text-decoration: underline; }} .search {{ margin-left: auto; display: flex; gap: .4rem; align-items: baseline; font-size: .8rem; color: var(--accent); }} .search input {{ font: inherit; color: var(--text); background: none; border: 0; border-bottom: 1px solid var(--border); outline: none; width: 11rem; padding: .1rem 0; }} .search input:focus {{ border-bottom-color: var(--accent); }} #people {{ columns: 21rem auto; column-gap: 3rem; margin-top: 1.5rem; }} .person {{ break-inside: avoid-column; margin: 0 0 2rem; scroll-margin-top: 1rem; }} .u-chk {{ position: absolute; opacity: 0; pointer-events: none; width: 0; height: 0; }} .person > header {{ display: flex; align-items: baseline; gap: .5rem; }} .person > header a {{ color: var(--text); text-decoration: none; font-weight: bold; overflow-wrap: anywhere; }} .person > header a::before {{ content: "@"; color: var(--accent); }} .person > header a:hover {{ color: var(--accent); }} .person > header span {{ color: var(--muted); font-size: .78rem; }} ul {{ list-style: none; margin: 0; padding: 0; }} .repos {{ margin: .25rem 0 0 .35rem; }} .repos li {{ position: relative; padding: .16rem 0 .16rem 1.5rem; }} .repos li:hover, .repos li:focus-within {{ z-index: 20; }} .repos li::before {{ content: ""; position: absolute; left: .35rem; top: 0; bottom: 0; border-left: 1px solid var(--accent); opacity: .45; }} .repos li:last-child::before, .repos li.last::before, #people:not(.searching) .u-chk:not(:checked) ~ .repos li.fold-last::before {{ bottom: auto; height: calc(.16rem + .8em); }} .repos li::after {{ content: ""; position: absolute; left: .35rem; top: calc(.16rem + .8em); width: .75rem; border-top: 1px solid var(--accent); opacity: .45; }} .repos .row {{ display: flex; align-items: baseline; gap: .42rem; min-width: 0; }} .repos a {{ color: var(--text); text-decoration: none; font-weight: normal; white-space: nowrap; }} .repos a:hover {{ color: var(--accent); }} .repos .desc {{ color: var(--muted); font-size: .8rem; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }} .pop {{ position: absolute; left: 1.5rem; top: calc(100% + .1rem); z-index: 30; max-width: min(26rem, calc(100vw - 3rem)); padding: .42rem .65rem; border-radius: .38rem; background: var(--card); color: var(--text); border: 1px solid var(--border); box-shadow: 0 6px 20px rgba(27, 21, 25, .14); font-size: .78rem; line-height: 1.45; white-space: normal; pointer-events: none; opacity: 0; transform: translateY(-2px); transition: opacity .12s ease, transform .12s ease; }} .repos li:hover .pop, .repos li:focus-within .pop {{ opacity: 1; transform: translateY(0); }} button {{ font: inherit; font-size: .8rem; cursor: pointer; border: 0; padding: 0; background: none; color: var(--muted); }} button:hover {{ color: var(--accent); }} .clone {{ line-height: 1; vertical-align: baseline; opacity: 0; transition: opacity .12s ease; flex-shrink: 0; }} .repos li:hover .clone, .repos li:focus-within .clone {{ opacity: .75; }} .repos li .clone:hover {{ opacity: 1; color: var(--accent); }} .clone svg {{ width: 1.05em; height: 1.05em; vertical-align: -.16em; }} #people:not(.searching) .u-chk:not(:checked) ~ .repos li.more {{ display: none; }} .unwind {{ display: inline-block; cursor: pointer; margin: .18rem 0 0 1.85rem; font-size: .78rem; color: var(--accent); user-select: none; }} .unwind:hover {{ text-decoration: underline; }} .u-chk:checked ~ .unwind .a, .u-chk:not(:checked) ~ .unwind .b {{ display: none; }} #people.searching .unwind {{ display: none; }} mark {{ background: var(--soft); color: var(--accent); border-radius: 2px; }} footer {{ margin-top: 1.2rem; font-size: .78rem; color: var(--muted); display: flex; justify-content: space-between; flex-wrap: wrap; gap: .5rem; }} a {{ color: var(--accent); }} @media (hover: none) {{ .pop {{ display: none; }} }} @media (max-width: 760px) {{ .face {{ order: 3; width: 100%; }} }} @media (max-width: 640px) {{ main {{ padding: 1.8rem 1rem 1.6rem; }} #people {{ columns: 1; margin-top: 1.2rem; }} .search input {{ width: 7.5rem; }} }}</style><noscript><style>.search,.clone{{display:none}}</style></noscript><style>{STYLE}</style><noscript><style>.search,.clone{{display:none}}.repos li:hover .age{{visibility:visible}}</style></noscript><script>document.documentElement.classList.add("js")</script></head><body><main> <header class="top"> <h1>{h1_inner}</h1> <div class="face">「{subtitle_html} <span id="kaomoji">{kaomoji}</span>」</div> <label class="search"><span>/</span><input id="q" type="search" placeholder="{total_repos} repos" autocomplete="off" spellcheck="false" aria-label="filter repos"></label> </header> <header class="bar"><h1 class="host">{h1_inner}</h1><span class="face">「{subtitle_html} <span id="kaomoji">{kaomoji}</span>」</span>{pulse_html}<span class="sp"></span><label class="search"><svg class="mg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="2.25" y="2.25" width="8.5" height="8.5"/><path d="M10.5 10.5L14 14"/></svg><input id="q" type="search" placeholder="filter {total_repos} repos" autocomplete="off" spellcheck="false" aria-label="filter repos"><kbd>/</kbd></label></header> <div id="people">{people_html} </div> <p id="empty" hidden style="color:var(--muted);font-size:.85rem">no matching repos ( ´•︵•` )</p> </div>{commons_html} <p id="empty" hidden>no matching repos ( ´•︵•` )</p> <footer><span>just files, served by nginx ฅ^•ﻌ•^ฅ</span></footer></main><script> const FACES = [{faces_js}]; const k = document.getElementById("kaomoji"); if (k) k.textContent = FACES[Math.floor(Math.random() * FACES.length)]; function copy(b) {{ const was = b.innerHTML; const done = () => {{ b.textContent = "♡"; setTimeout(() => (b.innerHTML = was), 1400); }}; if (navigator.clipboard) return navigator.clipboard.writeText(b.dataset.clone).then(done); const t = Object.assign(document.createElement("textarea"), {{ value: b.dataset.clone }}); document.body.append(t); t.select(); document.execCommand("copy"); t.remove(); done(); }} const q = document.getElementById("q"), sk = document.getElementById("people"); const items = [...document.querySelectorAll("[data-q]")]; function threads() {{ document.querySelectorAll(".person .repos").forEach(u => {{ const vis = [...u.children].filter(l => !l.hidden && getComputedStyle(l).display !== "none"); u.querySelectorAll("li").forEach(l => l.classList.remove("last")); if (vis.length) vis[vis.length - 1].classList.add("last"); }}); }} function hl(el, term) {{ el.querySelectorAll("[data-t], .desc").forEach(n => {{ const s = n.dataset.t || (n.dataset.t = n.textContent); if (!term) {{ n.textContent = s; return; }} const i = s.toLowerCase().indexOf(term); if (i < 0) {{ n.textContent = s; return; }} n.innerHTML = ""; n.append(s.slice(0, i)); const m = document.createElement("mark"); m.textContent = s.slice(i, i + term.length); n.append(m, s.slice(i + term.length)); }}); }} function apply() {{ const t = q.value.trim().toLowerCase(); let n = 0; items.forEach(el => {{ const on = !t || el.dataset.q.includes(t); el.hidden = !on; if (on) {{ n++; hl(el, t); }} }}); document.querySelectorAll("[data-group]").forEach(g => {{ g.hidden = !g.querySelector("[data-q]:not([hidden])"); }}); sk.classList.toggle("searching", !!t); const emp = document.getElementById("empty"); if (emp) emp.hidden = n > 0 || !items.length; threads(); }} if (q) {{ q.addEventListener("input", apply); addEventListener("keydown", e => {{ const tag = e.target && e.target.tagName; if (e.key === "/" && tag !== "INPUT" && tag !== "TEXTAREA") {{ e.preventDefault(); q.focus(); q.select(); }} else if (e.key === "Escape" && document.activeElement === q) {{ q.value = ""; apply(); q.blur(); }} }}); }}</script>{SCRIPT}</script></body></html>"# )1 unmodified line