From 6f61821d1edf2830f802cd5255f028b476a54456 Mon Sep 17 00:00:00 2001 From: Luna Date: Tue, 16 Jun 2026 19:29:24 -0700 Subject: [PATCH] =?UTF-8?q?web=20UI:=20stats=20modal=20=E2=86=92=20maximal?= =?UTF-8?q?ist=20"observatory"=20data=20dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuild the Library statistics modal from plain tiles + flat bars + tables into a real data dashboard, driven by the same /api/stats payload and the theme variables (so all 10 themes inherit it). - Metric cards with giant serif numbers that count up from zero on open (size/runtime spin up through their units), an accent top-rule, and a glowing hero tile. - A self-drawing SVG area chart for downloads-per-week: Catmull-Rom-smoothed line that animates in via stroke-dashoffset, a gradient area fill, gridlines, a date axis, and hover dots with tooltips. - A growing-column histogram for videos-by-upload-year with value labels. - A ranked channel leaderboard with animated meter fills, gold/silver/bronze ranks, and a Size/Count segmented toggle that re-animates on switch. - Reveal animations gate behind .sx-go (added post-layout) so they play once per open, not on re-render; honours prefers-reduced-motion; modal widened to 1040px. Verified: JS passes node --check, release builds clean, the live 8081 server serves it, and a headless screenshot of a harness running the *extracted* dashboard CSS+JS against a mock payload confirms the chart math, count-up, histogram and leaderboard all render correctly. Co-Authored-By: Claude Opus 4.8 --- src/web_ui/index.html | 294 ++++++++++++++++++++++++++++++++---------- 1 file changed, 228 insertions(+), 66 deletions(-) diff --git a/src/web_ui/index.html b/src/web_ui/index.html index 5e15828..08a9fda 100644 --- a/src/web_ui/index.html +++ b/src/web_ui/index.html @@ -327,6 +327,79 @@ @keyframes uiFade{from{opacity:0}to{opacity:1}} @keyframes uiDrop{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:none}} + /* ── Stats: "observatory" data dashboard (maximalist) ───────────────────── + Scoped under .sx-* and built entirely from the theme variables so it + inherits every theme. Reveal animations are gated behind .sx-go (added + after the DOM is in place) so they play once per open, not on re-render. */ + .sx-body{position:relative;font-variant-numeric:tabular-nums} + .sx-body::before{content:'';position:absolute;inset:0;pointer-events:none;z-index:0;opacity:.55; + background-image:linear-gradient(var(--hair) 1px,transparent 1px),linear-gradient(90deg,var(--hair) 1px,transparent 1px); + background-size:34px 34px; + -webkit-mask-image:radial-gradient(120% 90% at 25% 0%,#000,transparent 72%); + mask-image:radial-gradient(120% 90% at 25% 0%,#000,transparent 72%)} + .sx-body>*{position:relative;z-index:1} + .sx-eyebrow{font-size:10px;letter-spacing:.24em;text-transform:uppercase;color:var(--muted);font-weight:700} + .sx-sect{margin:20px 0 9px;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap} + .sx-sect h3{font-family:var(--font-display);font-weight:400;font-size:1.5rem;letter-spacing:.01em;margin:0;line-height:1} + .sx-note{margin-left:auto;font-size:11px;color:var(--muted);letter-spacing:.02em;font-variant-numeric:tabular-nums} + .sx-empty{color:var(--muted);font-style:italic;font-family:var(--font-display);font-size:1.1rem;padding:10px 2px} + + .sx-metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-top:9px} + .sx-card{position:relative;overflow:hidden;border:1px solid var(--hair);border-radius:var(--radius);padding:13px 14px; + background:linear-gradient(158deg,color-mix(in srgb,var(--card) 92%,transparent),color-mix(in srgb,var(--panel) 78%,transparent)); + transition:transform .2s,box-shadow .2s,border-color .2s} + .sx-card:hover{transform:translateY(-3px);border-color:var(--accent);box-shadow:var(--shadow)} + .sx-card::after{content:'';position:absolute;left:0;top:0;height:2px;width:100%; + background:linear-gradient(90deg,var(--accent),transparent)} + .sx-card .sx-k{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--muted);font-weight:700} + .sx-card .sx-v{font-family:var(--font-display);font-weight:400;font-size:2.1rem;line-height:1.04;margin-top:5px;word-break:break-word} + .sx-card .sx-sub{font-size:11px;color:var(--muted);margin-top:3px} + .sx-hero{grid-column:span 2; + background:radial-gradient(150% 130% at 0% 0%,var(--glow),transparent 58%),linear-gradient(158deg,var(--card),var(--panel))} + .sx-hero .sx-v{font-size:3.6rem} + + .sx-chart{width:100%;height:auto;display:block;overflow:visible} + .sx-chart .sx-line{fill:none;stroke:var(--accent);stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round; + stroke-dasharray:1;stroke-dashoffset:1;transition:stroke-dashoffset 1.3s cubic-bezier(.65,0,.35,1)} + .sx-go .sx-chart .sx-line{stroke-dashoffset:0} + .sx-chart .sx-fill{opacity:0;transition:opacity .9s ease .45s} + .sx-go .sx-chart .sx-fill{opacity:1} + .sx-chart .sx-grid{stroke:var(--hair);stroke-width:1} + .sx-chart .sx-axis{fill:var(--muted);font-size:11px;font-family:var(--font-body)} + .sx-chart .sx-dot{fill:var(--bg);stroke:var(--accent);stroke-width:2;opacity:0;transition:opacity .4s ease .95s,r .15s} + .sx-go .sx-chart .sx-dot{opacity:1} + .sx-chart .sx-dot:hover{r:5.5;fill:var(--accent)} + + .sx-2col{display:grid;grid-template-columns:1.05fr 1fr;gap:24px;align-items:start} + .sx-cols{display:flex;align-items:flex-end;gap:6px;height:150px;padding-top:20px} + .sx-col{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%;min-width:0} + .sx-bar{width:100%;max-width:46px;height:0;border-radius:5px 5px 0 0;position:relative; + background:linear-gradient(180deg,var(--accent),color-mix(in srgb,var(--accent) 28%,transparent)); + transition:height .95s cubic-bezier(.22,1,.36,1)} + .sx-bar::after{content:attr(data-v);position:absolute;top:-17px;left:-4px;right:-4px;text-align:center; + font-size:11px;font-weight:700;color:var(--text);opacity:0;transition:opacity .4s ease .6s} + .sx-go .sx-bar::after{opacity:1} + .sx-cl{font-size:10px;color:var(--muted);margin-top:6px;font-variant-numeric:tabular-nums} + + .sx-seg{margin-left:auto;display:inline-flex;border:1px solid var(--hair);border-radius:999px;overflow:hidden} + .sx-seg button{border:none;border-radius:0;background:transparent;color:var(--muted);font-size:11px;padding:4px 13px;font-weight:600} + .sx-seg button:hover{color:var(--text);background:transparent} + .sx-seg button.on,.sx-seg button.on:hover{background:var(--accent);color:#fff} + .sx-lb{display:flex;flex-direction:column;gap:8px} + .sx-row{display:grid;grid-template-columns:22px 1fr auto;align-items:center;gap:10px} + .sx-rank{font-family:var(--font-display);font-size:1.2rem;color:var(--muted);text-align:center;line-height:1} + .sx-lb .sx-row:nth-child(1) .sx-rank{color:#f6c453} + .sx-lb .sx-row:nth-child(2) .sx-rank{color:#c7ced8} + .sx-lb .sx-row:nth-child(3) .sx-rank{color:#cd8b62} + .sx-track{position:relative;height:32px;border-radius:8px;overflow:hidden;display:flex;align-items:center; + background:color-mix(in srgb,var(--card) 55%,transparent);border:1px solid var(--hair)} + .sx-meter{position:absolute;left:0;top:0;bottom:0;width:0;border-radius:8px; + background:linear-gradient(90deg,color-mix(in srgb,var(--accent) 60%,transparent),color-mix(in srgb,var(--accent) 14%,transparent)); + transition:width 1s cubic-bezier(.22,1,.36,1)} + .sx-name{position:relative;z-index:1;padding:0 11px;font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + .sx-val{font-size:12px;color:var(--muted);white-space:nowrap;font-variant-numeric:tabular-nums} + @media(max-width:760px){.sx-2col{grid-template-columns:1fr;gap:14px}.sx-hero{grid-column:span 2}} + @media(prefers-reduced-motion:reduce){*{animation:none!important;transition-duration:.001ms!important}} @@ -2330,9 +2403,9 @@ async function saveSettings(btn){ /* ── Statistics ─────────────────────────────────────────────────── */ async function openStats(){ const bg=document.createElement('div');bg.className='modal-bg';bg.onclick=e=>{if(e.target===bg)bg.remove()}; - bg.innerHTML=`