git-ents.gitmain
⌘K
foforge
commit 5aa890d
style: add design scaffolding

Assisted-by: Claude-Design:claude-opus-4-8

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

design_handoff_git_ents/Git Ents.dc.html @@ -1,0 +1,895 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<script src="./support.js"></script> +</head> +<body> +<x-dc> +<helmet> +<link rel="preconnect" href="https://fonts.googleapis.com"> +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> +<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Lora:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet"> +<style> +:root{ + --color-bg:#faf8f4; --color-surface:#ffffff; --color-text:#2a2518; --color-text-muted:#8a7e6a; + --color-link:#b07d10; --color-link-hover:#96690a; --color-border:#ede9de; --color-code-bg:#f5f3eb; + --color-accent:#b07d10; --color-accent-subtle:#b07d100f; + --shadow-sm:0 1px 3px #0000000d; --shadow-md:0 4px 16px #0000000f; + --radius-sm:10px; --radius-pill:100px; + --kw:#9d0006; --fn:#427b58; --ty:#b57614; --str:#79740e; --num:#8f3f71; --cmt:#9c8f74; --prop:#076678; + --diff-add:#4e9a0622; --diff-rem:#cc241d22; + --font-body:"DM Sans",system-ui,sans-serif; --font-display:"Lora",Georgia,serif; --font-mono:"IBM Plex Mono",ui-monospace,Menlo,monospace; +} +@media (prefers-color-scheme: dark){:root{ + --color-bg:#171510; --color-surface:#211f17; --color-text:#ede8d8; --color-text-muted:#a89e88; + --color-link:#d4a030; --color-link-hover:#e4b850; --color-border:#383324; --color-code-bg:#211f17; + --color-accent:#d4a030; --color-accent-subtle:#d4a03012; + --shadow-sm:0 1px 3px #00000040; --shadow-md:0 4px 16px #0000004d; + --kw:#fb4934; --fn:#8ec07c; --ty:#fabd2f; --str:#b8bb26; --num:#d3869b; --cmt:#928374; --prop:#83a598; + --diff-add:#b8bb2620; --diff-rem:#fb493420; +}} +*{box-sizing:border-box} +html,body{margin:0;padding:0} +body{background:var(--color-bg);color:var(--color-text);font-family:var(--font-body);font-size:17px;line-height:1.7;-webkit-font-smoothing:antialiased;background-image:radial-gradient(58rem 30rem at 50% -10rem, var(--color-accent-subtle), transparent 72%);background-repeat:no-repeat;background-attachment:fixed} +a{color:inherit;text-decoration:none} +::selection{background:var(--color-accent-subtle)} +input,textarea,select,button{font-family:inherit} +@keyframes spin{to{transform:rotate(360deg)}} +@keyframes fadein{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}} +::-webkit-scrollbar{height:9px;width:9px} +::-webkit-scrollbar-thumb{background:var(--color-border);border-radius:100px} +::-webkit-scrollbar-track{background:transparent} +</style> +</helmet> + +<div style="min-height:100vh"> + + <!-- ============ STICKY NAV ============ --> + <header style="position:sticky;top:0;z-index:50;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:color-mix(in srgb, var(--color-bg) 82%, transparent);border-bottom:1px solid var(--color-border)"> + <div style="max-width:78rem;margin:0 auto;padding:0 1.5rem;height:58px;display:flex;align-items:center;gap:18px"> + <a href="#" style="display:flex;align-items:center;gap:9px;font-family:var(--font-mono);font-weight:600;font-size:.95rem;letter-spacing:-.01em;white-space:nowrap;flex:none" style-hover="{ color:'var(--color-accent)' }"> + <svg width="19" height="19" viewBox="0 0 16 16" fill="currentColor" style="color:var(--color-accent)"><path d="M8 1L2 4.2v4.3c0 3.4 2.5 5.6 6 6.5 3.5-.9 6-3.1 6-6.5V4.2L8 1zm0 1.5l4.5 2.4v3.6c0 2.5-1.8 4.2-4.5 5-2.7-.8-4.5-2.5-4.5-5V4.9L8 2.5z"/><circle cx="8" cy="7" r="1.6"/></svg> + git-ents + </a> + <div style="flex:1;max-width:24rem;position:relative;display:flex;align-items:center"> + <svg width="14" height="14" viewBox="0 0 16 16" fill="var(--color-text-muted)" style="position:absolute;left:11px;pointer-events:none"><path d="M10.7 9.6a5 5 0 10-1.1 1.1l3 3 1.1-1.1-3-3zM2.5 6.5a3.5 3.5 0 117 0 3.5 3.5 0 01-7 0z"/></svg> + <input placeholder="Jump to file or symbol" style="width:100%;padding:7px 11px 7px 32px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);font-size:.82rem;color:var(--color-text);outline:none" style-focus="{ borderColor:'var(--color-accent)' }"/> + </div> + <nav style="display:flex;align-items:center;gap:18px;font-size:.85rem;color:var(--color-text-muted)"> + <a href="#" style-hover="{ color:'var(--color-accent)' }">Explore</a> + <a href="#" style-hover="{ color:'var(--color-accent)' }">Docs</a> + <div style="width:30px;height:30px;border-radius:100px;background:var(--color-accent-subtle);border:1px solid var(--color-border);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-size:.72rem;font-weight:600;color:var(--color-accent)">el</div> + </nav> + </div> + </header> + + <!-- ============ REPO HEADER ============ --> + <div style="max-width:78rem;margin:0 auto;padding:0 1.5rem"> + <div style="padding:26px 0 0"> + <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:20px;flex-wrap:wrap"> + <div> + <div style="display:flex;align-items:center;gap:9px;font-family:var(--font-mono);font-size:1.18rem;letter-spacing:-.01em"> + <svg width="18" height="18" viewBox="0 0 16 16" fill="var(--color-text-muted)"><path d="M2 3.5C2 2.7 2.7 2 3.5 2h3l1.5 1.5h4.5c.8 0 1.5.7 1.5 1.5v7c0 .8-.7 1.5-1.5 1.5h-9C2.7 13.5 2 12.8 2 12V3.5z"/></svg> + <a href="#" style="color:var(--color-text-muted)" style-hover="{ color:'var(--color-accent)' }">git-ents</a> + <span style="color:var(--color-border)">/</span> + <a href="#" style="font-weight:600;color:var(--color-accent)">git-ents</a> + <span style="display:inline-flex;align-items:center;gap:5px;margin-left:4px;padding:2px 10px;font-size:.7rem;font-weight:500;color:var(--color-accent);background:var(--color-accent-subtle);border:1px solid var(--color-accent);border-radius:var(--radius-pill)"> + <svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor"><path d="M11.5 3.5a2 2 0 10-2.7 1.9v1.3c0 .6-.5 1.1-1.1 1.1H5.6a2.5 2.5 0 00-1.1.3V5.4a2 2 0 10-1.5 0v5.2a2 2 0 101.6 0V9.5c0-.6.5-1 1-1h2.1A2.6 2.6 0 0010.3 6V5.4a2 2 0 001.2-1.9zM3.75 3.5a.75.75 0 110 1.5.75.75 0 010-1.5zm0 9a.75.75 0 110-1.5.75.75 0 010 1.5zm5.75-9a.75.75 0 110 1.5.75.75 0 010-1.5z"/></svg> + main + </span> + <span style="padding:2px 10px;font-size:.7rem;color:var(--color-text-muted);background:var(--color-code-bg);border:1px solid var(--color-border);border-radius:var(--radius-pill)">Public</span> + </div> + <p style="margin:11px 0 0;max-width:40rem;color:var(--color-text);font-size:.98rem">A warm, editorial Git forge — serve your repositories as paper-grain HTML. Readable, fast, and quiet, with no client-side JavaScript required.</p> + <div style="display:flex;flex-wrap:wrap;gap:7px;margin-top:13px"> + <span style="padding:3px 11px;font-family:var(--font-mono);font-size:.72rem;color:var(--color-link);background:var(--color-accent-subtle);border-radius:var(--radius-pill)">rust</span> + <span style="padding:3px 11px;font-family:var(--font-mono);font-size:.72rem;color:var(--color-link);background:var(--color-accent-subtle);border-radius:var(--radius-pill)">git-forge</span> + <span style="padding:3px 11px;font-family:var(--font-mono);font-size:.72rem;color:var(--color-link);background:var(--color-accent-subtle);border-radius:var(--radius-pill)">self-hosted</span> + <span style="padding:3px 11px;font-family:var(--font-mono);font-size:.72rem;color:var(--color-link);background:var(--color-accent-subtle);border-radius:var(--radius-pill)">html-first</span> + </div> + </div> + <div style="display:flex;gap:9px"> + <button style="display:flex;align-items:center;gap:7px;padding:7px 13px;font-size:.8rem;font-weight:500;color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);cursor:pointer;box-shadow:var(--shadow-sm)" style-hover="{ borderColor:'var(--color-accent)' }"> + <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"/><path d="M8 2C4.5 2 1.7 4.4 1 7.9c.7 3.5 3.5 5.9 7 5.9s6.3-2.4 7-5.9C14.3 4.4 11.5 2 8 2zm0 10c-2.6 0-4.9-1.7-5.6-4.1C3.1 5.4 5.4 3.7 8 3.7s4.9 1.7 5.6 4.2C12.9 10.3 10.6 12 8 12z"/></svg> + Watch + </button> + <button style="display:flex;align-items:center;gap:7px;padding:7px 13px;font-size:.8rem;font-weight:500;color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);cursor:pointer;box-shadow:var(--shadow-sm)" style-hover="{ borderColor:'var(--color-accent)' }"> + <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1.5l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.3 4.2 13.3l.7-4.3L1.8 6l4.3-.6L8 1.5z"/></svg> + Star <span style="font-family:var(--font-mono);color:var(--color-text-muted)">128</span> + </button> + </div> + </div> + + <!-- TAB BAR --> + <div style="display:flex;gap:2px;margin-top:22px;border-bottom:1px solid var(--color-border)"> + <sc-for list="{{ tabs }}" as="t" hint-placeholder-count="6"> + <button onClick="{{ t.onClick }}" style="{{ t.style }}" style-hover="{ color:'var(--color-text)' }"> + <span>{{ t.label }}</span> + <sc-if value="{{ t.showCount }}"><span style="padding:1px 8px;font-family:var(--font-mono);font-size:.68rem;color:var(--color-text-muted);background:var(--color-code-bg);border:1px solid var(--color-border);border-radius:var(--radius-pill)">{{ t.count }}</span></sc-if> + <sc-if value="{{ t.dot }}"><span style="{{ t.dotStyle }}"></span></sc-if> + <sc-if value="{{ t.active }}"><span style="position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--color-accent);border-radius:2px"></span></sc-if> + </button> + </sc-for> + </div> + </div> + + <!-- ============ TAB CONTENT ============ --> + <main style="padding:30px 0 80px"> + + <!-- ===== OVERVIEW ===== --> + <sc-if value="{{ isOverview }}" hint-placeholder-val="{{ true }}"> + <div style="display:grid;grid-template-columns:1fr 19rem;gap:34px;align-items:start"> + <!-- README CARD --> + <article style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);overflow:hidden"> + <div style="display:flex;align-items:center;gap:8px;padding:11px 18px;background:var(--color-code-bg);border-bottom:1px solid var(--color-border);font-family:var(--font-mono);font-size:.72rem;letter-spacing:.06em;text-transform:uppercase;color:var(--color-text-muted)"> + <svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor"><path d="M3 1.5h6L13 5v9.5H3V1.5zm5.5 1H4v11h8V5.7L8.5 2.5z"/></svg> + README.md + </div> + <div style="padding:40px 48px 52px;max-width:44rem"> + <h1 style="font-family:var(--font-display);font-weight:700;font-size:2.4rem;letter-spacing:-.02em;margin:0 0 6px;line-height:1.15">git&#8209;ents</h1> + <p style="font-family:var(--font-display);font-style:italic;font-size:1.18rem;color:var(--color-text-muted);margin:0 0 22px">An editorial Git forge for people who love to read their code.</p> + <div style="display:flex;flex-wrap:wrap;gap:7px;margin-bottom:30px"> + <span style="display:inline-flex;align-items:center;gap:5px;padding:3px 11px;font-family:var(--font-mono);font-size:.7rem;color:var(--fn);background:var(--color-code-bg);border:1px solid var(--color-border);border-radius:var(--radius-pill)"><span style="width:7px;height:7px;border-radius:100px;background:var(--fn)"></span>build passing</span> + <span style="padding:3px 11px;font-family:var(--font-mono);font-size:.7rem;color:var(--ty);background:var(--color-code-bg);border:1px solid var(--color-border);border-radius:var(--radius-pill)">v0.7.0</span> + <span style="padding:3px 11px;font-family:var(--font-mono);font-size:.7rem;color:var(--prop);background:var(--color-code-bg);border:1px solid var(--color-border);border-radius:var(--radius-pill)">MIT</span> + <span style="padding:3px 11px;font-family:var(--font-mono);font-size:.7rem;color:var(--color-text-muted);background:var(--color-code-bg);border:1px solid var(--color-border);border-radius:var(--radius-pill)">rust 1.78+</span> + </div> + <p style="margin:0 0 26px">git-ents renders a repository as quiet, server-side HTML — a README that reads like a page, a file tree you can wander, releases as a changelog, and CI defined entirely with the Git hooks you already know. The whole UI is paper-grain and works without a single byte of client JavaScript.</p> + + <h2 style="font-family:var(--font-display);font-weight:600;font-size:1.4rem;letter-spacing:-.01em;margin:34px 0 4px;position:relative">Why git&#8209;ents</h2> + <div style="width:3rem;height:2px;background:var(--color-accent);margin-bottom:16px"></div> + <ul style="margin:0 0 12px;padding-left:1.2rem"> + <li style="margin-bottom:7px"><strong>Readable by default.</strong> Lora display type, generous measure, warm paper palette — light and dark are equals.</li> + <li style="margin-bottom:7px"><strong>Hooks, not pipelines.</strong> CI is just <code style="font-family:var(--font-mono);font-size:.85em;background:var(--color-code-bg);padding:1px 5px;border-radius:5px">pre-receive</code> and <code style="font-family:var(--font-mono);font-size:.85em;background:var(--color-code-bg);padding:1px 5px;border-radius:5px">post-receive</code> scripts, with run history and logs.</li> + <li style="margin-bottom:7px"><strong>No JavaScript.</strong> Every page is fully server-rendered; the browser just paints.</li> + <li><strong>One binary.</strong> Point it at a directory of bare repos and serve.</li> + </ul> + + <h2 style="font-family:var(--font-display);font-weight:600;font-size:1.4rem;letter-spacing:-.01em;margin:34px 0 4px">Install</h2> + <div style="width:3rem;height:2px;background:var(--color-accent);margin-bottom:16px"></div> + <pre style="margin:0 0 20px;padding:15px 18px;background:var(--color-code-bg);border:1px solid var(--color-border);border-radius:var(--radius-sm);overflow-x:auto;font-family:var(--font-mono);font-size:.82rem;line-height:1.7;color:var(--color-text)"><span style="color:var(--color-text-muted)">$</span> cargo install git-ents +<span style="color:var(--color-text-muted)">$</span> git-ents serve ./repos <span style="color:var(--cmt)"># http://localhost:8420</span></pre> + + <h2 style="font-family:var(--font-display);font-weight:600;font-size:1.4rem;letter-spacing:-.01em;margin:34px 0 4px">A taste of the renderer</h2> + <div style="width:3rem;height:2px;background:var(--color-accent);margin-bottom:16px"></div> + <pre style="margin:0 0 8px;padding:16px 18px;background:var(--color-code-bg);border:1px solid var(--color-border);border-radius:var(--radius-sm);overflow-x:auto;font-family:var(--font-mono);font-size:.82rem;line-height:1.6"><span style="color:var(--cmt)">// render any blob as warm, highlighted HTML</span> +<span style="color:var(--kw)">pub fn</span> <span style="color:var(--fn)">render_blob</span>(blob: &amp;<span style="color:var(--ty)">Blob</span>) -&gt; <span style="color:var(--ty)">Markup</span> { + <span style="color:var(--kw)">let</span> lang = <span style="color:var(--ty)">Language</span>::<span style="color:var(--fn)">guess</span>(&amp;blob.path); + html! { + pre.paper { (<span style="color:var(--fn)">highlight</span>(&amp;blob.text, lang)) } + } +}</pre> + + <h2 style="font-family:var(--font-display);font-weight:600;font-size:1.4rem;letter-spacing:-.01em;margin:34px 0 4px">License</h2> + <div style="width:3rem;height:2px;background:var(--color-accent);margin-bottom:16px"></div> + <p style="margin:0">Released under the MIT License. Grow something with it.</p> + </div> + </article> + + <!-- ASIDE --> + <aside style="display:flex;flex-direction:column;gap:18px;position:sticky;top:78px"> + <!-- Clone --> + <div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);overflow:hidden"> + <div style="padding:10px 15px;font-family:var(--font-mono);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--color-text-muted);border-bottom:1px solid var(--color-border)">Clone</div> + <div style="padding:14px 15px"> + <div style="display:flex;gap:7px"> + <input readonly value="git.ents.dev/git-ents/git-ents.git" style="flex:1;min-width:0;padding:7px 10px;font-family:var(--font-mono);font-size:.74rem;color:var(--color-text);background:var(--color-code-bg);border:1px solid var(--color-border);border-radius:8px;outline:none"/> + <button onClick="{{ onClone }}" title="Copy clone URL" style="flex:none;width:34px;display:flex;align-items:center;justify-content:center;background:var(--color-surface);border:1px solid var(--color-border);border-radius:8px;cursor:pointer;color:var(--color-text-muted)" style-hover="{ borderColor:'var(--color-accent)', color:'var(--color-accent)' }"> + <sc-if value="{{ cloneCopied }}" hint-placeholder-val="{{ false }}"><svg width="14" height="14" viewBox="0 0 16 16" fill="var(--fn)"><path d="M13.5 4.5l-7 7L3 8l1-1 2.5 2.5 6-6 1 1z"/></svg></sc-if> + <sc-if value="{{ notCloneCopied }}" hint-placeholder-val="{{ true }}"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4 2h6.5L13 4.5V11H4V2zm1 1v7h7V5.2L9.8 3H5z"/><path d="M3 5H2v9h8v-1H3V5z"/></svg></sc-if> + </button> + </div> + </div> + </div> + <!-- About --> + <div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm)"> + <div style="padding:10px 15px;font-family:var(--font-mono);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--color-text-muted);border-bottom:1px solid var(--color-border)">About</div> + <div style="padding:14px 15px;font-size:.84rem;color:var(--color-text-muted);display:flex;flex-direction:column;gap:9px"> + <div style="display:flex;align-items:center;gap:8px"><svg width="14" height="14" viewBox="0 0 16 16" fill="var(--ty)"><circle cx="8" cy="8" r="6"/></svg><span>Rust · MIT licensed</span></div> + <div style="display:flex;align-items:center;gap:8px"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1.5l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.3 4.2 13.3l.7-4.3L1.8 6l4.3-.6L8 1.5z"/></svg><span>128 stars · 9 forks</span></div> + <div style="display:flex;align-items:center;gap:8px"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 3.5V8l3 1.8.5-.8L9 7.5V3.5H8z"/><path d="M8 1.5A6.5 6.5 0 108 14.5 6.5 6.5 0 008 1.5zm0 1.5a5 5 0 110 10 5 5 0 010-10z"/></svg><span>Updated 3 hours ago</span></div> + </div> + </div> + <!-- Releases preview --> + <div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm)"> + <div style="display:flex;align-items:center;justify-content:space-between;padding:10px 15px;font-family:var(--font-mono);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--color-text-muted);border-bottom:1px solid var(--color-border)"><span>Releases</span><span style="color:var(--color-accent)">4</span></div> + <div style="padding:14px 15px"> + <div style="display:flex;align-items:center;gap:8px;margin-bottom:3px"><svg width="14" height="14" viewBox="0 0 16 16" fill="var(--color-accent)"><path d="M1.5 8L8 1.5h5.5V7L7 13.5 1.5 8zm10-4a1 1 0 100 2 1 1 0 000-2z"/></svg><span style="font-family:var(--font-mono);font-size:.82rem;font-weight:600;color:var(--color-accent)">v0.7.0</span><span style="padding:1px 7px;font-size:.62rem;color:var(--fn);background:var(--color-code-bg);border:1px solid var(--color-border);border-radius:var(--radius-pill)">Latest</span></div> + <p style="margin:0;font-size:.78rem;color:var(--color-text-muted)">Paper Trail — released 9 days ago</p> + </div> + </div> + <!-- Languages --> + <div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm)"> + <div style="padding:10px 15px;font-family:var(--font-mono);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--color-text-muted);border-bottom:1px solid var(--color-border)">Languages</div> + <div style="padding:14px 15px"> + <div style="display:flex;height:7px;border-radius:100px;overflow:hidden;margin-bottom:11px"><div style="width:86%;background:var(--ty)"></div><div style="width:9%;background:var(--fn)"></div><div style="width:5%;background:var(--prop)"></div></div> + <div style="display:flex;flex-wrap:wrap;gap:11px;font-size:.76rem;color:var(--color-text-muted)"> + <span style="display:flex;align-items:center;gap:5px"><span style="width:8px;height:8px;border-radius:100px;background:var(--ty)"></span>Rust 86%</span> + <span style="display:flex;align-items:center;gap:5px"><span style="width:8px;height:8px;border-radius:100px;background:var(--fn)"></span>HTML 9%</span> + <span style="display:flex;align-items:center;gap:5px"><span style="width:8px;height:8px;border-radius:100px;background:var(--prop)"></span>CSS 5%</span> + </div> + </div> + </div> + </aside> + </div> + </sc-if> + + <!-- ===== FILES ===== --> + <sc-if value="{{ isFiles }}" hint-placeholder-val="{{ false }}"> + <div style="display:grid;grid-template-columns:17rem 1fr;gap:0;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);overflow:hidden;min-height:30rem"> + <!-- TREE --> + <div style="border-right:1px solid var(--color-border);background:var(--color-bg)"> + <div style="display:flex;align-items:center;gap:7px;padding:11px 15px;border-bottom:1px solid var(--color-border);font-family:var(--font-mono);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--color-text-muted)"> + <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M11.5 3.5a2 2 0 10-2.7 1.9v1.3c0 .6-.5 1.1-1.1 1.1H5.6a2.5 2.5 0 00-1.1.3V5.4a2 2 0 10-1.5 0v5.2a2 2 0 101.6 0V9.5c0-.6.5-1 1-1h2.1A2.6 2.6 0 0010.3 6V5.4a2 2 0 001.2-1.9z"/></svg> + main + </div> + <div style="padding:7px 6px"> + <sc-for list="{{ tree }}" as="n1" hint-placeholder-count="7"> + <button onClick="{{ n1.onClick }}" style="{{ n1.style }}" style-hover="{ background:'var(--color-code-bg)' }"> + <sc-if value="{{ n1.isDir }}" hint-placeholder-val="{{ true }}"><span style="{{ n1.chevStyle }}"><svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor"><path d="M6 4l5 4-5 4V4z"/></svg></span><svg width="14" height="14" viewBox="0 0 16 16" fill="var(--color-accent)"><path d="M1.5 4c0-.6.4-1 1-1h3l1.2 1.2H13c.6 0 1 .4 1 1V12c0 .6-.4 1-1 1H2.5c-.6 0-1-.4-1-1V4z"/></svg></sc-if> + <sc-if value="{{ n1.isFile }}" hint-placeholder-val="{{ false }}"><span style="width:11px;flex:none"></span><svg width="13" height="13" viewBox="0 0 16 16" fill="var(--color-text-muted)"><path d="M3 1.5h6L13 5v9.5H3V1.5zm5.5 1H4v11h8V5.7L8.5 2.5z"/></svg></sc-if> + <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ n1.name }}</span> + </button> + <sc-if value="{{ n1.open }}" hint-placeholder-val="{{ false }}"> + <sc-for list="{{ n1.kids }}" as="n2" hint-placeholder-count="3"> + <button onClick="{{ n2.onClick }}" style="{{ n2.style }}" style-hover="{ background:'var(--color-code-bg)' }"> + <sc-if value="{{ n2.isDir }}" hint-placeholder-val="{{ true }}"><span style="{{ n2.chevStyle }}"><svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor"><path d="M6 4l5 4-5 4V4z"/></svg></span><svg width="14" height="14" viewBox="0 0 16 16" fill="var(--color-accent)"><path d="M1.5 4c0-.6.4-1 1-1h3l1.2 1.2H13c.6 0 1 .4 1 1V12c0 .6-.4 1-1 1H2.5c-.6 0-1-.4-1-1V4z"/></svg></sc-if> + <sc-if value="{{ n2.isFile }}" hint-placeholder-val="{{ false }}"><span style="width:11px;flex:none"></span><svg width="13" height="13" viewBox="0 0 16 16" fill="var(--color-text-muted)"><path d="M3 1.5h6L13 5v9.5H3V1.5zm5.5 1H4v11h8V5.7L8.5 2.5z"/></svg></sc-if> + <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ n2.name }}</span> + </button> + <sc-if value="{{ n2.open }}" hint-placeholder-val="{{ false }}"> + <sc-for list="{{ n2.kids }}" as="n3" hint-placeholder-count="3"> + <button onClick="{{ n3.onClick }}" style="{{ n3.style }}" style-hover="{ background:'var(--color-code-bg)' }"> + <span style="width:11px;flex:none"></span><svg width="13" height="13" viewBox="0 0 16 16" fill="var(--color-text-muted)"><path d="M3 1.5h6L13 5v9.5H3V1.5zm5.5 1H4v11h8V5.7L8.5 2.5z"/></svg> + <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ n3.name }}</span> + </button> + </sc-for> + </sc-if> + </sc-for> + </sc-if> + </sc-for> + </div> + </div> + <!-- BLOB --> + <div style="min-width:0;display:flex;flex-direction:column"> + <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;background:var(--color-code-bg);border-bottom:1px solid var(--color-border)"> + <div style="font-family:var(--font-mono);font-size:.78rem;color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ currentPath }}</div> + <div style="display:flex;align-items:center;gap:14px;flex:none"> + <span style="font-family:var(--font-mono);font-size:.7rem;color:var(--color-text-muted)">{{ lineCount }} lines · {{ currentSize }}</span> + <button onClick="{{ onCopyFile }}" style="display:flex;align-items:center;gap:5px;padding:4px 10px;font-size:.72rem;color:var(--color-text-muted);background:var(--color-surface);border:1px solid var(--color-border);border-radius:7px;cursor:pointer" style-hover="{ borderColor:'var(--color-accent)', color:'var(--color-accent)' }"> + <sc-if value="{{ fileCopied }}" hint-placeholder-val="{{ false }}"><svg width="12" height="12" viewBox="0 0 16 16" fill="var(--fn)"><path d="M13.5 4.5l-7 7L3 8l1-1 2.5 2.5 6-6 1 1z"/></svg>Copied</sc-if> + <sc-if value="{{ notFileCopied }}" hint-placeholder-val="{{ true }}"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M4 2h6.5L13 4.5V11H4V2zm1 1v7h7V5.2L9.8 3H5z"/><path d="M3 5H2v9h8v-1H3V5z"/></svg>Copy</sc-if> + </button> + </div> + </div> + <div style="display:grid;grid-template-columns:auto 1fr;overflow:auto;flex:1"> + <div style="padding:14px 0;background:var(--color-code-bg);border-right:1px solid var(--color-border);text-align:right;user-select:none"> + <sc-for list="{{ lines }}" as="ln" hint-placeholder-count="12"> + <div style="padding:0 14px;font-family:var(--font-mono);font-size:.78rem;line-height:1.55;color:var(--color-text-muted);opacity:.6">{{ ln.n }}</div> + </sc-for> + </div> + <div style="padding:14px 0;overflow-x:auto"> + <sc-for list="{{ lines }}" as="ln" hint-placeholder-count="12"> + <div style="padding:0 18px;font-family:var(--font-mono);font-size:.78rem;line-height:1.55;white-space:pre;color:var(--color-text);min-height:1.21em">{{ ln.t }}</div> + </sc-for> + </div> + </div> + </div> + </div> + </sc-if> + + <!-- ===== RELEASES ===== --> + <sc-if value="{{ isReleases }}" hint-placeholder-val="{{ false }}"> + <div style="margin-bottom:24px"> + <h1 style="font-family:var(--font-display);font-weight:700;font-size:1.5rem;letter-spacing:-.01em;margin:0 0 4px">Releases</h1> + <div style="width:3rem;height:2px;background:var(--color-accent)"></div> + </div> + <div style="position:relative;padding-left:30px"> + <div style="position:absolute;left:5px;top:6px;bottom:6px;width:2px;background:var(--color-border)"></div> + <sc-for list="{{ releases }}" as="r" hint-placeholder-count="4"> + <div style="position:relative;margin-bottom:30px"> + <span style="{{ r.dotStyle }}"></span> + <div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);overflow:hidden"> + <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:14px 20px;border-bottom:1px solid var(--color-border)"> + <div style="display:flex;align-items:center;gap:11px"> + <svg width="16" height="16" viewBox="0 0 16 16" fill="var(--color-accent)"><path d="M1.5 8L8 1.5h5.5V7L7 13.5 1.5 8zm10-4a1 1 0 100 2 1 1 0 000-2z"/></svg> + <span style="font-family:var(--font-mono);font-size:1rem;font-weight:600;color:var(--color-accent)">{{ r.tag }}</span> + <span style="font-family:var(--font-display);font-size:1.05rem;color:var(--color-text)">{{ r.name }}</span> + <sc-if value="{{ r.latest }}" hint-placeholder-val="{{ false }}"><span style="padding:2px 9px;font-size:.64rem;font-weight:500;color:var(--fn);background:var(--color-code-bg);border:1px solid var(--color-border);border-radius:var(--radius-pill)">Latest</span></sc-if> + </div> + <span style="font-size:.78rem;color:var(--color-text-muted)">{{ r.date }}</span> + </div> + <div style="padding:18px 20px"> + <sc-for list="{{ r.sections }}" as="s" hint-placeholder-count="2"> + <div style="margin-bottom:13px"> + <div style="font-family:var(--font-mono);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:{{ s.color }};margin-bottom:5px">{{ s.label }}</div> + <ul style="margin:0;padding-left:1.1rem"> + <sc-for list="{{ s.items }}" as="it" hint-placeholder-count="2"> + <li style="font-size:.88rem;margin-bottom:3px">{{ it }}</li> + </sc-for> + </ul> + </div> + </sc-for> + <sc-if value="{{ r.hasDiff }}" hint-placeholder-val="{{ false }}"> + <div style="margin:6px 0 13px;border:1px solid var(--color-border);border-radius:8px;overflow:hidden;font-family:var(--font-mono);font-size:.76rem;line-height:1.6"> + <div style="padding:3px 14px;background:var(--diff-add);color:var(--color-text)">+ render release notes with the Lora display face</div> + <div style="padding:3px 14px;background:var(--diff-rem);color:var(--color-text)">- render release notes with the body sans</div> + </div> + </sc-if> + <div style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-top:14px;padding-top:13px;border-top:1px solid var(--color-border)"> + <sc-for list="{{ r.assets }}" as="a" hint-placeholder-count="2"> + <a href="#" style="display:flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:.74rem;color:var(--color-link)" style-hover="{ color:'var(--color-link-hover)' }"> + <svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1.5v7.8l2.5-2.5 1 1L8 12.3 4.5 8.8l1-1L8 9.3V1.5H8zM3 13h10v1.2H3V13z"/></svg> + {{ a.name }} <span style="color:var(--color-text-muted)">{{ a.size }}</span> + </a> + </sc-for> + <span style="display:flex;align-items:center;gap:6px;margin-left:auto;font-family:var(--font-mono);font-size:.72rem;color:var(--color-text-muted)"> + <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><circle cx="5" cy="8" r="2"/><circle cx="11" cy="8" r="2" fill="none" stroke="currentColor" stroke-width="1.4"/></svg> + {{ r.sha }} + </span> + </div> + </div> + </div> + </div> + </sc-for> + </div> + </sc-if> + + <!-- ===== HOOKS ===== --> + <sc-if value="{{ isHooks }}" hint-placeholder-val="{{ false }}"> + <div style="margin-bottom:22px"> + <h1 style="font-family:var(--font-display);font-weight:700;font-size:1.5rem;letter-spacing:-.01em;margin:0 0 4px">Hooks</h1> + <div style="width:3rem;height:2px;background:var(--color-accent)"></div> + <p style="margin:12px 0 0;max-width:38rem;font-size:.9rem;color:var(--color-text-muted)">Continuous integration defined as plain Git hooks. Each push runs the stages in <code style="font-family:var(--font-mono);font-size:.85em;background:var(--color-code-bg);padding:1px 5px;border-radius:5px">.gitents/hooks.toml</code> — no YAML, no marketplace.</p> + </div> + <div style="display:grid;grid-template-columns:20rem 1fr;gap:20px;align-items:start"> + <!-- run list --> + <div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);overflow:hidden"> + <div style="padding:10px 15px;font-family:var(--font-mono);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--color-text-muted);border-bottom:1px solid var(--color-border)">Recent runs</div> + <sc-for list="{{ runs }}" as="run" hint-placeholder-count="4"> + <button onClick="{{ run.onClick }}" style="{{ run.style }}" style-hover="{ background:'var(--color-code-bg)' }"> + <span style="{{ run.iconWrap }}"> + <sc-if value="{{ run.isRunning }}" hint-placeholder-val="{{ false }}"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--ty)" stroke-width="1.6" style="animation:spin 1s linear infinite"><path d="M8 2a6 6 0 106 6" stroke-linecap="round"/></svg></sc-if> + <sc-if value="{{ run.isPassed }}" hint-placeholder-val="{{ true }}"><svg width="13" height="13" viewBox="0 0 16 16" fill="var(--fn)"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM7 10.4L4.3 7.7l1-1L7 8.4l3.7-3.7 1 1L7 10.4z"/></svg></sc-if> + <sc-if value="{{ run.isFailed }}" hint-placeholder-val="{{ false }}"><svg width="13" height="13" viewBox="0 0 16 16" fill="var(--kw)"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zm2.5 8.4l-1 1L8 9l-1.5 1.5-1-1L7 8 5.5 6.5l1-1L8 7l1.5-1.5 1 1L9 8l1.5 1.5z"/></svg></sc-if> + </span> + <span style="min-width:0;flex:1;text-align:left"> + <span style="display:block;font-size:.82rem;color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ run.subject }}</span> + <span style="display:block;font-family:var(--font-mono);font-size:.68rem;color:var(--color-text-muted);margin-top:1px">#{{ run.id }} · {{ run.branch }} · {{ run.trigger }}</span> + </span> + <span style="flex:none;font-family:var(--font-mono);font-size:.66rem;color:var(--color-text-muted);text-align:right">{{ run.age }}</span> + </button> + </sc-for> + </div> + <!-- run detail --> + <div style="display:flex;flex-direction:column;gap:18px;min-width:0"> + <div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);overflow:hidden"> + <div style="display:flex;align-items:center;gap:11px;flex-wrap:wrap;padding:15px 20px;border-bottom:1px solid var(--color-border)"> + <span style="{{ cur.iconWrap }}"> + <sc-if value="{{ cur.isRunning }}" hint-placeholder-val="{{ false }}"><svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--ty)" stroke-width="1.6" style="animation:spin 1s linear infinite"><path d="M8 2a6 6 0 106 6" stroke-linecap="round"/></svg></sc-if> + <sc-if value="{{ cur.isPassed }}" hint-placeholder-val="{{ true }}"><svg width="15" height="15" viewBox="0 0 16 16" fill="var(--fn)"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM7 10.4L4.3 7.7l1-1L7 8.4l3.7-3.7 1 1L7 10.4z"/></svg></sc-if> + <sc-if value="{{ cur.isFailed }}" hint-placeholder-val="{{ false }}"><svg width="15" height="15" viewBox="0 0 16 16" fill="var(--kw)"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zm2.5 8.4l-1 1L8 9l-1.5 1.5-1-1L7 8 5.5 6.5l1-1L8 7l1.5-1.5 1 1L9 8l1.5 1.5z"/></svg></sc-if> + </span> + <span style="font-family:var(--font-display);font-size:1.1rem;color:var(--color-text)">{{ cur.subject }}</span> + <span style="font-family:var(--font-mono);font-size:.72rem;color:var(--color-text-muted);margin-left:auto">{{ cur.sha }} · {{ cur.duration }}</span> + </div> + <!-- stages --> + <div style="display:flex;flex-wrap:wrap;gap:8px;padding:15px 20px;border-bottom:1px solid var(--color-border)"> + <sc-for list="{{ cur.stages }}" as="st" hint-placeholder-count="4"> + <div style="display:flex;align-items:center;gap:7px;padding:6px 12px;background:var(--color-code-bg);border:1px solid var(--color-border);border-radius:var(--radius-pill)"> + <span style="width:8px;height:8px;border-radius:100px;background:{{ st.color }}"></span> + <span style="font-family:var(--font-mono);font-size:.74rem;color:var(--color-text)">{{ st.name }}</span> + <span style="font-family:var(--font-mono);font-size:.68rem;color:var(--color-text-muted)">{{ st.duration }}</span> + </div> + </sc-for> + </div> + <!-- log --> + <div style="background:var(--color-code-bg);padding:14px 0;overflow-x:auto"> + <sc-for list="{{ cur.log }}" as="lg" hint-placeholder-count="6"> + <div style="display:flex;gap:14px;padding:0 18px;font-family:var(--font-mono);font-size:.76rem;line-height:1.7;white-space:pre"> + <span style="color:var(--color-text-muted);opacity:.5;user-select:none;min-width:1.4rem;text-align:right">{{ lg.n }}</span> + <span style="color:{{ lg.color }}">{{ lg.text }}</span> + </div> + </sc-for> + </div> + </div> + <!-- config --> + <div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);overflow:hidden"> + <div style="display:flex;align-items:center;gap:8px;padding:10px 16px;background:var(--color-code-bg);border-bottom:1px solid var(--color-border);font-family:var(--font-mono);font-size:.72rem;letter-spacing:.06em;text-transform:uppercase;color:var(--color-text-muted)"> + <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M3 1.5h6L13 5v9.5H3V1.5zm5.5 1H4v11h8V5.7L8.5 2.5z"/></svg>.gitents/hooks.toml + </div> + <div style="padding:14px 0;overflow-x:auto"> + <sc-for list="{{ hooksToml }}" as="hl" hint-placeholder-count="10"> + <div style="padding:0 18px;font-family:var(--font-mono);font-size:.78rem;line-height:1.6;white-space:pre;color:{{ hl.color }};min-height:1.25em">{{ hl.t }}</div> + </sc-for> + </div> + </div> + </div> + </div> + </sc-if> + + <!-- ===== ISSUES ===== --> + <sc-if value="{{ isIssues }}" hint-placeholder-val="{{ false }}"> + <div style="display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:18px"> + <div> + <h1 style="font-family:var(--font-display);font-weight:700;font-size:1.5rem;letter-spacing:-.01em;margin:0 0 4px;white-space:nowrap">Bug reports</h1> + <div style="width:3rem;height:2px;background:var(--color-accent)"></div> + </div> + <button style="display:flex;align-items:center;gap:7px;padding:8px 15px;font-size:.82rem;font-weight:600;color:var(--color-bg);background:var(--color-accent);border:none;border-radius:var(--radius-sm);cursor:pointer" style-hover="{ background:'var(--color-link-hover)' }"> + <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M7.25 3.5h1.5v3.75H12.5v1.5H8.75V12.5h-1.5V8.75H3.5v-1.5h3.75V3.5z"/></svg>New issue + </button> + </div> + <div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:16px"> + <div style="flex:1;min-width:14rem;position:relative;display:flex;align-items:center"> + <svg width="14" height="14" viewBox="0 0 16 16" fill="var(--color-text-muted)" style="position:absolute;left:12px;pointer-events:none"><path d="M10.7 9.6a5 5 0 10-1.1 1.1l3 3 1.1-1.1-3-3zM2.5 6.5a3.5 3.5 0 117 0 3.5 3.5 0 01-7 0z"/></svg> + <input value="{{ iq }}" onInput="{{ onSearch }}" placeholder="Search bug reports" style="width:100%;padding:8px 12px 8px 34px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);font-size:.84rem;color:var(--color-text);outline:none" style-focus="{ borderColor:'var(--color-accent)' }"/> + </div> + <div style="display:flex;gap:6px"> + <sc-for list="{{ labelChips }}" as="lc" hint-placeholder-count="4"> + <button onClick="{{ lc.onClick }}" style="{{ lc.style }}">{{ lc.label }}</button> + </sc-for> + </div> + </div> + <div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);overflow:hidden"> + <div style="display:flex;gap:20px;padding:11px 18px;border-bottom:1px solid var(--color-border);background:var(--color-code-bg)"> + <sc-for list="{{ issueTabs }}" as="ft" hint-placeholder-count="2"> + <button onClick="{{ ft.onClick }}" style="{{ ft.style }}"> + <sc-if value="{{ ft.isOpen }}" hint-placeholder-val="{{ true }}"><svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zm0 1.5a5 5 0 110 10 5 5 0 010-10zm0 2.5a2.5 2.5 0 100 5 2.5 2.5 0 000-5z"/></svg></sc-if> + <sc-if value="{{ ft.isClosed }}" hint-placeholder-val="{{ false }}"><svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM7 10.4L4.3 7.7l1-1L7 8.4l3.7-3.7 1 1L7 10.4z"/></svg></sc-if> + {{ ft.label }} <span style="color:var(--color-text-muted)">{{ ft.count }}</span> + </button> + </sc-for> + </div> + <sc-for list="{{ issues }}" as="iss" hint-placeholder-count="4"> + <div style="display:flex;align-items:flex-start;gap:12px;padding:14px 18px;border-bottom:1px solid var(--color-border)" style-hover="{ background:'var(--color-code-bg)' }"> + <span style="flex:none;margin-top:2px;color:{{ iss.statusColor }}"> + <sc-if value="{{ iss.isOpen }}" hint-placeholder-val="{{ true }}"><svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zm0 1.5a5 5 0 110 10 5 5 0 010-10zm0 2.5a2.5 2.5 0 100 5 2.5 2.5 0 000-5z"/></svg></sc-if> + <sc-if value="{{ iss.isClosed }}" hint-placeholder-val="{{ false }}"><svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM7 10.4L4.3 7.7l1-1L7 8.4l3.7-3.7 1 1L7 10.4z"/></svg></sc-if> + </span> + <div style="min-width:0;flex:1"> + <div style="display:flex;align-items:center;gap:9px;flex-wrap:wrap"> + <a href="#" style="font-size:.96rem;font-weight:500;color:var(--color-text)" style-hover="{ color:'var(--color-accent)' }">{{ iss.title }}</a> + <sc-for list="{{ iss.labels }}" as="lb" hint-placeholder-count="1"> + <span style="padding:1px 9px;font-family:var(--font-mono);font-size:.66rem;color:{{ lb.color }};background:var(--color-code-bg);border:1px solid var(--color-border);border-radius:var(--radius-pill)">{{ lb.name }}</span> + </sc-for> + </div> + <div style="font-size:.76rem;color:var(--color-text-muted);margin-top:3px">#{{ iss.num }} {{ iss.meta }}</div> + </div> + <sc-if value="{{ iss.hasComments }}" hint-placeholder-val="{{ false }}"> + <span style="flex:none;display:flex;align-items:center;gap:5px;font-size:.76rem;color:var(--color-text-muted)"> + <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M2 3c0-.6.4-1 1-1h10c.6 0 1 .4 1 1v7c0 .6-.4 1-1 1H6l-3 2.5V11H3c-.6 0-1-.4-1-1V3z"/></svg>{{ iss.comments }} + </span> + </sc-if> + </div> + </sc-for> + <sc-if value="{{ noIssues }}" hint-placeholder-val="{{ false }}"> + <div style="padding:40px;text-align:center;color:var(--color-text-muted);font-size:.88rem">No bug reports match these filters.</div> + </sc-if> + </div> + </sc-if> + + <!-- ===== SETTINGS ===== --> + <sc-if value="{{ isSettings }}" hint-placeholder-val="{{ false }}"> + <div style="max-width:46rem"> + <div style="margin-bottom:22px"> + <h1 style="font-family:var(--font-display);font-weight:700;font-size:1.5rem;letter-spacing:-.01em;margin:0 0 4px">Repository settings</h1> + <div style="width:3rem;height:2px;background:var(--color-accent)"></div> + </div> + <!-- General --> + <div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);margin-bottom:20px"> + <div style="padding:11px 18px;font-family:var(--font-mono);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--color-text-muted);border-bottom:1px solid var(--color-border)">General</div> + <div style="padding:20px 18px;display:flex;flex-direction:column;gap:17px"> + <label style="display:block"> + <span style="display:block;font-size:.8rem;font-weight:500;margin-bottom:6px">Repository name</span> + <input value="git-ents" style="width:100%;padding:9px 12px;font-family:var(--font-mono);font-size:.85rem;color:var(--color-text);background:var(--color-bg);border:1px solid var(--color-border);border-radius:8px;outline:none" style-focus="{ borderColor:'var(--color-accent)' }"/> + </label> + <label style="display:block"> + <span style="display:block;font-size:.8rem;font-weight:500;margin-bottom:6px">Description</span> + <textarea rows="2" style="width:100%;padding:9px 12px;font-size:.85rem;color:var(--color-text);background:var(--color-bg);border:1px solid var(--color-border);border-radius:8px;outline:none;resize:vertical;font-family:var(--font-body)" style-focus="{ borderColor:'var(--color-accent)' }">A warm, editorial Git forge — serve your repositories as paper-grain HTML.</textarea> + </label> + <label style="display:block"> + <span style="display:block;font-size:.8rem;font-weight:500;margin-bottom:6px">Default branch</span> + <select style="width:11rem;padding:9px 12px;font-family:var(--font-mono);font-size:.85rem;color:var(--color-text);background:var(--color-bg);border:1px solid var(--color-border);border-radius:8px;outline:none"> + <option>main</option><option>develop</option><option>trunk</option> + </select> + </label> + </div> + </div> + <!-- Features --> + <div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);margin-bottom:20px"> + <div style="padding:11px 18px;font-family:var(--font-mono);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--color-text-muted);border-bottom:1px solid var(--color-border)">Features</div> + <div> + <sc-for list="{{ features }}" as="f" hint-placeholder-count="4"> + <div style="display:flex;align-items:center;justify-content:space-between;gap:14px;padding:14px 18px;border-bottom:1px solid var(--color-border)"> + <div> + <div style="font-size:.9rem;font-weight:500">{{ f.label }}</div> + <div style="font-size:.76rem;color:var(--color-text-muted)">{{ f.desc }}</div> + </div> + <button onClick="{{ f.onClick }}" style="{{ f.trackStyle }}"><span style="{{ f.knobStyle }}"></span></button> + </div> + </sc-for> + </div> + </div> + <!-- Visibility --> + <div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);margin-bottom:20px"> + <div style="padding:11px 18px;font-family:var(--font-mono);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--color-text-muted);border-bottom:1px solid var(--color-border)">Visibility</div> + <div style="padding:14px 18px;display:flex;flex-direction:column;gap:10px"> + <sc-for list="{{ visOptions }}" as="v" hint-placeholder-count="2"> + <button onClick="{{ v.onClick }}" style="{{ v.style }}"> + <span style="{{ v.radioStyle }}"><sc-if value="{{ v.on }}" hint-placeholder-val="{{ true }}"><span style="width:8px;height:8px;border-radius:100px;background:var(--color-accent)"></span></sc-if></span> + <span style="text-align:left"><span style="display:block;font-size:.86rem;font-weight:500;color:var(--color-text)">{{ v.label }}</span><span style="display:block;font-size:.76rem;color:var(--color-text-muted)">{{ v.desc }}</span></span> + </button> + </sc-for> + </div> + </div> + <!-- Danger zone --> + <div style="background:var(--color-surface);border:1px solid var(--kw);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);overflow:hidden"> + <div style="padding:11px 18px;font-family:var(--font-mono);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--kw);border-bottom:1px solid var(--color-border)">Danger zone</div> + <div style="padding:6px 18px"> + <div style="display:flex;align-items:center;justify-content:space-between;gap:14px;padding:14px 0;border-bottom:1px solid var(--color-border)"> + <div><div style="font-size:.88rem;font-weight:500">Archive this repository</div><div style="font-size:.76rem;color:var(--color-text-muted)">Mark read-only. Hooks stop running.</div></div> + <button style="padding:6px 14px;font-size:.78rem;font-weight:500;color:var(--kw);background:transparent;border:1px solid var(--kw);border-radius:8px;cursor:pointer" style-hover="{ background:'var(--diff-rem)' }">Archive</button> + </div> + <div style="display:flex;align-items:center;justify-content:space-between;gap:14px;padding:14px 0"> + <div><div style="font-size:.88rem;font-weight:500">Delete this repository</div><div style="font-size:.76rem;color:var(--color-text-muted)">This cannot be undone.</div></div> + <button style="padding:6px 14px;font-size:.78rem;font-weight:600;color:#fff;background:var(--kw);border:1px solid var(--kw);border-radius:8px;cursor:pointer" style-hover="{ opacity:'.9' }">Delete</button> + </div> + </div> + </div> + </div> + </sc-if> + + </main> + </div> + + <!-- ============ FOOTER ============ --> + <footer style="border-top:1px solid var(--color-border);margin-top:20px"> + <div style="max-width:78rem;margin:0 auto;padding:24px 1.5rem;text-align:center;font-family:var(--font-mono);font-size:.72rem;color:var(--color-text-muted)"> + git-ents · served as paper-grain HTML · no JavaScript required + </div> + </footer> +</div> +</x-dc> +<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:&quot;100%&quot;},&quot;defaultTab&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;options&quot;:[&quot;overview&quot;,&quot;files&quot;,&quot;releases&quot;,&quot;hooks&quot;,&quot;issues&quot;,&quot;settings&quot;],&quot;default&quot;:&quot;overview&quot;,&quot;tsType&quot;:&quot;string&quot;},&quot;collapseTree&quot;:{&quot;editor&quot;:&quot;boolean&quot;,&quot;default&quot;:false,&quot;tsType&quot;:&quot;boolean&quot;}}"> +class Component extends DCLogic { + state = { + tab: 'overview', + exp: { 'src': true, 'src/forge': true, 'src/hooks': false, 'templates': false, 'static': false, '.gitents': true }, + file: 'Cargo.toml', + run: 142, + itab: 'open', + ilabel: 'all', + iq: '', + copied: '', + features: { issues: true, releases: true, hooks: true, wiki: false }, + visibility: 'public', + }; + + constructor(props) { + super(props); + const valid = ['overview', 'files', 'releases', 'hooks', 'issues', 'settings']; + if (props && valid.includes(props.defaultTab)) this.state.tab = props.defaultTab; + if (props && props.collapseTree) this.state.exp = { 'src': false, 'src/forge': false, 'src/hooks': false, 'templates': false, 'static': false, '.gitents': false }; + } + + componentWillUnmount() { clearTimeout(this._t); } + + copy(text, key) { + try { if (navigator.clipboard) navigator.clipboard.writeText(text); } catch (e) {} + this.setState({ copied: key }); + clearTimeout(this._t); + this._t = setTimeout(() => this.setState(s => (s.copied === key ? { copied: '' } : null)), 1400); + } + + renderVals() { + const accent = 'var(--color-accent)', muted = 'var(--color-text-muted)', text = 'var(--color-text)'; + const tab = this.state.tab; + + // ---------- TABS ---------- + const tabBtn = (active) => ({ + position: 'relative', display: 'flex', alignItems: 'center', gap: '8px', + padding: '10px 14px', background: 'none', border: 'none', cursor: 'pointer', + fontSize: '.88rem', fontWeight: active ? 600 : 500, + color: active ? text : muted, fontFamily: 'var(--font-body)', + }); + const tabDefs = [ + { id: 'overview', label: 'Overview' }, + { id: 'files', label: 'Files' }, + { id: 'releases', label: 'Releases', count: 4 }, + { id: 'hooks', label: 'Hooks', dot: true }, + { id: 'issues', label: 'Issues', count: 3 }, + { id: 'settings', label: 'Settings' }, + ]; + const tabs = tabDefs.map(t => ({ + label: t.label, active: tab === t.id, + showCount: t.count != null, count: t.count, + dot: !!t.dot, + dotStyle: { width: '7px', height: '7px', borderRadius: '100px', background: 'var(--fn)' }, + onClick: () => this.setState({ tab: t.id }), + style: tabBtn(tab === t.id), + })); + + // ---------- FILE TREE ---------- + const T = [ + { t: 'dir', name: '.gitents', path: '.gitents', children: [ + { t: 'file', name: 'hooks.toml', path: '.gitents/hooks.toml' }, + ]}, + { t: 'dir', name: 'src', path: 'src', children: [ + { t: 'file', name: 'main.rs', path: 'src/main.rs' }, + { t: 'dir', name: 'forge', path: 'src/forge', children: [ + { t: 'file', name: 'mod.rs', path: 'src/forge/mod.rs' }, + { t: 'file', name: 'repo.rs', path: 'src/forge/repo.rs' }, + { t: 'file', name: 'render.rs', path: 'src/forge/render.rs' }, + { t: 'file', name: 'blob.rs', path: 'src/forge/blob.rs' }, + ]}, + { t: 'dir', name: 'hooks', path: 'src/hooks', children: [ + { t: 'file', name: 'mod.rs', path: 'src/hooks/mod.rs' }, + { t: 'file', name: 'runner.rs', path: 'src/hooks/runner.rs' }, + ]}, + ]}, + { t: 'dir', name: 'templates', path: 'templates', children: [ + { t: 'file', name: 'repo.html', path: 'templates/repo.html' }, + { t: 'file', name: 'blob.html', path: 'templates/blob.html' }, + ]}, + { t: 'dir', name: 'static', path: 'static', children: [ + { t: 'file', name: 'paper.css', path: 'static/paper.css' }, + ]}, + { t: 'file', name: 'Cargo.toml', path: 'Cargo.toml' }, + { t: 'file', name: 'LICENSE', path: 'LICENSE' }, + { t: 'file', name: 'README.md', path: 'README.md' }, + ]; + const rowBase = (pad, active) => ({ + display: 'flex', alignItems: 'center', gap: '7px', width: '100%', + padding: '4px 8px', paddingLeft: pad + 'px', border: 'none', + background: active ? 'var(--color-accent-subtle)' : 'transparent', + color: active ? accent : text, cursor: 'pointer', textAlign: 'left', + fontFamily: 'var(--font-mono)', fontSize: '.79rem', borderRadius: '7px', + fontWeight: active ? 600 : 400, + }); + const dec = (n, depth) => { + const pad = 8 + depth * 15; + if (n.t === 'dir') { + const open = !!this.state.exp[n.path]; + return { + key: n.path, name: n.name, isDir: true, isFile: false, open, + chevStyle: { display: 'inline-flex', transition: 'transform .15s', transform: open ? 'rotate(90deg)' : 'none', color: muted, flex: 'none' }, + onClick: () => this.setState(s => ({ exp: { ...s.exp, [n.path]: !s.exp[n.path] } })), + style: rowBase(pad, false), + kids: (n.children || []).map(c => dec(c, depth + 1)), + }; + } + const active = this.state.file === n.path; + return { + key: n.path, name: n.name, isDir: false, isFile: true, open: false, active, + onClick: () => this.setState({ file: n.path }), + style: rowBase(pad, active), kids: [], + }; + }; + const tree = T.map(n => dec(n, 0)); + + // ---------- FILE CONTENTS ---------- + const FILES = { + 'Cargo.toml': '[package]\nname = "git-ents"\nversion = "0.7.0"\nedition = "2021"\nlicense = "MIT"\ndescription = "A warm, editorial Git forge."\nrepository = "https://git.ents.dev/git-ents/git-ents"\n\n[dependencies]\naxum = "0.7"\nmaud = "0.26"\ngix = "0.63"\nsyntect = "5.2"\ntoml = "0.8"\npulldown-cmark = "0.11"', + 'README.md': '# git-ents\n\nAn editorial Git forge for people who love to read their code.\n\ngit-ents renders a repository as quiet, server-side HTML. The whole UI is\npaper-grain and works without a single byte of client JavaScript.\n\n## Why git-ents\n\n- Readable by default.\n- Hooks, not pipelines.\n- No JavaScript.\n- One binary.\n\n## Install\n\n cargo install git-ents\n git-ents serve ./repos\n\n## License\n\nMIT.', + 'LICENSE': 'MIT License\n\nCopyright (c) 2026 The Git Ents\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so.', + 'src/main.rs': 'use axum::Router;\n\nmod forge;\nmod hooks;\n\n#[tokio::main]\nasync fn main() {\n let app = Router::new()\n .merge(forge::routes())\n .merge(hooks::routes());\n\n let addr = "0.0.0.0:8420";\n println!("git-ents serving on {addr}");\n axum::serve(listen(addr).await, app).await.unwrap();\n}', + 'src/forge/mod.rs': 'use axum::{Router, routing::get};\n\nmod repo;\nmod render;\nmod blob;\n\npub fn routes() -> Router {\n Router::new()\n .route("/:owner/:repo", get(repo::overview))\n .route("/:owner/:repo/tree/*path", get(blob::tree))\n}', + 'src/forge/repo.rs': 'use crate::render::Markup;\n\npub struct Repo {\n pub owner: String,\n pub name: String,\n pub head: String,\n}\n\nimpl Repo {\n pub fn open(path: &str) -> Result<Self, Error> {\n let git = gix::open(path)?;\n Ok(Self::from(git))\n }\n}', + 'src/forge/render.rs': '// turn a markdown README into warm, paper-grain HTML\nuse pulldown_cmark::{Parser, html};\n\npub fn readme(src: &str) -> String {\n let parser = Parser::new(src);\n let mut out = String::new();\n html::push_html(&mut out, parser);\n out\n}', + 'src/forge/blob.rs': 'use syntect::easy::HighlightLines;\n\npub fn highlight(text: &str, lang: Language) -> Markup {\n let mut hl = HighlightLines::new(lang.syntax(), &THEME);\n for line in text.lines() {\n let spans = hl.highlight_line(line, &SET).unwrap();\n emit(spans);\n }\n}', + 'src/hooks/mod.rs': 'mod runner;\n\npub use runner::Runner;\n\n// CI is just pre-receive + post-receive scripts\npub fn routes() -> axum::Router {\n axum::Router::new()\n .route("/:owner/:repo/hooks", get(list_runs))\n}', + 'src/hooks/runner.rs': 'use std::process::Command;\n\npub struct Runner {\n pub stages: Vec<Stage>,\n}\n\nimpl Runner {\n pub fn run(&self, push: &Push) -> RunResult {\n for stage in &self.stages {\n let ok = stage.exec(push)?;\n if !ok { return RunResult::Failed; }\n }\n RunResult::Passed\n }\n}', + 'templates/repo.html': '<article class="readme">\n {{ readme | safe }}\n</article>\n<aside class="meta">\n <section class="clone">{{ clone_url }}</section>\n</aside>', + 'templates/blob.html': '<div class="blob">\n <div class="gutter">{{ line_numbers }}</div>\n <pre class="code">{{ highlighted }}</pre>\n</div>', + 'static/paper.css': ':root {\n --paper: #faf8f4;\n --ink: #2a2518;\n}\nbody {\n background: var(--paper);\n color: var(--ink);\n font-family: "DM Sans", sans-serif;\n}\n.blob { font-family: "IBM Plex Mono", monospace; }', + }; + const SIZES = { + 'Cargo.toml': '412 B', 'README.md': '688 B', 'LICENSE': '1.1 KB', + 'src/main.rs': '386 B', 'src/forge/mod.rs': '298 B', 'src/forge/repo.rs': '274 B', + 'src/forge/render.rs': '256 B', 'src/forge/blob.rs': '288 B', + 'src/hooks/mod.rs': '241 B', 'src/hooks/runner.rs': '352 B', + 'templates/repo.html': '178 B', 'templates/blob.html': '162 B', 'static/paper.css': '224 B', + }; + const content = FILES[this.state.file] || ''; + const lines = content.split('\n').map((t, i) => ({ n: i + 1, t: t === '' ? ' ' : t })); + + // ---------- RELEASES ---------- + const SEC = { added: 'var(--fn)', changed: 'var(--ty)', fixed: 'var(--prop)' }; + const SECLABEL = { added: 'Added', changed: 'Changed', fixed: 'Fixed' }; + const relDefs = [ + { tag: 'v0.7.0', name: 'Paper Trail', date: 'June 12, 2026', sha: 'a1f3c9d', latest: true, hasDiff: true, + notes: { added: ['Releases timeline rail with the accent dot', 'Per-blob ETag caching'], changed: ['Render release notes with the Lora display face'] }, + assets: [{ name: 'git-ents-0.7.0-x86_64-linux.tar.gz', size: '4.2 MB' }, { name: 'git-ents-0.7.0-aarch64-macos.tar.gz', size: '4.0 MB' }] }, + { tag: 'v0.6.1', name: '', date: 'May 28, 2026', sha: '7c20e8b', latest: false, hasDiff: false, + notes: { fixed: ['Blob gutter alignment for 5-digit line numbers', 'Dark-mode flash on first paint'] }, + assets: [{ name: 'git-ents-0.6.1-x86_64-linux.tar.gz', size: '4.1 MB' }] }, + { tag: 'v0.6.0', name: 'Hairline', date: 'May 3, 2026', sha: '3e9a112', latest: false, hasDiff: false, + notes: { added: ['Two-pane file explorer'], changed: ['Hairline borders replace heavy dividers everywhere'] }, + assets: [{ name: 'git-ents-0.6.0-x86_64-linux.tar.gz', size: '4.0 MB' }] }, + { tag: 'v0.5.0', name: 'First Light', date: 'April 10, 2026', sha: 'bd44f0a', latest: false, hasDiff: false, + notes: { added: ['Auto dark mode via prefers-color-scheme', 'Gruvbox-warm syntax theme'] }, + assets: [{ name: 'git-ents-0.5.0-x86_64-linux.tar.gz', size: '3.8 MB' }] }, + ]; + const releases = relDefs.map(r => ({ + tag: r.tag, name: r.name, date: r.date, sha: r.sha, latest: r.latest, hasDiff: r.hasDiff, + assets: r.assets, + dotStyle: { + position: 'absolute', left: '-30px', top: '16px', width: '12px', height: '12px', + borderRadius: '100px', background: r.latest ? accent : 'var(--color-surface)', + border: '2px solid ' + (r.latest ? accent : 'var(--color-border)'), boxShadow: '0 0 0 4px var(--color-bg)', + }, + sections: Object.keys(r.notes).map(k => ({ label: SECLABEL[k], color: SEC[k], items: r.notes[k] })), + })); + + // ---------- HOOKS ---------- + const LOGCOL = { cmd: 'var(--prop)', ok: 'var(--fn)', err: 'var(--kw)', warn: 'var(--ty)', info: muted }; + const runDefs = [ + { id: 142, status: 'passed', subject: 'Render release notes with the Lora display face', sha: 'a1f3c9d', branch: 'main', trigger: 'push', duration: '1m 42s', age: '3h ago', + stages: [['lint', 'passed', '6s'], ['build', 'passed', '48s'], ['test', 'passed', '41s'], ['deploy', 'passed', '7s']], + log: [['cmd', '$ cargo fmt --check && cargo clippy -- -D warnings'], ['ok', 'formatting clean · 0 warnings'], ['cmd', '$ cargo build --release'], ['info', ' Compiling git-ents v0.7.0'], ['ok', ' Finished release [optimized] in 48.2s'], ['cmd', '$ cargo test --all'], ['ok', 'test result: ok. 86 passed; 0 failed'], ['cmd', '$ ./deploy.sh'], ['ok', 'deployed git.ents.dev @ a1f3c9d']] }, + { id: 141, status: 'failed', subject: 'Fix blob gutter alignment for 5-digit line numbers', sha: '7c20e8b', branch: 'pr/gutter', trigger: 'pre-receive', duration: '2m 05s', age: '5h ago', + stages: [['lint', 'passed', '6s'], ['build', 'passed', '47s'], ['test', 'failed', '1m 12s'], ['deploy', 'skipped', '—']], + log: [['cmd', '$ cargo test --all'], ['info', 'running 86 tests'], ['err', 'FAILED: blob::gutter::aligns_five_digits'], ['err', ' left: " 1024", right: "1024 "'], ['err', 'test result: FAILED. 85 passed; 1 failed'], ['warn', 'deploy skipped — tests did not pass']] }, + { id: 140, status: 'passed', subject: 'Bump gix to 0.63, syntect to 5.2', sha: '3e9a112', branch: 'main', trigger: 'push', duration: '1m 31s', age: 'yesterday', + stages: [['lint', 'passed', '5s'], ['build', 'passed', '52s'], ['test', 'passed', '34s'], ['deploy', 'passed', '6s']], + log: [['cmd', '$ cargo build --release'], ['ok', ' Finished release [optimized] in 52.0s'], ['cmd', '$ cargo test --all'], ['ok', 'test result: ok. 86 passed; 0 failed']] }, + { id: 139, status: 'running', subject: 'Cache rendered markdown per blob oid', sha: 'f02b8c1', branch: 'main', trigger: 'push', duration: '0m 38s', age: 'just now', + stages: [['lint', 'passed', '6s'], ['build', 'running', '32s'], ['test', 'pending', '—'], ['deploy', 'pending', '—']], + log: [['cmd', '$ cargo fmt --check && cargo clippy -- -D warnings'], ['ok', 'formatting clean · 0 warnings'], ['cmd', '$ cargo build --release'], ['info', ' Compiling git-ents v0.7.0 ...']] }, + ]; + const stColor = (s) => s === 'passed' ? 'var(--fn)' : s === 'failed' ? 'var(--kw)' : s === 'running' ? 'var(--ty)' : 'var(--color-text-muted)'; + const runIconWrap = { flex: 'none', width: '18px', display: 'flex', alignItems: 'center', justifyContent: 'center' }; + const runs = runDefs.map(r => ({ + id: r.id, subject: r.subject, branch: r.branch, trigger: r.trigger, age: r.age, + isRunning: r.status === 'running', isPassed: r.status === 'passed', isFailed: r.status === 'failed', + iconWrap: runIconWrap, + onClick: () => this.setState({ run: r.id }), + style: { + display: 'flex', alignItems: 'center', gap: '10px', width: '100%', padding: '12px 15px', + border: 'none', borderBottom: '1px solid var(--color-border)', cursor: 'pointer', + background: this.state.run === r.id ? 'var(--color-accent-subtle)' : 'transparent', + borderLeft: '2px solid ' + (this.state.run === r.id ? accent : 'transparent'), + }, + })); + const curDef = runDefs.find(r => r.id === this.state.run) || runDefs[0]; + const cur = { + subject: curDef.subject, sha: curDef.sha, duration: curDef.duration, + isRunning: curDef.status === 'running', isPassed: curDef.status === 'passed', isFailed: curDef.status === 'failed', + iconWrap: { flex: 'none', width: '20px', display: 'flex', alignItems: 'center', justifyContent: 'center' }, + stages: curDef.stages.map(s => ({ name: s[0], color: stColor(s[1]), duration: s[2] })), + log: curDef.log.map((l, i) => ({ n: i + 1, color: LOGCOL[l[0]], text: l[1] })), + }; + const HT = [ + ['# .gitents/hooks — CI defined as Git hooks', 'var(--cmt)'], + ['', text], + ['[pre-receive.lint]', 'var(--prop)'], + ['run = "cargo fmt --check && cargo clippy -- -D warnings"', text], + ['', text], + ['[post-receive.build]', 'var(--prop)'], + ['run = "cargo build --release"', text], + ['', text], + ['[post-receive.test]', 'var(--prop)'], + ['run = "cargo test --all"', text], + ['needs = ["build"]', text], + ['', text], + ['[post-receive.deploy]', 'var(--prop)'], + ['run = "./deploy.sh"', text], + ['needs = ["test"]', text], + ['when = "branch == \'main\'"', text], + ]; + const hooksToml = HT.map(l => ({ t: l[0] === '' ? ' ' : l[0], color: l[1] })); + + // ---------- ISSUES ---------- + const LBLCOL = { bug: 'var(--kw)', enhancement: 'var(--fn)', question: 'var(--prop)', docs: 'var(--ty)' }; + const issueDefs = [ + { num: 58, title: 'Blob gutter misaligns on files past 9,999 lines', state: 'open', labels: ['bug'], meta: 'opened 2 days ago by mira', comments: 4 }, + { num: 57, title: 'Add ETag caching for rendered README blobs', state: 'open', labels: ['enhancement'], meta: 'opened 4 days ago by elm', comments: 2 }, + { num: 55, title: 'How do I configure a post-receive deploy step?', state: 'open', labels: ['question'], meta: 'opened 6 days ago by toml-fan', comments: 7 }, + { num: 54, title: 'Dark mode flashes white on first paint', state: 'closed', labels: ['bug'], meta: 'closed 9 days ago by elm', comments: 3 }, + { num: 52, title: 'Timeline rail for the releases page', state: 'closed', labels: ['enhancement'], meta: 'closed 12 days ago by mira', comments: 1 }, + { num: 49, title: 'Document the hooks.toml schema', state: 'closed', labels: ['docs'], meta: 'closed 18 days ago by elm', comments: 0 }, + ]; + const openCount = issueDefs.filter(i => i.state === 'open').length; + const closedCount = issueDefs.filter(i => i.state === 'closed').length; + let filtered = issueDefs.filter(i => i.state === this.state.itab); + if (this.state.ilabel !== 'all') filtered = filtered.filter(i => i.labels.includes(this.state.ilabel)); + if (this.state.iq.trim()) { const q = this.state.iq.toLowerCase(); filtered = filtered.filter(i => i.title.toLowerCase().includes(q)); } + const issues = filtered.map(i => ({ + num: i.num, title: i.title, meta: i.meta, comments: i.comments, hasComments: i.comments > 0, + isOpen: i.state === 'open', isClosed: i.state === 'closed', + statusColor: i.state === 'open' ? 'var(--fn)' : 'var(--color-text-muted)', + labels: i.labels.map(l => ({ name: l, color: LBLCOL[l] || muted })), + })); + const itabBtn = (active) => ({ + display: 'flex', alignItems: 'center', gap: '7px', background: 'none', border: 'none', + cursor: 'pointer', fontSize: '.84rem', fontWeight: active ? 600 : 500, + color: active ? text : muted, fontFamily: 'var(--font-body)', padding: 0, + }); + const issueTabs = [ + { id: 'open', label: 'Open', count: openCount }, + { id: 'closed', label: 'Closed', count: closedCount }, + ].map(f => ({ + label: f.label, count: f.count, isOpen: f.id === 'open', isClosed: f.id === 'closed', + onClick: () => this.setState({ itab: f.id }), style: itabBtn(this.state.itab === f.id), + })); + const chipStyle = (active) => ({ + padding: '5px 13px', fontSize: '.76rem', fontWeight: 500, cursor: 'pointer', + borderRadius: 'var(--radius-pill)', fontFamily: 'var(--font-body)', + color: active ? 'var(--color-bg)' : muted, + background: active ? accent : 'var(--color-surface)', + border: '1px solid ' + (active ? accent : 'var(--color-border)'), + }); + const labelChips = [ + { id: 'all', label: 'All' }, { id: 'bug', label: 'bug' }, + { id: 'enhancement', label: 'enhancement' }, { id: 'question', label: 'question' }, + ].map(c => ({ label: c.label, onClick: () => this.setState({ ilabel: c.id }), style: chipStyle(this.state.ilabel === c.id) })); + + // ---------- SETTINGS ---------- + const featDefs = [ + { key: 'issues', label: 'Bug reports', desc: 'Let collaborators file and track issues.' }, + { key: 'releases', label: 'Releases', desc: 'Publish tagged releases with assets.' }, + { key: 'hooks', label: 'Hooks (CI)', desc: 'Run pre-receive and post-receive stages on push.' }, + { key: 'wiki', label: 'Wiki', desc: 'A companion docs space, rendered as paper.' }, + ]; + const features = featDefs.map(f => { + const on = this.state.features[f.key]; + return { + label: f.label, desc: f.desc, + onClick: () => this.setState(s => ({ features: { ...s.features, [f.key]: !s.features[f.key] } })), + trackStyle: { + flex: 'none', width: '42px', height: '24px', borderRadius: '100px', cursor: 'pointer', + border: 'none', padding: 0, position: 'relative', transition: 'background .18s', + background: on ? accent : 'var(--color-border)', + }, + knobStyle: { + position: 'absolute', top: '3px', left: on ? '21px' : '3px', width: '18px', height: '18px', + borderRadius: '100px', background: '#fff', transition: 'left .18s', boxShadow: '0 1px 2px #00000033', + }, + }; + }); + const visBtn = (active) => ({ + display: 'flex', alignItems: 'center', gap: '11px', width: '100%', padding: '12px 14px', + background: active ? 'var(--color-accent-subtle)' : 'var(--color-bg)', cursor: 'pointer', + border: '1px solid ' + (active ? accent : 'var(--color-border)'), borderRadius: '8px', textAlign: 'left', + }); + const visOptions = [ + { id: 'public', label: 'Public', desc: 'Anyone can read; the README and tree are served openly.' }, + { id: 'private', label: 'Private', desc: 'Only invited collaborators can clone or browse.' }, + ].map(v => ({ + label: v.label, desc: v.desc, on: this.state.visibility === v.id, + onClick: () => this.setState({ visibility: v.id }), style: visBtn(this.state.visibility === v.id), + radioStyle: { + flex: 'none', width: '16px', height: '16px', borderRadius: '100px', + border: '2px solid ' + (this.state.visibility === v.id ? accent : 'var(--color-border)'), + display: 'flex', alignItems: 'center', justifyContent: 'center', + }, + })); + + return { + tabs, + isOverview: tab === 'overview', isFiles: tab === 'files', isReleases: tab === 'releases', + isHooks: tab === 'hooks', isIssues: tab === 'issues', isSettings: tab === 'settings', + // overview + onClone: () => this.copy('git.ents.dev/git-ents/git-ents.git', 'clone'), + cloneCopied: this.state.copied === 'clone', notCloneCopied: this.state.copied !== 'clone', + // files + tree, lines, currentPath: this.state.file, lineCount: lines.length, currentSize: SIZES[this.state.file] || '—', + onCopyFile: () => this.copy(content, 'file'), + fileCopied: this.state.copied === 'file', notFileCopied: this.state.copied !== 'file', + // releases + releases, + // hooks + runs, cur, hooksToml, + // issues + issues, issueTabs, labelChips, iq: this.state.iq, + onSearch: (e) => this.setState({ iq: e.target.value }), + noIssues: issues.length === 0, + // settings + features, visOptions, + }; + } +} +</script> +</body> +</html>
design_handoff_git_ents/README.md @@ -1,0 +1,275 @@ +# Handoff: Git Ents — repository web UI + +## Overview + +A web presence for a self-hosted Git forge ("git-ents"). +It is a single repository page that deliberately centers the things a code editor is *bad* at: a large, editorial rendered README; a wander-able file tree; releases as a changelog; CI defined as Git hooks with run logs; bug reports; and repository configuration. + +The visual language is "Warm Ledger" — technical minimalism with warmth: a cream-and-gold paper palette, serif display type (Lora) over a geometric sans (DM Sans), monospace (IBM Plex Mono) for anything code-like, hairline borders, barely-there shadows, and a signature 3rem gold accent-underline under section headers. +Light and dark are equals and switch automatically with the OS setting. + +## About the Design Files + +The file in this bundle (`Git Ents.dc.html`) is a **design reference created in HTML** — a working prototype showing intended look and behavior, **not production code to copy directly**. +It is authored as a "Design Component" (a streaming single-file format) and will not run standalone in a normal app. + +The task is to **recreate this design in your target codebase** using its established framework and patterns (React, Vue, Svelte, server-rendered templates, etc.). +If no front-end environment exists yet, pick the most appropriate one for the project. +Note: the prototype itself is client-rendered, but the product it depicts is explicitly **server-rendered, no client JS** — if you are building the real forge, prefer a server-side HTML approach (the mock's interactivity, e.g. tab switching, would become separate page routes). + +## Fidelity + +**High-fidelity.** +Final colors, typography, spacing, and interactions. +Recreate the UI faithfully using your codebase's libraries and patterns. +Exact hex values, font sizes, and measurements are documented below and in the source file. + +--- + +## Design Tokens + +All styling reads from CSS custom properties. +Light is the base; dark overrides under `@media (prefers-color-scheme: dark)`. +**Never hardcode hex in components — read from these.** + +### Color + +| Token | Light | Dark | Used for | +|---|---|---|---| +| `--color-bg` | `#faf8f4` | `#171510` | page background | +| `--color-surface` | `#ffffff` | `#211f17` | cards, raised panels | +| `--color-text` | `#2a2518` | `#ede8d8` | body text | +| `--color-text-muted` | `#8a7e6a` | `#a89e88` | secondary text, metadata | +| `--color-link` | `#b07d10` | `#d4a030` | links | +| `--color-link-hover` | `#96690a` | `#e4b850` | link hover | +| `--color-border` | `#ede9de` | `#383324` | hairlines, card borders | +| `--color-code-bg` | `#f5f3eb` | `#211f17` | code, card headers, gutters | +| `--color-accent` | `#b07d10` | `#d4a030` | gold accent: rules, active states | +| `--color-accent-subtle` | `#b07d100f` | `#d4a03012` | accent tint backgrounds | + +Shadows: `--shadow-sm` = `0 1px 3px #0000000d` (light) / `0 1px 3px #00000040` (dark); `--shadow-md` = `0 4px 16px #0000000f` / `0 4px 16px #0000004d`. + +Page background has a faint top-center radial glow in the accent color: `radial-gradient(58rem 30rem at 50% -10rem, var(--color-accent-subtle), transparent 72%)`, `background-attachment: fixed`. + +### Syntax / categorical palette (Gruvbox-warm) — light / dark + +| Token | Light | Dark | Role | +|---|---|---|---| +| `--kw` | `#9d0006` | `#fb4934` | keyword / error / danger | +| `--fn` | `#427b58` | `#8ec07c` | function / success / "passed" | +| `--ty` | `#b57614` | `#fabd2f` | type / "running" / Rust lang dot | +| `--str` | `#79740e` | `#b8bb26` | string | +| `--num` | `#8f3f71` | `#d3869b` | number / constant | +| `--cmt` | `#9c8f74` | `#928374` | comment | +| `--prop` | `#076678` | `#83a598` | property / link / TOML section headers | + +Diff tints: `--diff-add` = `#4e9a0622` / `#b8bb2620`; `--diff-rem` = `#cc241d22` / `#fb493420`. + +### Typography + +Loaded from Google Fonts. + +- `--font-body`: `"DM Sans", system-ui, sans-serif` — weights 400–700. + UI + body. +- `--font-display`: `"Lora", Georgia, serif` — weights 500–700, `letter-spacing: -.01em`. + Headings (the signature move). +- `--font-mono`: `"IBM Plex Mono", ui-monospace, Menlo, monospace` — weights 400–600. + Code, repo/file names, labels, logos, metadata. + +Base: `font-size: 17px`, `line-height: 1.7`, antialiased. +Page section headings ~`1.5rem` Lora 700. +README h1 `2.4rem` Lora 700. +Uppercase mono micro-labels at `.7rem` with `.06em` letter-spacing for card headers and eyebrows. + +### Shape & spacing + +`--radius-sm: 10px` (cards, inputs, buttons); list rows / small controls use `7–8px`; `--radius-pill: 100px` (badges, branch tags, toggles). +App content max-width `78rem`, side padding `1.5rem`. +README reading column capped ~`44rem`. +Generous vertical rhythm; sections separated by hairlines + the accent underline, not heavy dividers. + +--- + +## Global Chrome + +### Sticky nav (height 58px) + +- `position: sticky; top: 0; z-index: 50`. + Background `color-mix(in srgb, var(--color-bg) 82%, transparent)` with `backdrop-filter: blur(10px)`. + Bottom hairline. +- Left: mono wordmark "git-ents" with a small tree/shield glyph in accent; hover → accent; `white-space: nowrap`. +- Center: search input (`max-width: 24rem`), placeholder "Jump to file or symbol", leading search icon, focus border → accent. (Decorative in mock.) +- Right: "Explore", "Docs" muted links (hover → accent), and a 30px circular avatar chip showing initials ("el") in accent on `--color-accent-subtle`. + +### Repo header band + +- Path line (mono, `1.18rem`): folder icon · `git-ents` (muted link) · `/` · `git-ents` (accent, weight 600) · **branch pill** (`main`, accent text on `--color-accent-subtle`, thin accent border, pill, with branch icon) · **`Public` pill** (muted on `--color-code-bg`). +- Description paragraph (`.98rem`, max ~40rem). +- Topic chips: `rust`, `git-forge`, `self-hosted`, `html-first` — mono `.72rem`, link color on `--color-accent-subtle`, pill. +- Right side: `Watch` and `Star 128` buttons — surface bg, hairline border, `--shadow-sm`, hover border → accent, leading icons. + +### Tab bar + +Row of buttons over a `1px` bottom border. +Each tab: `10px 14px`, `.88rem`. +Active tab = `--color-text` + weight 600 + a full-width **2px accent underline** pinned to `bottom: -1px`. +Idle = `--color-text-muted`, hover → `--color-text`. +Tabs: `Overview`, `Files`, `Releases` (count pill "4"), `Hooks` (green status dot), `Issues` (count pill "3"), `Settings`. +Count pills are mono `.68rem`, muted, on `--color-code-bg` with hairline border. + +### Footer + +Top hairline, centered mono `.72rem` muted: "git-ents · served as paper-grain HTML · no JavaScript required". + +--- + +## Screens / Views + +### 1. Overview (default tab) + +**Purpose:** read the project at a glance — the README is the star. +**Layout:** CSS grid, `grid-template-columns: 1fr 19rem`, gap `34px`, `align-items: start`. + +- **README card** (left): surface, hairline border, `--radius-sm`, `--shadow-sm`, overflow hidden. + - Header strip: `--color-code-bg`, bottom hairline, mono uppercase `.72rem` muted label "README.md" with a file icon. + - Body: padding `40px 48px 52px`, `max-width: 44rem`. + - `h1` "git-ents" — Lora 700, `2.4rem`, `letter-spacing: -.02em`. + - Italic Lora subtitle in muted (`1.18rem`). + - Badge row: pills (build passing — with `--fn` dot, `v0.7.0` in `--ty`, `MIT` in `--prop`, `rust 1.78+` muted), mono `.7rem` on `--color-code-bg` with hairline. + - Body paragraphs; `h2` section headers (Lora 600, `1.4rem`) each followed by a **3rem × 2px accent underline**. + - Bulleted lists; inline `code` on `--color-code-bg`, `5px` radius. + - Two code blocks on `--color-code-bg` with hairline + `--radius-sm`: an install block (shell, `$` prompts muted, comments in `--cmt`) and a syntax-highlighted Rust snippet using the syntax palette (`--kw` keywords, `--fn` functions, `--ty` types, `--cmt` comments). +- **Aside** (right, `position: sticky; top: 78px`, column, gap `18px`): four cards, each with a mono uppercase header strip + hairline: + - **Clone**: readonly mono input (`git.ents.dev/git-ents/git-ents.git`) on `--color-code-bg` + 34px copy button; on copy → green check for 1.4s. + - **About**: Rust·MIT (with `--ty` dot), stars/forks, "Updated 3 hours ago". + - **Releases**: count "4" in accent; latest `v0.7.0` row with tag icon + "Latest" pill + "Paper Trail — released 9 days ago". + - **Languages**: thin stacked bar (Rust 86% `--ty` / HTML 9% `--fn` / CSS 5% `--prop`) + legend with color dots. + +### 2. Files + +**Purpose:** browse the repo tree and read files. +**Layout:** grid `17rem 1fr` inside one bordered, shadowed, rounded card; min-height `30rem`; overflow hidden. + +- **Left tree pane:** `--color-bg`, right hairline. + Header strip = mono "main" with branch icon. + - Rows: mono `.79rem`, `padding: 4px 8px`, indent `8 + depth*15` px. + Folders show a chevron (rotates 90° when open, `.15s`) + accent folder icon; files show a spacer + muted file icon. + Hover bg → `--color-code-bg`. + **Selected file**: bg `--color-accent-subtle`, text → accent, weight 600. + - Tree data: `.gitents/hooks.toml`; `src/{main.rs, forge/{mod,repo,render,blob}.rs, hooks/{mod,runner}.rs}`; `templates/{repo,blob}.html`; `static/paper.css`; `Cargo.toml`; `LICENSE`; `README.md`. + Default-expanded: `.gitents`, `src`, `src/forge`. + Default selection: `Cargo.toml`. +- **Right blob pane:** column. + - Header: `--color-code-bg`, bottom hairline. + Left = mono current path. + Right = mono meta ("N lines · SIZE") + a Copy button (→ green "Copied" check for 1.4s). + - Body: grid `auto 1fr`, scrollable. + Left = line-number gutter on `--color-code-bg`, right hairline, mono `.78rem`, muted at `opacity .6`, right-aligned. + Right = code lines, mono `.78rem`, `line-height: 1.55`, `white-space: pre`, horizontal scroll. (Mock renders plain mono per file; real impl should syntax-highlight server-side.) + +### 3. Releases + +**Purpose:** read the changelog. +**Layout:** Page header "Releases" (Lora `1.5rem`) + 3rem accent underline. +Then a **timeline rail**: container `padding-left: 30px` with a `2px` vertical line at `left: 5px` in `--color-border`. + +- Each release: a **dot** absolutely positioned at `left: -30px; top: 16px`, 12px, pill, with a `4px` ring in `--color-bg` (`box-shadow`). + Latest = filled accent; others = surface fill with border ring. +- Release card (surface, hairline, rounded, shadow): + - Header row (bottom hairline): tag icon (accent) + mono tag (`v0.7.0`, accent, weight 600) + Lora name ("Paper Trail") + optional "Latest" pill (`--fn`); right-aligned date (muted). + - Body: note sections grouped **Added / Changed / Fixed**, each with a mono uppercase eyebrow colored `--fn` / `--ty` / `--prop` respectively, then a bullet list (`.88rem`). + The latest release also shows a 2-line **diff** block (added line on `--diff-add`, removed line on `--diff-rem`, mono `.76rem`). + - Footer (top hairline): asset download links (mono `.74rem`, link color, download icon, size in muted) + right-aligned mono sha with a commit glyph. +- Four releases: `v0.7.0 Paper Trail` (latest, June 12 2026), `v0.6.1` (May 28), `v0.6.0 Hairline` (May 3), `v0.5.0 First Light` (Apr 10). + +### 4. Hooks + +**Purpose:** CI defined as Git hooks — view runs and logs. +**Layout:** Page header "Hooks" + underline + intro paragraph (notes CI is plain `pre-receive`/`post-receive`, config in `.gitents/hooks.toml`). +Then grid `20rem 1fr`, gap `20px`, `align-items: start`. + +- **Run list** (left card): header "Recent runs". + Each run is a button row (bottom hairline): a status icon (passed = `--fn` filled check-circle; failed = `--kw` x-circle; running = `--ty` spinner, `@keyframes spin` 1s linear) + subject (truncated) + mono submeta "#id · branch · trigger" + right-aligned age. + **Selected run**: bg `--color-accent-subtle` + `2px` left accent border. + Hover → `--color-code-bg`. +- **Run detail** (right column, gap 18px): + - Detail card: header (status icon + Lora subject + right-aligned mono "sha · duration"). + **Stages** row (bottom hairline): pills on `--color-code-bg` each = a status dot (`--fn`/`--kw`/`--ty`/muted) + mono stage name + mono duration (`lint`, `build`, `test`, `deploy`). + **Log viewer**: `--color-code-bg`, mono `.76rem`, `line-height: 1.7`, `white-space: pre`, horizontal scroll. + Each line = muted line number + text colored by level: cmd→`--prop`, ok→`--fn`, err→`--kw`, warn→`--ty`, info→muted. + - Config card: mono header "`.gitents/hooks.toml`" on `--color-code-bg`; body renders the TOML with section headers (`[pre-receive.lint]` etc.) in `--prop`, comments in `--cmt`, rest in text color. +- Four runs (#142 passed, #141 failed, #140 passed, #139 running). + Default selection: #142. + +### 5. Issues ("Bug reports") + +**Purpose:** file and track bugs. +**Layout:** Header row: "Bug reports" (Lora `1.5rem`, `white-space: nowrap`) + underline on the left; "New issue" button on the right (solid accent bg, `--color-bg` text, weight 600, plus icon, hover → `--color-link-hover`). + +- **Filter row:** search input (flex-grow, leading icon, `onInput` filters by title, focus border → accent) + **label chips** (`All`, `bug`, `enhancement`, `question`). + Active chip = solid accent bg + `--color-bg` text; idle = surface + muted + hairline. + Pill shape, `.76rem`. +- **List card:** header strip on `--color-code-bg` with **Open / Closed** sub-tabs (each = status icon + label + count; active = text + weight 600). + Each issue row (bottom hairline, hover → `--color-code-bg`): status icon (open = `--fn` open-issue dot-circle; closed = muted check-circle) + title link (hover → accent) + label pills (mono `.66rem`, colored per label: bug→`--kw`, enhancement→`--fn`, question→`--prop`, docs→`--ty`, on `--color-code-bg` + hairline) + mono submeta "#num opened/closed … by author" + right-aligned comment count with speech icon (hidden if 0). + Empty state: centered muted "No bug reports match these filters." +- Six issues (3 open, 3 closed) covering bug/enhancement/question/docs labels. + +### 6. Settings ("Repository settings") + +**Purpose:** configure the repo. +**Layout:** `max-width: 46rem`. +Page header + underline. +Four stacked cards, each with a mono uppercase header strip + hairline. + +- **General:** labeled fields — repo name (mono input), description (textarea, body font, vertical resize), default branch (`select`: main/develop/trunk). + Inputs: `--color-bg` bg, hairline, `8px` radius, focus border → accent. +- **Features:** four rows (Bug reports, Releases, Hooks (CI), Wiki), each = label + description + a **toggle switch** (42×24 track, pill; on = accent bg + knob at `left: 21px`; off = `--color-border` + knob at `left: 3px`; 18px white knob, `.18s` transition). + Defaults: issues/releases/hooks on, wiki off. +- **Visibility:** two radio rows (Public / Private) — each a button with a 16px radio (filled accent dot when selected), title + description; selected row = `--color-accent-subtle` bg + accent border. + Default: Public. +- **Danger zone:** card with a `--kw` border + `--kw` header. + Two rows: "Archive this repository" (outline `--kw` button, hover bg `--diff-rem`) and "Delete this repository" (solid `--kw` button, white text). + +--- + +## Interactions & Behavior + +- **Tabs:** clicking a tab swaps the content region; active underline + weight change. + In a server-rendered build these become separate routes/pages. +- **File tree:** folder rows toggle expand/collapse (chevron rotates `.15s`); file rows select and load the blob into the right pane. +- **Run list:** selecting a run swaps the detail card (stages + log + the run's own logs). + Running runs show an infinite spinner. +- **Issue filters:** Open/Closed sub-tabs, label chips, and the search box compose (AND) to filter the list live; empty result → empty-state message. +- **Copy buttons** (clone URL, blob): write to clipboard, then show a green check / "Copied" for ~1.4s before reverting. +- **Settings toggles & radios:** flip local state with animated knob / radio fill. +- **Hover motion** is small (≤3px) and quick (`.15s–.22s`); hover states lean on the accent and the subtle tint, rarely on shadow. + List-row hover tints to `--color-code-bg`. +- **Dark mode:** automatic via `prefers-color-scheme`; every token has a dark value — no manual toggle. + +## State Management + +Local UI state only (no data fetching in the mock): + +- `tab` — active top-level tab. +- `exp` — map of expanded folder paths (file tree). +- `file` — selected file path (blob view). +- `run` — selected CI run id. +- `itab` (open|closed), `ilabel` (all|bug|enhancement|question), `iq` (search string) — issue filters. +- `copied` — transient key marking which copy button is showing its confirmation. +- `features` — map of feature toggles; `visibility` — public|private. + +In a real forge most of this maps to **server routes + query params** (tab/path/run/ filters in the URL) since the product is server-rendered with no client JS; only the copy-to-clipboard affordance strictly needs a sprinkle of JS. + +## Assets + +- **Fonts:** DM Sans, Lora, IBM Plex Mono (Google Fonts). +- **Icons:** inline 16px line icons in the Octicon idiom, `fill: currentColor` so they inherit accent/muted from context. + No emoji in chrome. + Recreate with your icon set (e.g. Octicons / Lucide) at equivalent weights. +- No raster images; the avatar is an initials chip. + +## Files + +- `Git Ents.dc.html` — the full hi-fi prototype (all six tabs, all interactions). + Open it in a browser to inspect exact markup, inline styles, and the logic that computes the lists/states. + All design tokens are defined in its `:root` block (and the dark `@media` override) near the top of the file.