feat: the redesigned Event Clock
The new reader app replaces the old UI: Checklist with a rail (focus pills, Next to expire, today's dailies, pinned routines), Timeline in a new and a classic look, Routines, Archive and a Settings area, in a Structured (default) and a Relaxed interface. It follows the approved Round 6 B and Round 7 mockups, corrected screen by screen with the owner. Reader data moves to a v2 store (gacha-tracker:v2:*). Existing v1 data is migrated on first load, a recovery copy is written first, and the v1 keys are never removed, so a rollback finds them unchanged. Redeem codes for Genshin arrive as their own feed. Also: live's urgency ramp, editable per theme; completion and section-complete fireworks; a returning-user tour; the design mockups, specs, plans, reviews and handoff record. Co-Authored-By: Claude Opus 5.5 <[email protected]>
@@ -0,0 +1 @@
|
||||
58245
|
||||
@@ -0,0 +1 @@
|
||||
e13012f97798f2f41fa46ae58204a4bcd50a142b56e7cca42efec09e7633540f
|
||||
@@ -0,0 +1,700 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>A · Every game has its own setup</title>
|
||||
<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=Chakra+Petch:wght@500;600;700&family=Public+Sans:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--ground:#12141c;--surface:#1b1e2a;--raised:#232739;--hairline:#2c3145;
|
||||
--ink:#e9ebf3;--ink-strong:#fff;--muted:#888fa6;--faint:#5a6178;--glow:#1a1e2c;
|
||||
--calm:#55607f;--near:#5b8def;--soon:#e8b33c;--critical:#ff4d6a;
|
||||
--scrim:rgb(18 20 28 / .8);
|
||||
--read:"Chakra Petch",ui-sans-serif,system-ui,sans-serif;
|
||||
--lang:"Public Sans",ui-sans-serif,system-ui,sans-serif;
|
||||
--bar:44px;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html,body{margin:0}
|
||||
body{background:var(--ground) radial-gradient(120% 70% at 50% -10%,var(--glow),var(--ground) 60%) no-repeat;color:var(--ink);font:400 14px/1.6 var(--lang);min-height:100vh;overflow-x:hidden}
|
||||
button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}
|
||||
:focus-visible{outline:2px solid var(--near);outline-offset:2px;border-radius:4px}
|
||||
.tnum{font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ---------- prototype chrome (deliberately not the app) ---------- */
|
||||
.mock{position:fixed;inset:0 0 auto 0;height:var(--bar);z-index:50;background:#d4d8e1;color:#20242f;border-bottom:1px dashed #7d8496;display:flex;align-items:center;gap:6px;padding:0 12px;font:500 12px/1 system-ui,sans-serif;overflow-x:auto;white-space:nowrap}
|
||||
.mock b{font-weight:700;margin-right:8px}
|
||||
.mock button{padding:6px 9px;border-radius:5px;border:1px solid transparent}
|
||||
.mock button:hover{background:#c3c8d3}
|
||||
.mock button[aria-current="true"]{background:#20242f;color:#eef0f5}
|
||||
.mock .sep{width:1px;height:20px;background:#9aa1b2;margin:0 4px;flex:none}
|
||||
.mock .why{border-color:#20242f}
|
||||
.why-panel{position:fixed;top:var(--bar);right:0;bottom:0;width:min(440px,100%);z-index:49;background:#e9ebf0;color:#20242f;border-left:1px dashed #7d8496;padding:20px 22px 40px;overflow:auto;font:400 13.5px/1.6 system-ui,sans-serif;transform:translateX(100%);transition:transform .18s ease-out}
|
||||
.why-panel.open{transform:none}
|
||||
.why-panel h3{font:700 13px/1.3 system-ui;margin:18px 0 6px}
|
||||
.why-panel h3:first-child{margin-top:0}
|
||||
.why-panel code{font-size:12px;background:#d4d8e1;padding:1px 4px;border-radius:3px}
|
||||
.why-panel pre{background:#20242f;color:#e9ebf3;font-size:11.5px;line-height:1.5;padding:10px 12px;border-radius:6px;overflow:auto}
|
||||
.why-panel ul{padding-left:18px;margin:4px 0}
|
||||
|
||||
/* ---------- app shell ---------- */
|
||||
.app{padding-top:var(--bar)}
|
||||
.shell{max-width:1320px;margin:0 auto;border-inline:1px solid var(--hairline);min-height:calc(100vh - var(--bar))}
|
||||
header.top{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px;border-bottom:1px solid var(--hairline)}
|
||||
.wordmark{font:700 18px/1 var(--read);letter-spacing:.02em}
|
||||
.wordmark span{color:var(--near)}
|
||||
.sub{font-size:12px;color:var(--faint);margin-top:4px}
|
||||
.tabs{display:flex;padding:2px;border:1px solid var(--hairline);border-radius:8px;gap:2px;position:relative}
|
||||
.tabs button{font:500 12px/1 var(--lang);padding:8px 12px;border-radius:6px;color:var(--faint);display:flex;align-items:center;gap:6px;transition:color .12s}
|
||||
.tabs button:hover{color:var(--muted)}
|
||||
.tabs button[aria-selected="true"]{background:var(--raised);color:var(--ink)}
|
||||
.tabs .divider{width:1px;background:var(--hairline);margin:4px 2px}
|
||||
.tabs svg{width:14px;height:14px}
|
||||
.tabs .hl{box-shadow:0 0 0 2px var(--near);animation:pulse 1.4s ease-in-out 3}
|
||||
@keyframes pulse{50%{box-shadow:0 0 0 5px rgb(91 141 239 / .25)}}
|
||||
.callout{position:absolute;right:0;top:calc(100% + 10px);width:300px;background:var(--surface);border:1px solid var(--near);border-radius:12px;padding:14px;z-index:5;font-size:13px;color:var(--muted)}
|
||||
.callout b{color:var(--ink);font-weight:600}
|
||||
.callout button{margin-top:10px}
|
||||
|
||||
.cols{display:grid;grid-template-columns:22rem 1fr}
|
||||
.rail{border-right:1px solid var(--hairline);position:sticky;top:var(--bar);align-self:start;max-height:calc(100vh - var(--bar));overflow:auto}
|
||||
.rail section{padding:20px 16px;border-bottom:1px solid var(--hairline)}
|
||||
.legend{font:600 11px/1 var(--read);letter-spacing:.06em;color:var(--faint);display:flex;align-items:center;gap:8px;min-height:22px}
|
||||
.legend .r{margin-left:auto;display:flex;align-items:center;gap:8px;letter-spacing:0;font-weight:500}
|
||||
.gear{width:22px;height:22px;display:grid;place-items:center;border-radius:6px;color:var(--faint)}
|
||||
.gear:hover{color:var(--ink);background:var(--raised)}
|
||||
.gear svg{width:14px;height:14px}
|
||||
|
||||
/* headline */
|
||||
.headline{position:relative;overflow:hidden}
|
||||
.headline .wash{position:absolute;inset:-40% -20% auto -20%;height:120%;background:radial-gradient(closest-side,var(--heat),transparent);opacity:.16;filter:blur(20px);pointer-events:none}
|
||||
.headline h2{font:600 26px/1.15 var(--read);letter-spacing:-.02em;margin:10px 0 4px;position:relative}
|
||||
.headline .game{font-size:14px;position:relative}
|
||||
.numeral{font:700 44px/1 var(--read);letter-spacing:-.025em;margin:18px 0 12px;display:flex;align-items:baseline;justify-content:space-between;position:relative}
|
||||
.numeral small{font:400 12px/1.3 var(--lang);color:var(--faint);text-align:right;letter-spacing:0}
|
||||
.then{list-style:none;margin:16px 0 0;padding:14px 0 0;border-top:1px solid var(--hairline)}
|
||||
.then li{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--muted);padding:3px 0}
|
||||
.then .dot{width:6px;height:6px;border-radius:50%;flex:none}
|
||||
.then .t{margin-left:auto;font:600 12px var(--read)}
|
||||
|
||||
/* meter */
|
||||
.meter{display:flex;gap:2px;height:10px}
|
||||
.meter i{flex:1;border-radius:1px;background:var(--hairline)}
|
||||
.meter i.on{background:var(--tick);box-shadow:0 0 6px -1px var(--tick)}
|
||||
.meter.unknown{background:repeating-linear-gradient(135deg,var(--hairline) 0 3px,transparent 3px 7px);border-radius:1px}
|
||||
|
||||
/* game lines in rail */
|
||||
.glines{margin-top:10px}
|
||||
.gline{display:grid;grid-template-columns:3px 1fr auto;gap:0 12px;align-items:center;padding:9px 0;border-top:1px solid color-mix(in srgb,var(--hairline) 60%,transparent)}
|
||||
.gline:first-child{border-top:0}
|
||||
.gline .rail3{width:3px;height:70%;border-radius:2px;background:var(--hue);opacity:.7;align-self:center;min-height:18px}
|
||||
.gline .nm{font-size:14px;font-weight:500;line-height:1.3}
|
||||
.gline .what{font-size:12px;color:var(--faint);line-height:1.35}
|
||||
.gline .what.go{color:var(--muted)}
|
||||
.gline .end{display:flex;align-items:center;gap:6px}
|
||||
.tick{width:24px;height:24px;border-radius:6px;border:1px solid var(--hairline);display:grid;place-items:center;color:transparent;transition:background .12s,border-color .12s}
|
||||
.tick svg{width:14px;height:14px}
|
||||
.tick:hover{border-color:var(--faint)}
|
||||
.tick.done{background:color-mix(in srgb,var(--near) 18%,transparent);border-color:var(--near);color:var(--near)}
|
||||
.count{font:600 12px var(--read);color:var(--muted)}
|
||||
.count.out{color:var(--soon)}
|
||||
.count.met{color:var(--near)}
|
||||
.chev{width:16px;height:16px;color:var(--faint);transition:transform .14s}
|
||||
.open .chev{transform:rotate(90deg)}
|
||||
.gline.btn{cursor:pointer;width:100%;text-align:left}
|
||||
.gline.btn:hover .nm{color:var(--ink-strong)}
|
||||
.expand{grid-column:2 / 4;padding:8px 0 4px;margin-top:6px}
|
||||
.item{display:flex;align-items:center;gap:10px;padding:6px 0;font-size:13px;color:var(--muted)}
|
||||
.item .lbl{flex:1;line-height:1.3}
|
||||
.item .lbl small{display:block;color:var(--faint);font-size:11.5px}
|
||||
.item.met .lbl{color:var(--faint)}
|
||||
.amt{display:flex;align-items:center;gap:6px;font:600 12px var(--read);color:var(--faint)}
|
||||
.amt input{width:72px;background:var(--ground);border:1px solid var(--hairline);border-radius:6px;color:var(--ink);font:600 12px var(--read);padding:4px 6px;text-align:right}
|
||||
.mine{font:500 10px/1 var(--lang);padding:3px 5px;border-radius:3px;border:1px solid var(--hairline);color:var(--muted);margin-left:6px;vertical-align:1px}
|
||||
.addhere{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--faint);padding:6px 0 0}
|
||||
.addhere:hover{color:var(--ink)}
|
||||
.addhere svg{width:12px;height:12px}
|
||||
.empty{font-size:13px;color:var(--muted);margin:10px 0 0;line-height:1.5}
|
||||
.link{color:var(--near);text-decoration:underline;text-underline-offset:3px}
|
||||
|
||||
/* main column */
|
||||
main{min-width:0}
|
||||
.banner{margin:16px;border:1px solid var(--hairline);border-left:3px solid var(--near);border-radius:8px;padding:14px 16px;display:flex;gap:16px;align-items:center;flex-wrap:wrap}
|
||||
.banner p{margin:0;flex:1;min-width:240px;font-size:13.5px;color:var(--muted)}
|
||||
.banner p b{color:var(--ink);font-weight:600}
|
||||
.qbtn{font:500 12px/1 var(--lang);padding:8px 12px;border:1px solid var(--hairline);border-radius:8px;color:var(--muted);transition:color .12s,border-color .12s}
|
||||
.qbtn:hover{color:var(--ink);border-color:var(--faint)}
|
||||
.cbtn{font:600 13px/1 var(--lang);padding:10px 16px;border-radius:8px;background:var(--ink);color:var(--ground)}
|
||||
.cbtn:hover{background:var(--ink-strong)}
|
||||
.cbtn:disabled{background:var(--raised);color:var(--faint);cursor:default}
|
||||
.listhead{display:flex;align-items:center;gap:12px;padding:16px 16px 10px;border-bottom:1px solid var(--hairline);flex-wrap:wrap}
|
||||
.focus{display:flex;gap:2px 14px;flex-wrap:wrap;font-size:12.5px}
|
||||
.focus button{color:var(--faint);padding:4px 0;border-bottom:2px solid transparent}
|
||||
.focus button:hover{color:var(--ink)}
|
||||
.focus button[aria-pressed="true"]{color:var(--ink);border-color:var(--fh,var(--near))}
|
||||
.focus .n{font:600 11px var(--read);opacity:.7;margin-left:4px}
|
||||
.erow{display:grid;grid-template-columns:3px 1fr auto;gap:0 14px;padding:14px 16px;border-bottom:1px solid var(--hairline);width:100%;text-align:left;transition:background .12s}
|
||||
.erow:hover{background:color-mix(in srgb,var(--raised) 62%,transparent)}
|
||||
.erow .rail3{width:3px;border-radius:2px;background:var(--hue);opacity:.55;height:82%;align-self:center}
|
||||
.erow:hover .rail3{opacity:1;height:100%;box-shadow:0 0 10px var(--hue)}
|
||||
.eyebrow{font:600 11px/1 var(--read);letter-spacing:.06em;color:var(--hue)}
|
||||
.etitle{display:flex;justify-content:space-between;gap:12px;margin:6px 0 8px;font-size:15px;font-weight:500;line-height:1.375}
|
||||
.etitle .rd{font:600 14px var(--read);white-space:nowrap}
|
||||
.emeta{font-size:12px;color:var(--faint);margin-top:6px}
|
||||
.erow .chev{align-self:center}
|
||||
.mutednote{padding:16px;font-size:13px;color:var(--faint)}
|
||||
|
||||
/* settings */
|
||||
.settings{display:grid;grid-template-columns:20rem 1fr;min-height:calc(100vh - var(--bar) - 70px)}
|
||||
.slist{border-right:1px solid var(--hairline);padding:12px 0}
|
||||
.slist h4{font:600 11px/1 var(--read);letter-spacing:.06em;color:var(--faint);margin:14px 16px 6px}
|
||||
.srow{display:grid;grid-template-columns:3px 1fr auto;gap:0 12px;align-items:center;width:100%;text-align:left;padding:9px 16px;transition:background .12s}
|
||||
.srow:hover{background:color-mix(in srgb,var(--raised) 50%,transparent)}
|
||||
.srow[aria-current="true"]{background:var(--raised)}
|
||||
.srow .rail3{width:3px;height:26px;border-radius:2px;background:var(--hue)}
|
||||
.srow .nm{font-size:13.5px;font-weight:500;line-height:1.3}
|
||||
.srow .what{font-size:11.5px;color:var(--faint);line-height:1.3}
|
||||
.srow.off .nm{color:var(--muted)}
|
||||
.srow.off .rail3{opacity:.25}
|
||||
.srow .cnt{font:600 11px var(--read);color:var(--faint)}
|
||||
.more{font-size:12px;color:var(--faint);padding:8px 16px}
|
||||
.more:hover{color:var(--ink)}
|
||||
.profile{padding:24px 32px 64px;max-width:760px}
|
||||
.ptitle{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
||||
.ptitle h2{font:600 26px/1.15 var(--read);letter-spacing:-.02em;margin:0;color:var(--hue)}
|
||||
.ptitle .studio{font-size:12.5px;color:var(--faint)}
|
||||
.switch{margin-left:auto;display:flex;align-items:center;gap:10px;font-size:13px;color:var(--muted)}
|
||||
.sw{width:34px;height:20px;border-radius:10px;background:var(--hairline);position:relative;transition:background .14s;flex:none}
|
||||
.sw::after{content:"";position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:var(--muted);transition:transform .14s,background .14s}
|
||||
.sw[aria-checked="true"]{background:color-mix(in srgb,var(--hue,var(--near)) 70%,var(--ground))}
|
||||
.sw[aria-checked="true"]::after{transform:translateX(14px);background:var(--ink-strong)}
|
||||
.pdesc{color:var(--muted);font-size:13.5px;margin:10px 0 0;max-width:60ch}
|
||||
.ladder{margin:24px 0 8px;display:grid;grid-template-columns:repeat(4,1fr);border:1px solid var(--hairline);border-radius:10px;overflow:hidden}
|
||||
.ladder button{padding:12px 12px 11px;text-align:left;border-left:1px solid var(--hairline);transition:background .12s}
|
||||
.ladder button:first-child{border-left:0}
|
||||
.ladder button:hover{background:color-mix(in srgb,var(--raised) 50%,transparent)}
|
||||
.ladder button[aria-pressed="true"]{background:color-mix(in srgb,var(--hue) 14%,transparent);box-shadow:inset 0 -2px 0 var(--hue)}
|
||||
.ladder .ln{font-size:13px;font-weight:600;display:block}
|
||||
.ladder .ls{font-size:11.5px;color:var(--faint);display:block;margin-top:2px;line-height:1.3}
|
||||
.ladnote{font-size:12px;color:var(--faint);min-height:18px}
|
||||
.grp{margin-top:26px}
|
||||
.grp h3{display:flex;align-items:baseline;gap:10px;font:600 12px/1 var(--read);letter-spacing:.05em;color:var(--muted);margin:0 0 4px;padding-bottom:8px;border-bottom:1px solid var(--hairline)}
|
||||
.grp h3 span{font:400 12px var(--lang);letter-spacing:0;color:var(--faint)}
|
||||
.grp h3 .r{margin-left:auto}
|
||||
.trow{display:flex;align-items:center;gap:14px;padding:11px 0;border-bottom:1px solid color-mix(in srgb,var(--hairline) 55%,transparent)}
|
||||
.trow .lbl{flex:1;font-size:14px;line-height:1.35}
|
||||
.trow .lbl small{display:block;font-size:12px;color:var(--faint)}
|
||||
.trow .when{font:600 12px var(--read);color:var(--faint);white-space:nowrap}
|
||||
.trow.sub{padding-left:24px}
|
||||
.trow.sub .lbl{font-size:13px;color:var(--muted)}
|
||||
.radio{display:flex;gap:10px;align-items:flex-start;padding:10px 0;width:100%;text-align:left}
|
||||
.radio .dot{width:16px;height:16px;border-radius:50%;border:1px solid var(--faint);flex:none;margin-top:2px;display:grid;place-items:center}
|
||||
.radio[aria-checked="true"] .dot{border-color:var(--hue)}
|
||||
.radio[aria-checked="true"] .dot::after{content:"";width:8px;height:8px;border-radius:50%;background:var(--hue)}
|
||||
.radio .lbl{font-size:14px}
|
||||
.radio .lbl small{display:block;font-size:12px;color:var(--faint)}
|
||||
.form{display:flex;gap:8px;flex-wrap:wrap;padding:12px 0}
|
||||
.input,select.input{background:var(--ground);border:1px solid var(--hairline);border-radius:8px;color:var(--ink);font:400 13.5px var(--lang);padding:8px 12px}
|
||||
.input:focus{border-color:var(--faint);outline:none}
|
||||
.seg{display:inline-flex;border:1px solid var(--hairline);border-radius:8px;padding:2px;gap:2px}
|
||||
.seg button{font:500 12px/1 var(--lang);padding:7px 11px;border-radius:6px;color:var(--faint)}
|
||||
.seg button[aria-pressed="true"]{background:var(--raised);color:var(--ink)}
|
||||
.flash{animation:flash 1.2s ease-out}
|
||||
@keyframes flash{0%{background:color-mix(in srgb,var(--near) 22%,transparent)}100%{background:transparent}}
|
||||
|
||||
/* archive */
|
||||
.arch{padding:20px 16px 64px;max-width:900px}
|
||||
.arch h2{font:600 24px/1.2 var(--read);margin:0 0 4px;letter-spacing:-.02em}
|
||||
.arow{display:grid;grid-template-columns:3px 1fr auto;gap:0 14px;align-items:center;padding:13px 0;border-bottom:1px solid var(--hairline)}
|
||||
.arow .rail3{width:3px;height:30px;border-radius:2px;background:var(--hue);opacity:.5}
|
||||
.arow .t{font-size:14.5px;color:var(--muted)}
|
||||
.arow.done .t{text-decoration:line-through;text-decoration-color:var(--faint)}
|
||||
.tag{font:500 10px/1 var(--lang);padding:3px 6px;border-radius:3px;margin-right:6px}
|
||||
.tag.d{background:color-mix(in srgb,var(--near) 16%,transparent);color:var(--near)}
|
||||
.tag.i{border:1px solid var(--hairline);color:var(--faint)}
|
||||
|
||||
/* onboarding */
|
||||
.ob{max-width:640px;margin:0 auto;padding:40px 16px 80px}
|
||||
.ob h1{font:600 28px/1.15 var(--read);letter-spacing:-.02em;margin:28px 0 8px}
|
||||
.ob .lead{color:var(--muted);margin:0 0 24px;max-width:52ch}
|
||||
.steps{display:flex;gap:6px;margin-top:24px}
|
||||
.steps i{height:3px;flex:1;border-radius:2px;background:var(--hairline)}
|
||||
.steps i.on{background:var(--near)}
|
||||
.pick{display:grid;grid-template-columns:1fr 1fr;gap:0 24px}
|
||||
.pick button{display:flex;align-items:center;gap:12px;padding:11px 0;border-bottom:1px solid var(--hairline);text-align:left;font-size:14px;color:var(--muted)}
|
||||
.pick button .box{width:18px;height:18px;border-radius:5px;border:1px solid var(--faint);display:grid;place-items:center;flex:none;color:transparent}
|
||||
.pick button .box svg{width:12px;height:12px}
|
||||
.pick button[aria-pressed="true"]{color:var(--ink)}
|
||||
.pick button[aria-pressed="true"] .box{background:var(--hue);border-color:var(--hue);color:var(--ground)}
|
||||
.obfoot{display:flex;align-items:center;gap:16px;margin-top:28px;flex-wrap:wrap}
|
||||
.obfoot .q{font-size:12.5px;color:var(--faint)}
|
||||
.obfoot .q:hover{color:var(--ink)}
|
||||
.pre{display:block;width:100%;text-align:left;padding:14px 16px;border:1px solid var(--hairline);border-radius:12px;margin-bottom:8px;transition:border-color .12s,background .12s}
|
||||
.pre:hover{border-color:var(--faint)}
|
||||
.pre[aria-pressed="true"]{border-color:var(--hue);background:color-mix(in srgb,var(--hue) 9%,transparent)}
|
||||
.pre .pn{font-weight:600;font-size:14px}
|
||||
.pre .pp{font-size:12.5px;color:var(--faint);margin-top:3px;line-height:1.45}
|
||||
.pre[aria-pressed="true"] .pp{color:var(--muted)}
|
||||
.map{border:1px solid var(--hairline);border-radius:12px;padding:16px;margin:8px 0 0}
|
||||
.map .row{display:flex;gap:14px;align-items:flex-start;padding:12px 0;border-top:1px solid var(--hairline)}
|
||||
.map .row:first-child{border-top:0;padding-top:0}
|
||||
.map .ic{width:32px;height:32px;border-radius:8px;border:1px solid var(--hairline);display:grid;place-items:center;flex:none;color:var(--muted)}
|
||||
.map .ic svg{width:16px;height:16px}
|
||||
.map .row p{margin:0;font-size:13.5px;color:var(--muted)}
|
||||
.map .row b{color:var(--ink);font-weight:600;display:block}
|
||||
|
||||
/* sheets */
|
||||
.scrim{position:fixed;inset:var(--bar) 0 0 0;background:var(--scrim);z-index:40;display:flex;justify-content:flex-end}
|
||||
.scrim.center{justify-content:center;align-items:center;padding:16px}
|
||||
.sheet{background:var(--surface);border-left:1px solid var(--hairline);width:min(440px,100%);height:100%;overflow:auto;padding:20px 20px 40px}
|
||||
.scrim.center .sheet{height:auto;border:1px solid var(--hairline);border-radius:16px;max-width:480px}
|
||||
.sheet h3{font:600 20px/1.2 var(--read);margin:4px 0 6px;color:var(--hue)}
|
||||
.sheet .grp h3{font:600 12px/1 var(--read);letter-spacing:.05em;color:var(--muted);margin:0 0 4px}
|
||||
.sheet .x{float:right;width:28px;height:28px;border-radius:8px;display:grid;place-items:center;color:var(--faint)}
|
||||
.sheet .x:hover{color:var(--ink);background:var(--raised)}
|
||||
.sheet .x svg{width:14px;height:14px}
|
||||
.sheet p{color:var(--muted);font-size:13.5px;margin:0 0 10px}
|
||||
.btnrow{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap}
|
||||
|
||||
@media (max-width:960px){
|
||||
.cols,.settings{grid-template-columns:1fr}
|
||||
.rail{position:static;max-height:none;border-right:0;border-bottom:1px solid var(--hairline)}
|
||||
.slist{border-right:0;border-bottom:1px solid var(--hairline)}
|
||||
.profile{padding:20px 16px 48px}
|
||||
}
|
||||
@media (max-width:560px){
|
||||
header.top{flex-wrap:wrap}
|
||||
.tabs{width:100%}
|
||||
.tabs button{flex:1;justify-content:center;padding:8px 6px}
|
||||
.tabs .tl{display:none}
|
||||
.ladder{grid-template-columns:1fr 1fr}
|
||||
.ladder button:nth-child(3){border-left:0}
|
||||
.ladder button:nth-child(n+3){border-top:1px solid var(--hairline)}
|
||||
.pick{grid-template-columns:1fr}
|
||||
.callout{width:min(300px,calc(100vw - 32px))}
|
||||
.numeral{font-size:38px}
|
||||
}
|
||||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mock" id="mock"></div>
|
||||
<aside class="why-panel" id="why" aria-label="Why and foundation"></aside>
|
||||
<div class="app" id="app"></div>
|
||||
|
||||
<script>
|
||||
/* ---------- real content (from the shipped feed and routines.ts) ---------- */
|
||||
const NOW = Date.parse("2026-10-16T12:00:00Z");
|
||||
const GAMES = [
|
||||
{id:"genshin",name:"Genshin Impact",short:"Genshin",hue:"#4EA8DE",studio:"HoYoverse",chores:"Commissions, resin"},
|
||||
{id:"hsr",name:"Honkai: Star Rail",short:"Star Rail",hue:"#7B8CFF",studio:"HoYoverse",chores:"Daily training, Trailblaze Power"},
|
||||
{id:"zzz",name:"Zenless Zone Zero",short:"ZZZ",hue:"#F2A03D",studio:"HoYoverse",chores:"Daily missions, battery"},
|
||||
{id:"wuwa",name:"Wuthering Waves",short:"Wuwa",hue:"#3DD6A0",studio:"Kuro Games",chores:"Daily activity, waveplate"},
|
||||
{id:"endfield",name:"Arknights: Endfield",short:"Endfield",hue:"#E8635A",studio:"Hypergryph",chores:"Daily missions"},
|
||||
{id:"nte",name:"Neverness to Everness",short:"NTE",hue:"#C77DFF",studio:"Hotta Studio",chores:"Daily tasks"},
|
||||
{id:"arknights",name:"Arknights",short:"Arknights",hue:"#9AA3B8",studio:"Hypergryph",chores:"Daily missions, sanity"},
|
||||
{id:"ba",name:"Blue Archive",short:"Blue Archive",hue:"#3FCBDD",studio:"Nexon Games",chores:"Daily missions, AP"},
|
||||
{id:"hi3",name:"Honkai Impact 3rd",short:"Honkai 3rd",hue:"#8B5CF6",studio:"HoYoverse",chores:"Daily missions, stamina"},
|
||||
{id:"nikki",name:"Infinity Nikki",short:"Nikki",hue:"#F27BB0",studio:"Infold Games",chores:"Daily tasks, Vital Energy"},
|
||||
{id:"pgr",name:"Punishing: Gray Raven",short:"PGR",hue:"#CC292B",studio:"Kuro Games",chores:"Daily missions, serum"},
|
||||
{id:"r1999",name:"Reverse: 1999",short:"R1999",hue:"#C9A227",studio:"Bluepoch",chores:"Daily missions"},
|
||||
];
|
||||
const G = Object.fromEntries(GAMES.map(g=>[g.id,g]));
|
||||
// [game, title, unit, cycleDays, kind, target, unit, pulls, daysLeftInCycle]
|
||||
const R = [
|
||||
["genshin","Weekly Bosses","week",0,"amount",3,"runs"],["genshin","Battle Pass weekly EXP","week",0,"amount",10000,"EXP"],
|
||||
["genshin","Reputation Bounties","week",0,"amount",3,"bounties"],["genshin","Reputation Requests","week",0,"amount",3,"requests"],
|
||||
["genshin","Realm Depot: Realm Treasures","week",0,"days",1,""],["genshin","Paimon's Bargains: Intertwined Fates","month",0,"amount",5,"fates","premium"],
|
||||
["genshin","Paimon's Bargains: Acquaint Fates","month",0,"amount",5,"fates","standard"],["genshin","Spiral Abyss","month",0,"days",1,""],
|
||||
["genshin","Imaginarium Theater","month",0,"days",1,""],
|
||||
["hsr","Echo of War","week",0,"amount",3,"runs"],["hsr","Nameless Honor weekly EXP","week",0,"amount",8000,"EXP"],
|
||||
["hsr","Embers Exchange: Star Rail Special Pass","month",0,"amount",5,"passes","premium"],["hsr","Embers Exchange: Star Rail Pass","month",0,"amount",5,"passes","standard"],
|
||||
["hsr","Divergent Universe & Currency Wars points","week",0,"amount",18000,"points"],
|
||||
["hsr","Pure Fiction","cycle",42,"days",1,"",null,6],["hsr","Memory of Chaos","cycle",42,"days",1,"",null,20],["hsr","Apocalyptic Shadow","cycle",42,"days",1,"",null,34],
|
||||
["zzz","Ridu Weekly","week",0,"amount",2100,"points"],["zzz","Bounty Commissions","week",0,"amount",8000,"points"],
|
||||
["zzz","Notorious Hunt","week",0,"amount",3,"runs"],["zzz","New Eridu City Fund weekly EXP","week",0,"amount",10000,"EXP"],
|
||||
["zzz","Signal Shop: Encrypted Master Tapes","month",0,"amount",5,"tapes","premium"],["zzz","Signal Shop: Master Tapes","month",0,"amount",5,"tapes","standard"],
|
||||
["zzz","Signal Shop: Boopons","month",0,"amount",5,"Boopons","premium"],
|
||||
["zzz","Shiyu Defense","cycle",14,"days",1,"",null,9],["zzz","Deadly Assault","cycle",14,"days",1,"",null,2],
|
||||
["endfield","Weekly Routine","week",0,"amount",10,"points"],["endfield","Etchspace Salvage: Focus Commissions","week",0,"amount",3,"commissions"],
|
||||
["wuwa","Weekly Challenge","week",0,"amount",3,"claims"],["wuwa","Pioneer Podcast weekly EXP","week",0,"amount",12000,"EXP"],
|
||||
["wuwa","Weekly Activities","week",0,"amount",6000,"points"],["wuwa","Tower of Adversity","cycle",28,"days",1,"",null,11],
|
||||
["nte","Season Pass weekly EXP","week",0,"amount",12000,"EXP"],["nte","Anomaly Pilgrimage","week",0,"amount",3,"claims"],
|
||||
["nte","City Stamina","week",0,"amount",700,"stamina"],["nte","Realm of Greed","week",0,"days",1,""],
|
||||
["nte","Special City Commission","week",0,"days",1,""],["nte","Beyond the Rails","cycle",14,"days",1,"",null,4],
|
||||
["nte","Tycoon Incentive Fund","cycle",14,"amount",1000000,"fons",null,4],
|
||||
];
|
||||
const slug=s=>s.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");
|
||||
const ROUT = R.map(([game,title,unit,days,kind,target,u,pulls,left])=>({id:`routine:${game}:${slug(title)}`,game,title,unit,days,kind,target,u,pulls:pulls||null,left:left??null}));
|
||||
const EVENTS = [
|
||||
["genshin","Anniversary Memories Album","2026-09-28","2026-10-18"],["zzz","Clink, Clank, Pinball Knight!","2026-09-10","2026-10-19"],
|
||||
["zzz","Diary of an Orbie Parent","2026-09-30","2026-10-19"],["zzz","Chronicles of the Hobbling Crow","2026-10-03","2026-10-19"],
|
||||
["nte","Stamina Recharge","2026-10-05","2026-10-19"],["zzz","Data Bounty - Combat Simulation","2026-10-14","2026-10-19"],
|
||||
["zzz","All-New Program","2026-09-09","2026-10-20"],["genshin","Predictive Victory Dynamics","2026-10-12","2026-10-20"],
|
||||
["wuwa","Bountiful Crescendo","2026-10-15","2026-10-22"],["hsr","4.6 Planar Fissure","2026-10-12","2026-10-25"],
|
||||
["wuwa","Artisan's Search","2026-10-08","2026-10-26"],["wuwa","Waking Moon Fishing","2026-10-15","2026-11-01"],
|
||||
["genshin","To Temper Thyself and Journey Far","2026-08-10","2026-11-02"],["genshin","Battle Pass - Moontrace","2026-09-23","2026-11-02"],
|
||||
["hsr","Astral Imagea","2026-09-28","2026-11-11"],["hsr","Love, Ghosts & Robots","2026-09-28","2026-11-11"],
|
||||
["hsr","Wishpower UP! Down with Voracity!","2026-09-28","2026-11-11"],["nte","Pukaland Travelogue","2026-09-30","2026-11-11"],
|
||||
["wuwa","3.7 Moonlit Path","2026-09-30","2026-11-11"],["wuwa","Ascendant Aces","2026-09-30","2026-11-11"],
|
||||
["nte","Coal Lumps Treasure","2026-10-08","2026-11-11"],["hsr","Free Gilgamesh or Archer","2026-07-24",null],
|
||||
["endfield","Originium Supply Run","2026-10-09","2026-10-23"],["arknights","Lingering Echoes Rerun","2026-10-13","2026-10-27"],
|
||||
].map(([game,title,s,e])=>({id:`${game}:${slug(title)}:${s}`,game,title,start:Date.parse(s+"T00:00:00Z"),end:e?Date.parse(e+"T00:00:00Z"):null}));
|
||||
|
||||
/* ---------- presets: the four steps every profile offers ---------- */
|
||||
const PRESETS = [
|
||||
{id:"events",name:"Just events",sub:"No chores, no tasks"},
|
||||
{id:"dailies",name:"Dailies",sub:"Today's chores"},
|
||||
{id:"weeklies",name:"Dailies + weeklies",sub:"Adds the weekly reset"},
|
||||
{id:"all",name:"Everything",sub:"Monthlies, cycles, pulls"},
|
||||
];
|
||||
const routinesOf = g => ROUT.filter(r=>r.game===g);
|
||||
function applyPreset(p, preset){
|
||||
p.chores = preset!=="events";
|
||||
for (const r of routinesOf(p.game)) {
|
||||
p.routines[r.id] = preset==="all" ? r.pulls!=="standard" : preset==="weeklies" ? (r.unit==="week" && !r.pulls) : false;
|
||||
}
|
||||
if (preset!=="all") p.standardPulls=false;
|
||||
}
|
||||
function presetOf(p){
|
||||
for (const pr of PRESETS){
|
||||
const t={game:p.game,routines:{},chores:false,standardPulls:p.standardPulls};
|
||||
applyPreset(t,pr.id);
|
||||
if (t.chores===p.chores && routinesOf(p.game).every(r=> r.pulls==="standard" ? true : !!t.routines[r.id]===!!p.routines[r.id])) return pr.id;
|
||||
}
|
||||
return "custom";
|
||||
}
|
||||
|
||||
/* ---------- state: one GameProfile per game is the single source ---------- */
|
||||
let S;
|
||||
function fresh(kind){
|
||||
const followed = ["genshin","hsr","zzz","wuwa","endfield","nte"];
|
||||
const start = {genshin:"weeklies",hsr:"all",zzz:"dailies",wuwa:"weeklies",endfield:"dailies",nte:"events"};
|
||||
const profiles = {};
|
||||
for (const g of GAMES){
|
||||
const p = {game:g.id,followed:followed.includes(g.id),chores:false,routines:{},standardPulls:false,placement:"rail"};
|
||||
applyPreset(p, start[g.id]||"events");
|
||||
profiles[g.id]=p;
|
||||
}
|
||||
profiles.nte.placement="focus";
|
||||
return {
|
||||
view:"main", sel:"zzz", focus:"all", sheet:null, open:{hsr:true}, returning:kind==="returning", callout:false,
|
||||
profiles,
|
||||
chores:{genshin:true,hsr:false},
|
||||
prog:{"routine:genshin:weekly-bosses":2,"routine:genshin:battle-pass-exp":6400,"routine:genshin:reputation-bounties":3,"routine:hsr:echo-of-war":3,"routine:hsr:nameless-honor-weekly-exp":8000,"routine:hsr:pure-fiction":1,"routine:hsr:divergent-universe-currency-wars-points":11200,"routine:wuwa:weekly-challenge":1},
|
||||
tasks:[{id:"mytask:a",game:"genshin",title:"Weekly co-op with friends",unit:"week",kind:"days",target:1},{id:"mytask:b",game:"zzz",title:"Hollow Zero run with the group",unit:"week",kind:"days",target:1}],
|
||||
ownGames:[{name:"Snowbreak: Containment Zone",hue:"#9ad1ff"}],
|
||||
ownEvents:[{title:"Snowbreak anniversary login",game:"Snowbreak: Containment Zone",end:"Oct 30"}],
|
||||
archive:{"zzz:diary-of-an-orbie-parent:2026-09-30":{k:"done",at:"Oct 14"},"wuwa:artisan-s-search:2026-10-08":{k:"done",at:"Oct 15"},"hsr:free-gilgamesh-or-archer:2026-07-24":{k:"ignored",at:"Aug 2"},"nte:coal-lumps-treasure:2026-10-08":{k:"ignored",at:"Oct 9"}},
|
||||
archFilter:"all", archGame:"all",
|
||||
ob:{step:1,picked:[],idx:0,choice:{}},
|
||||
addOpen:false,
|
||||
};
|
||||
}
|
||||
S = fresh("main");
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
const ic = {
|
||||
gear:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="8" cy="8" r="2.2"/><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M3.4 12.6l1.4-1.4M11.2 4.8l1.4-1.4"/></svg>',
|
||||
chev:'<svg class="chev" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M6 3.5 10.5 8 6 12.5"/></svg>',
|
||||
check:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3.5 8.5 6.5 11.5 12.5 4.5"/></svg>',
|
||||
plus:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M8 3v10M3 8h10"/></svg>',
|
||||
x:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 4l8 8M12 4l-8 8"/></svg>',
|
||||
list:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5.5 4h8M5.5 8h8M5.5 12h8M2.5 4h.5M2.5 8h.5M2.5 12h.5"/></svg>',
|
||||
bars:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h7M5 8h9M3 12h6"/></svg>',
|
||||
box:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="12" height="3" rx="1"/><path d="M3 6v6.5h10V6M6.5 9h3"/></svg>',
|
||||
};
|
||||
const esc=s=>String(s).replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
|
||||
const hue=id=>G[id]?.hue||"#9aa3b8";
|
||||
function left(ms){const h=ms/36e5; if(h<24) return `${Math.floor(h)}h ${Math.floor((ms%36e5)/6e4)}m`; const d=Math.floor(h/24); return `${d}d ${Math.floor(h%24)}h`;}
|
||||
function heat(ms){ if(ms==null) return "var(--faint)"; const h=ms/36e5; return h<24?"var(--critical)":h<72?"var(--soon)":h<168?"var(--near)":"var(--calm)"; }
|
||||
function meter(e,n=24){
|
||||
if(e.end==null) return `<div class="meter unknown" aria-label="End date unknown"></div>`;
|
||||
const tot=e.end-e.start, rem=Math.max(0,e.end-NOW); const lit=Math.max(1,Math.round(n*rem/tot));
|
||||
return `<div class="meter" style="--tick:${heat(rem)}">${Array.from({length:n},(_,i)=>`<i class="${i<lit?"on":""}"></i>`).join("")}</div>`;
|
||||
}
|
||||
const fmtDay=t=>new Date(t).toLocaleDateString("en-GB",{day:"numeric",month:"short",timeZone:"UTC"});
|
||||
const P = id => S.profiles[id];
|
||||
const followed = () => GAMES.filter(g=>P(g.id).followed);
|
||||
const visibleHere = id => P(id).followed && (P(id).placement==="rail" || S.focus===id);
|
||||
const onRoutines = id => routinesOf(id).filter(r=>P(id).routines[r.id] && (r.pulls!=="standard"||P(id).standardPulls));
|
||||
const tasksOf = id => S.tasks.filter(t=>t.game===id);
|
||||
const val = id => S.prog[id]||0;
|
||||
const met = r => val(r.id) >= r.target;
|
||||
const periodLeft = r => r.unit==="week"?"3d":r.unit==="month"?"16d":`${r.left}d`;
|
||||
const unitWord = {week:"this week",month:"this month",cycle:"this cycle"};
|
||||
function profileSummary(id){
|
||||
const p=P(id); if(!p.followed) return "Not followed";
|
||||
const pr=presetOf(p); const n=onRoutines(id).length+tasksOf(id).length;
|
||||
const name = pr==="custom"?"Custom":PRESETS.find(x=>x.id===pr).name;
|
||||
return `${name}${n?` · ${n} task${n>1?"s":""}`:""}${p.placement==="focus"?" · focus only":""}`;
|
||||
}
|
||||
|
||||
/* ---------- views ---------- */
|
||||
function mockbar(){
|
||||
const items=[["main","Main"],["add","Add & choose systems"],["settings","Settings"],["onboard","Onboarding (new user)"],["returning","Returning user"],["archive","Archive"]];
|
||||
const cur = S.mode==="returning"&&S.view==="main"?"returning":S.sheet?.kind==="add"?"add":S.view;
|
||||
document.getElementById("mock").innerHTML = `<b>A · Every game has its own setup</b>${items.map(([k,l])=>`<button data-jump="${k}" aria-current="${cur===k}">${l}</button>`).join("")}<span class="sep"></span><button data-jump="reset">Reset demo</button><button class="why" data-jump="why">Why / foundation</button>`;
|
||||
}
|
||||
function header(){
|
||||
const tabs=[["main","Checklist",ic.list],["timeline","Timeline",ic.bars],["archive","Archive",ic.box]];
|
||||
return `<header class="top"><div><div class="wordmark">EVENT<span>CLOCK</span></div><div class="sub tnum">${liveEvents().length} running · ${followed().length} games</div></div>
|
||||
<div class="tabs" role="tablist">${tabs.map(([k,l,i])=>`<button role="tab" data-view="${k}" aria-label="${l}" aria-selected="${S.view===k}">${i}<span class="tl">${l}</span></button>`).join("")}<span class="divider"></span>
|
||||
<button role="tab" data-view="settings" aria-label="Settings" aria-selected="${S.view==="settings"}" class="${S.callout?"hl":""}">${ic.gear}<span class="tl">Settings</span></button>
|
||||
${S.callout?`<div class="callout" role="dialog"><b>Your games are set up here now.</b> Each game has one page for its dailies, weeklies, monthlies, cycles and pulls, plus your own tasks for it. The gear next to any heading in the left column opens the right game's page directly.<br><button class="qbtn" data-act="callout-off">Got it</button></div>`:""}</div></header>`;
|
||||
}
|
||||
function liveEvents(){ return EVENTS.filter(e=>visibleHere(e.game) && !S.archive[e.id] && (S.focus==="all"||S.focus===e.game)).sort((a,b)=>(a.end??9e15)-(b.end??9e15)); }
|
||||
|
||||
function railToday(){
|
||||
const gs=followed().filter(g=>visibleHere(g.id)&&P(g.id).chores&&(S.focus==="all"||S.focus===g.id));
|
||||
const done=gs.filter(g=>S.chores[g.id]).length;
|
||||
return `<section><div class="legend">TODAY <span class="tnum">${gs.length?`${done}/${gs.length}`:""}</span><span class="r"><span class="tnum">resets in 3h 38m</span><button class="gear" data-act="gear" data-game="${gs[0]?.id||S.sel}" aria-label="Set up dailies">${ic.gear}</button></span></div>
|
||||
${gs.length?`<div class="glines">${gs.map(g=>`<div class="gline" style="--hue:${g.hue}"><span class="rail3"></span><div><div class="nm">${g.short}</div><div class="what">${g.chores}</div></div><div class="end"><button class="tick ${S.chores[g.id]?"done":""}" data-act="chore" data-game="${g.id}" aria-label="${g.short} dailies done" aria-pressed="${!!S.chores[g.id]}">${ic.check}</button></div></div>`).join("")}</div>`
|
||||
:`<p class="empty">No dailies yet. Turn them on for a game in its <button class="link" data-act="gear" data-game="${S.sel}">setup</button>.</p>`}</section>`;
|
||||
}
|
||||
function railPeriod(){
|
||||
const gs=followed().filter(g=>visibleHere(g.id)&&(onRoutines(g.id).length||tasksOf(g.id).length)&&(S.focus==="all"||S.focus===g.id));
|
||||
return `<section><div class="legend">THIS WEEK, MONTH AND CYCLE<span class="r"><button class="gear" data-act="gear" data-game="${gs[0]?.id||S.sel}" aria-label="Set up weekly tasks">${ic.gear}</button></span></div>
|
||||
${gs.length?`<div class="glines">${gs.map(g=>{
|
||||
const rs=onRoutines(g.id), ts=tasksOf(g.id);
|
||||
const wk=rs.filter(r=>r.unit==="week"), mo=rs.filter(r=>r.unit==="month"), cy=rs.filter(r=>r.unit==="cycle");
|
||||
const wkDone=wk.filter(met).length+ts.filter(t=>val(t.id)>=t.target).length, wkAll=wk.length+ts.length;
|
||||
const bits=[]; if(wkAll) bits.push(`${wkDone} of ${wkAll} this week`); if(mo.length) bits.push(`${mo.filter(met).length}/${mo.length} month`);
|
||||
if(cy.length){const soon=cy.filter(r=>!met(r)).sort((a,b)=>a.left-b.left)[0]; bits.push(soon?`cycle ${soon.left}d`:"cycles done");}
|
||||
const open=S.open[g.id];
|
||||
return `<div class="gline ${open?"open":""}" style="--hue:${g.hue}"><span class="rail3"></span>
|
||||
<button class="btn" style="text-align:left" data-act="expand" data-game="${g.id}" aria-expanded="${!!open}"><div class="nm">${g.short}</div><div class="what go tnum">${bits.join(" · ")}</div></button>
|
||||
<div class="end"><button data-act="expand" data-game="${g.id}" aria-label="Show ${g.short} tasks">${ic.chev}</button></div>
|
||||
${open?`<div class="expand">${[...rs,...ts.map(t=>({...t,mine:true}))].map(item).join("")}
|
||||
<button class="addhere" data-act="quickadd" data-game="${g.id}">${ic.plus} Add to ${g.short}</button></div>`:""}
|
||||
</div>`;}).join("")}</div>`
|
||||
:`<p class="empty">Nothing on a weekly or longer reset yet. Each game's setup has its weeklies, monthlies and cycles: <button class="link" data-act="gear" data-game="${S.sel}">choose some</button>.</p>`}</section>`;
|
||||
}
|
||||
function item(r){
|
||||
const v=val(r.id), done=v>=r.target;
|
||||
const when = r.unit==="cycle"?`${r.left}d left`:r.unit==="month"?"month":"week";
|
||||
const ctl = r.kind==="days"
|
||||
? `<button class="tick ${done?"done":""}" data-act="tickr" data-id="${r.id}" aria-pressed="${done}" aria-label="${esc(r.title)} done">${ic.check}</button>`
|
||||
: `<span class="amt tnum"><input inputmode="numeric" data-act="amt" data-id="${r.id}" value="${v}" aria-label="${esc(r.title)} amount">/ ${r.target.toLocaleString("en")}</span>`;
|
||||
return `<div class="item ${done?"met":""}"><span class="lbl">${esc(r.title)}${r.mine?'<span class="mine">yours</span>':""}<small class="tnum">${when}${r.u?` · ${r.u}`:""}</small></span>${ctl}</div>`;
|
||||
}
|
||||
function headline(){
|
||||
const ev=liveEvents(); const e=ev.find(x=>x.end!=null);
|
||||
if(!e) return `<section><div class="legend">NEXT TO EXPIRE</div><p class="empty">Nothing running for the games on show.</p></section>`;
|
||||
const rem=e.end-NOW;
|
||||
return `<section class="headline" style="--heat:${heat(rem)}"><div class="wash"></div><div class="legend">NEXT TO EXPIRE</div>
|
||||
<h2>${esc(e.title)}</h2><div class="game" style="color:${hue(e.game)}">${G[e.game].name}</div>
|
||||
<div class="numeral tnum" style="color:${heat(rem)}">${left(rem)}<small>left<br>ends ${fmtDay(e.end)}</small></div>${meter(e,22)}
|
||||
<ul class="then">${ev.filter(x=>x!==e).slice(0,2).map(x=>`<li><span class="dot" style="background:${hue(x.game)}"></span>${esc(x.title)}<span class="t tnum" style="color:${heat(x.end&&x.end-NOW)}">${x.end?left(x.end-NOW):"end unknown"}</span></li>`).join("")}</ul></section>`;
|
||||
}
|
||||
function mainView(){
|
||||
const fs=followed().filter(g=>P(g.id).followed);
|
||||
const ev=liveEvents();
|
||||
const all=EVENTS.filter(e=>P(e.game).followed&&P(e.game).placement==="rail"&&!S.archive[e.id]).length;
|
||||
return `<div class="cols"><aside class="rail">${headline()}${railToday()}${railPeriod()}</aside>
|
||||
<main>${S.returning?`<div class="banner"><p><b>Dailies, weeklies, monthlies and cycles are now set up one game at a time.</b> We kept everything you had switched on. Games and the old "What you see" options are in Settings now.</p><button class="cbtn" data-act="showme">Show me</button><button class="qbtn" data-act="dismiss">Dismiss</button></div>`:""}
|
||||
<div class="listhead"><div class="legend" style="min-height:0">RUNNING NOW</div><div class="focus" role="group" aria-label="Focus on a game">
|
||||
<button data-act="focus" data-game="all" aria-pressed="${S.focus==="all"}">All<span class="n">${all}</span></button>
|
||||
${fs.map(g=>`<button data-act="focus" data-game="${g.id}" aria-pressed="${S.focus===g.id}" style="--fh:${g.hue}">${g.short}<span class="n">${EVENTS.filter(e=>e.game===g.id&&!S.archive[e.id]).length}</span></button>`).join("")}</div></div>
|
||||
${ev.map(e=>{const rem=e.end&&e.end-NOW; return `<button class="erow" data-act="event" data-id="${e.id}" style="--hue:${hue(e.game)}"><span class="rail3"></span><div><div class="eyebrow">${G[e.game].short.toUpperCase()}</div><div class="etitle"><span>${esc(e.title)}</span><span class="rd tnum" style="color:${heat(rem)}">${e.end?left(rem):"end date unknown"}</span></div>${meter(e,30)}<div class="emeta tnum">${fmtDay(e.start)} – ${e.end?fmtDay(e.end):"?"}</div></div>${ic.chev}</button>`}).join("")}
|
||||
${followed().some(g=>P(g.id).placement==="focus")&&S.focus==="all"?`<p class="mutednote">${followed().filter(g=>P(g.id).placement==="focus").map(g=>g.short).join(", ")} only show${followed().filter(g=>P(g.id).placement==="focus").length>1?"":"s"} when you focus on ${followed().filter(g=>P(g.id).placement==="focus").length>1?"them":"it"}. Change that in its setup.</p>`:""}
|
||||
</main></div>`;
|
||||
}
|
||||
|
||||
function settingsView(){
|
||||
const fl=GAMES.filter(g=>P(g.id).followed), nf=GAMES.filter(g=>!P(g.id).followed);
|
||||
const sel=S.sel;
|
||||
const row=g=>`<button class="srow ${P(g.id).followed?"":"off"}" data-act="sel" data-game="${g.id}" aria-current="${sel===g.id}" style="--hue:${g.hue}"><span class="rail3"></span><span><span class="nm">${g.name}</span><br><span class="what">${profileSummary(g.id)}</span></span>${ic.chev}</button>`;
|
||||
const gen=[["general","Region and appearance"],["lists","Lists"],["own","Your own events and games"],["backup","Backup"]];
|
||||
return `<div class="settings"><nav class="slist" aria-label="Settings">
|
||||
<h4>YOUR GAMES</h4>${fl.map(row).join("")}
|
||||
<h4>OTHER GAMES</h4>${nf.slice(0,S.allGames?99:3).map(row).join("")}${!S.allGames&&nf.length>3?`<button class="more" data-act="allgames">Show ${nf.length-3} more games</button>`:""}
|
||||
<h4>GENERAL</h4>${gen.map(([k,l])=>`<button class="srow" data-act="sel" data-game="${k}" aria-current="${sel===k}" style="--hue:transparent"><span class="rail3"></span><span class="nm">${l}</span>${ic.chev}</button>`).join("")}
|
||||
</nav><div class="profile" id="profile">${G[sel]?profile(sel):generalPane(sel)}</div></div>`;
|
||||
}
|
||||
function profile(id){
|
||||
const g=G[id], p=P(id), pr=presetOf(p), rs=routinesOf(id);
|
||||
const groups=[["Weekly","resets Monday",rs.filter(r=>r.unit==="week"&&!r.pulls)],["Monthly","resets on the 1st",rs.filter(r=>r.unit==="month"&&!r.pulls)],["Cycles","endgame modes on their own clock",rs.filter(r=>r.unit==="cycle")]];
|
||||
const pulls=rs.filter(r=>r.pulls==="premium"), std=rs.filter(r=>r.pulls==="standard");
|
||||
const t=r=>`<div class="trow"><span class="lbl">${esc(r.title)}<small class="tnum">${r.kind==="amount"?`${r.target.toLocaleString("en")} ${r.u}`:"Once"}${r.unit==="cycle"?` · every ${r.days} days · ${r.left}d left now`:""}</small></span><button class="sw" role="switch" data-act="rt" data-id="${r.id}" aria-checked="${!!p.routines[r.id]}" aria-label="${esc(r.title)}"></button></div>`;
|
||||
return `<div style="--hue:${g.hue}">
|
||||
<div class="ptitle"><h2>${g.name}</h2><span class="studio">${g.studio}</span><label class="switch">Following<button class="sw" role="switch" data-act="follow" data-game="${id}" aria-checked="${p.followed}" aria-label="Follow ${g.name}"></button></label></div>
|
||||
${!p.followed?`<p class="pdesc">You're not following ${g.name}, so none of its events, chores or tasks show anywhere. Follow it to set it up.</p>`:`
|
||||
<p class="pdesc">Everything ${g.short} adds to your page is chosen here. Start from a level, then switch single items on or off.</p>
|
||||
<div class="ladder" role="group" aria-label="How much to track">${PRESETS.map(x=>`<button data-act="preset" data-p="${x.id}" aria-pressed="${pr===x.id}"><span class="ln">${x.name}</span><span class="ls">${x.sub}</span></button>`).join("")}</div>
|
||||
<div class="ladnote">${pr==="custom"?"Custom: you've changed single items. Pick a level to start over.":""}</div>
|
||||
<div class="grp"><h3>DAILY<span>resets 04:00 server time</span></h3><div class="trow"><span class="lbl">Daily chores<small>${g.chores}</small></span><button class="sw" role="switch" data-act="chores" aria-checked="${p.chores}" aria-label="Daily chores"></button></div></div>
|
||||
${groups.filter(x=>x[2].length).map(([n,s,list])=>`<div class="grp"><h3>${n.toUpperCase()}<span>${s}</span><span class="r tnum">${list.filter(r=>p.routines[r.id]).length} of ${list.length}</span></h3>${list.map(t).join("")}</div>`).join("")}
|
||||
${pulls.length?`<div class="grp"><h3>PULLS<span>shop items that buy gacha pulls</span></h3>${pulls.map(t).join("")}
|
||||
${std.length?`<div class="trow sub"><span class="lbl">Also standard-banner pulls<small>${std.map(r=>esc(r.title)).join(", ")}</small></span><button class="sw" role="switch" data-act="std" aria-checked="${p.standardPulls}" aria-label="Standard pulls"></button></div>`:""}</div>`:""}
|
||||
<div class="grp" id="yourtasks"><h3>YOUR OWN TASKS FOR ${g.short.toUpperCase()}<span>things the game doesn't track for you</span></h3>
|
||||
${tasksOf(id).map(t=>`<div class="trow"><span class="lbl">${esc(t.title)}<small>Once ${unitWord[t.unit]}</small></span><button class="qbtn" data-act="deltask" data-id="${t.id}">Remove</button></div>`).join("")||`<p class="empty" style="margin:8px 0">None yet.</p>`}
|
||||
${S.addOpen?taskForm(id):`<button class="addhere" data-act="addtask">${ic.plus} Add a task</button>`}</div>
|
||||
<div class="grp"><h3>WHERE ${g.short.toUpperCase()} SHOWS</h3>
|
||||
<button class="radio" role="radio" data-act="place" data-v="rail" aria-checked="${p.placement==="rail"}"><span class="dot"></span><span class="lbl">Everywhere<small>Its events in the list, its chores and tasks in the left column.</small></span></button>
|
||||
<button class="radio" role="radio" data-act="place" data-v="focus" aria-checked="${p.placement==="focus"}"><span class="dot"></span><span class="lbl">Only when I focus on it<small>Out of the way until you pick ${g.short} in the focus row.</small></span></button></div>`}
|
||||
</div>`;
|
||||
}
|
||||
function taskForm(id){
|
||||
return `<div class="form"><input class="input" id="tt" placeholder="What do you do?" style="flex:1;min-width:180px" aria-label="Task name"><select class="input" id="tu" aria-label="How often"><option value="week">Every week</option><option value="month">Every month</option></select><button class="cbtn" data-act="savetask" data-game="${id}">Add task</button><button class="qbtn" data-act="canceltask">Cancel</button></div>`;
|
||||
}
|
||||
function generalPane(k){
|
||||
if(k==="general") return `<div style="--hue:var(--near)"><div class="ptitle"><h2 style="color:var(--ink)">Region and appearance</h2></div>
|
||||
<div class="grp"><h3>SERVER REGION</h3><p class="pdesc" style="margin:8px 0 12px">Daily resets and event ends follow this server.</p><div class="seg">${["Asia","America","Europe"].map(r=>`<button aria-pressed="${r==="Europe"}">${r}</button>`).join("")}</div></div>
|
||||
<div class="grp"><h3>THEME</h3><div class="seg" style="margin-top:12px">${["Dark","Light","System"].map(r=>`<button aria-pressed="${r==="Dark"}">${r}</button>`).join("")}</div></div></div>`;
|
||||
if(k==="lists") return `<div style="--hue:var(--near)"><div class="ptitle"><h2 style="color:var(--ink)">Lists</h2></div><p class="pdesc">The few choices that are about the lists, not about any one game.</p>
|
||||
<div class="grp"><div class="trow"><span class="lbl">Show events that haven't started<small>As their own section under what's running.</small></span><button class="sw" role="switch" aria-checked="false"></button></div>
|
||||
<div class="trow"><span class="lbl">Guess which events repeat daily<small>Reads the source's wording, and is sometimes wrong.</small></span><button class="sw" role="switch" aria-checked="false"></button></div>
|
||||
<div class="trow"><span class="lbl">Finished and ignored events<small>Leave the lists and wait in the Archive tab.</small></span><button class="qbtn" data-view="archive">Open Archive</button></div></div></div>`;
|
||||
if(k==="own") return `<div style="--hue:var(--near)"><div class="ptitle"><h2 style="color:var(--ink)">Your own events and games</h2></div><p class="pdesc">Only what you typed in yourself. Built-in chores and tasks never show up here; they live on each game's page.</p>
|
||||
<div class="grp"><h3>YOUR GAMES</h3>${S.ownGames.map(o=>`<div class="trow" style="--hue:${o.hue}"><span class="lbl">${esc(o.name)}<small>Added by you</small></span><button class="qbtn">Edit</button></div>`).join("")}<button class="addhere">${ic.plus} Add a game</button></div>
|
||||
<div class="grp"><h3>YOUR EVENTS</h3>${S.ownEvents.map(o=>`<div class="trow"><span class="lbl">${esc(o.title)}<small>${esc(o.game)} · ends ${o.end}</small></span><button class="qbtn">Edit</button></div>`).join("")}<button class="addhere">${ic.plus} Add an event</button></div>
|
||||
<div class="grp"><h3>YOUR TASKS</h3>${S.tasks.map(t=>`<div class="trow"><span class="lbl">${esc(t.title)}<small>${G[t.game].short} · once ${unitWord[t.unit]}</small></span><button class="qbtn" data-act="sel" data-game="${t.game}">Open ${G[t.game].short}</button></div>`).join("")}</div></div>`;
|
||||
return `<div style="--hue:var(--near)"><div class="ptitle"><h2 style="color:var(--ink)">Backup</h2></div><p class="pdesc">Everything lives in this browser. Save it to a file to move it to another device.</p><div class="btnrow"><button class="cbtn">Save to a file</button><button class="qbtn">Load a file</button></div></div>`;
|
||||
}
|
||||
|
||||
function archiveView(){
|
||||
const rows=EVENTS.filter(e=>S.archive[e.id]).filter(e=>S.archFilter==="all"||S.archive[e.id].k===S.archFilter).filter(e=>S.archGame==="all"||e.game===S.archGame);
|
||||
const games=[...new Set(EVENTS.filter(e=>S.archive[e.id]).map(e=>e.game))];
|
||||
return `<div class="arch"><h2>Archive</h2><p class="pdesc" style="margin:0 0 18px">Events you finished or chose to ignore. They stay out of your lists until you restore them.</p>
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:center"><div class="seg">${[["all","All"],["done","Finished"],["ignored","Ignored"]].map(([k,l])=>`<button data-act="af" data-v="${k}" aria-pressed="${S.archFilter===k}">${l}</button>`).join("")}</div>
|
||||
<div class="focus"><button data-act="ag" data-game="all" aria-pressed="${S.archGame==="all"}">Every game</button>${games.map(g=>`<button data-act="ag" data-game="${g}" aria-pressed="${S.archGame===g}" style="--fh:${hue(g)}">${G[g].short}</button>`).join("")}</div></div>
|
||||
<div style="margin-top:12px">${rows.map(e=>{const a=S.archive[e.id];return `<div class="arow ${a.k}" style="--hue:${hue(e.game)}"><span class="rail3"></span><div><div class="eyebrow">${G[e.game].short.toUpperCase()}</div><div class="t">${esc(e.title)}</div><div class="emeta"><span class="tag ${a.k==="done"?"d":"i"}">${a.k==="done"?"Finished":"Ignored"}</span>${a.at}${e.end&&e.end<NOW?" · event over":e.end?` · still running, ${left(e.end-NOW)} left`:""}</div></div><button class="qbtn" data-act="restore" data-id="${e.id}">Restore</button></div>`}).join("")||`<p class="empty">Nothing here. Finish or ignore an event from its details and it moves here.</p>`}</div></div>`;
|
||||
}
|
||||
|
||||
function onboardView(){
|
||||
const o=S.ob;
|
||||
const steps=`<div class="steps">${[1,2,3].map(i=>`<i class="${i<=o.step?"on":""}"></i>`).join("")}</div>`;
|
||||
if(o.step===1) return `<div class="ob"><div class="wordmark">EVENT<span>CLOCK</span></div>${steps}<h1>Which games do you play?</h1><p class="lead">You'll see events only for these. Next, you'll choose how much of each game to track.</p>
|
||||
<div class="pick">${GAMES.map(g=>`<button data-act="obpick" data-game="${g.id}" aria-pressed="${o.picked.includes(g.id)}" style="--hue:${g.hue}"><span class="box">${ic.check}</span>${g.name}</button>`).join("")}</div>
|
||||
<div class="obfoot"><button class="cbtn" data-act="ob1" ${o.picked.length?"":"disabled"}>${o.picked.length?`Continue with ${o.picked.length}`:"Pick at least one game"}</button></div></div>`;
|
||||
if(o.step===2){
|
||||
const id=o.picked[o.idx], g=G[id], ch=o.choice[id]||"events", rs=routinesOf(id);
|
||||
const peek=pid=>{const t={game:id,routines:{},chores:false,standardPulls:false};applyPreset(t,pid);const on=rs.filter(r=>t.routines[r.id]);
|
||||
const bits=[]; if(t.chores) bits.push(g.chores.toLowerCase()+" each day"); if(on.length) bits.push(`${on.length} task${on.length>1?"s":""}: ${on.slice(0,3).map(r=>r.title).join(", ")}${on.length>3?", and more":""}`);
|
||||
return pid==="events"?"Only its events, with countdowns. Nothing to tick.": (pid!=="events"&&!rs.length&&pid!=="dailies")?`Same as Dailies: ${g.short} has no built-in weekly tasks yet.`:`Adds ${bits.join("; ")}.`;};
|
||||
return `<div class="ob" style="--hue:${g.hue}"><div class="wordmark">EVENT<span>CLOCK</span></div>${steps}
|
||||
<div class="legend" style="margin-top:28px">GAME ${o.idx+1} OF ${o.picked.length}</div><h1 style="margin-top:8px;color:${g.hue}">How much of ${g.name} do you want to track?</h1><p class="lead">You can change this later in Settings, item by item.</p>
|
||||
${PRESETS.map(x=>`<button class="pre" data-act="obpre" data-p="${x.id}" aria-pressed="${ch===x.id}"><div class="pn">${x.name}</div><div class="pp">${peek(x.id)}</div></button>`).join("")}
|
||||
<div class="obfoot"><button class="cbtn" data-act="ob2">${o.idx<o.picked.length-1?`Next: ${G[o.picked[o.idx+1]].short}`:"Continue"}</button>${o.picked.length-o.idx>1?`<button class="q" data-act="obsame">Use this for the remaining ${o.picked.length-o.idx-1}</button>`:""}<button class="q" data-act="obback">Back</button></div></div>`;
|
||||
}
|
||||
return `<div class="ob"><div class="wordmark">EVENT<span>CLOCK</span></div>${steps}<h1>Where things live</h1><p class="lead">Three places, so you always know where to look.</p>
|
||||
<div class="map"><div class="row"><span class="ic">${ic.list}</span><p><b>Checklist</b>What's ending soon on the right. Today's chores and your weekly tasks on the left, one line per game.</p></div>
|
||||
<div class="row"><span class="ic">${ic.gear}</span><p><b>Settings, and the gears</b>Each game has its own page for what it tracks. The gear next to a heading in the left column takes you straight there.</p></div>
|
||||
<div class="row"><span class="ic">${ic.box}</span><p><b>Archive</b>Events you finish or ignore wait here, and you can put any of them back.</p></div></div>
|
||||
<div class="obfoot"><button class="cbtn" data-act="ob3">Show my events</button><button class="q" data-act="obback">Back</button></div></div>`;
|
||||
}
|
||||
|
||||
function sheet(){
|
||||
if(!S.sheet) return "";
|
||||
if(S.sheet.kind==="event"){
|
||||
const e=EVENTS.find(x=>x.id===S.sheet.id);
|
||||
return `<div class="scrim center" data-act="close"><div class="sheet" style="--hue:${hue(e.game)}" data-stop><button class="x" data-act="close" aria-label="Close">${ic.x}</button><div class="eyebrow">${G[e.game].short.toUpperCase()}</div><h3 style="color:var(--ink)">${esc(e.title)}</h3><p class="tnum">${fmtDay(e.start)} – ${e.end?fmtDay(e.end):"end date unknown"}</p>${meter(e,24)}<div class="btnrow"><button class="cbtn" data-act="arch" data-k="done" data-id="${e.id}">Mark done</button><button class="qbtn" data-act="arch" data-k="ignored" data-id="${e.id}">Ignore</button></div><p style="margin-top:14px;font-size:12px;color:var(--faint)">Either one moves it to the Archive.</p></div></div>`;
|
||||
}
|
||||
const id=S.sheet.game, g=G[id], p=P(id);
|
||||
const off=routinesOf(id).filter(r=>!p.routines[r.id]&&(r.pulls!=="standard"||p.standardPulls));
|
||||
return `<div class="scrim" data-act="close"><div class="sheet" style="--hue:${g.hue}" data-stop><button class="x" data-act="close" aria-label="Close">${ic.x}</button>
|
||||
<div class="legend">ADD TO</div><h3>${g.name}</h3><p>Built-in tasks you're not tracking yet. Switching one on puts it in the left column straight away.</p>
|
||||
${off.length?off.map(r=>`<div class="trow"><span class="lbl">${esc(r.title)}<small class="tnum">${r.unit==="cycle"?`Every ${r.days} days`:r.unit==="week"?"Weekly":"Monthly"}${r.kind==="amount"?` · ${r.target.toLocaleString("en")} ${r.u}`:""}</small></span><button class="sw" role="switch" data-act="rt" data-id="${r.id}" aria-checked="false" aria-label="${esc(r.title)}"></button></div>`).join(""):`<p class="empty">You're tracking every built-in task ${g.short} has.</p>`}
|
||||
<div class="grp"><h3>OR YOUR OWN</h3>${taskForm(id).replace('data-act="canceltask"','data-act="close"')}</div>
|
||||
<div class="btnrow"><button class="qbtn" data-act="gear" data-game="${id}">Open ${g.short}'s full setup</button></div></div></div>`;
|
||||
}
|
||||
|
||||
function why(){
|
||||
document.getElementById("why").innerHTML = `
|
||||
<h3>The idea</h3><p>The game is the unit you customise. Every game you follow has one page that holds everything it adds to your screen: its events, its daily chores, each weekly, monthly and cycle task, its pulls, your own tasks for it, and where it shows up. A four-step level ("Just events" → "Everything") gets you most of the way in one click, and single switches handle the rest. New readers start on "Just events" for every game, so the page starts quiet.</p>
|
||||
<h3>The foundation</h3><p>Today about fifteen preferences each answer part of "what do I see for this game": <code>hiddenGames</code>, <code>knownGames</code>, <code>showChores</code> / <code>choresOn</code> / <code>choresOff</code>, <code>showRoutines</code> / <code>routineGames</code> / <code>routinesOff</code> / <code>routinesAsked</code> / <code>routinesHidden</code>, <code>showWeeklyTasks</code> / <code>showMonthlyTasks</code> / <code>showCycleTasks</code>, and <code>standardPulls</code>. They're spread across four settings panels, which is why turning one thing back on is hard to find. In this model they collapse into one record per game:</p>
|
||||
<pre>GameProfile {
|
||||
game: LaneId
|
||||
followed: boolean
|
||||
chores: boolean
|
||||
routines: { [routineId]: boolean } // explicit, opt-in
|
||||
standardPulls: boolean
|
||||
placement: "everywhere" | "focus-only"
|
||||
}
|
||||
prefs.profiles: Record<LaneId, GameProfile></pre>
|
||||
<p>Every view reads from <code>profiles</code> only: the rail, the lists, the timeline, the counts. A preset is just a function that writes a profile. It's never stored, and "Custom" means the profile doesn't match any preset. Your own tasks keep their own key space and attach to a game by <code>game</code>, so they show on that game's page without being mixed in with the built-in ones.</p>
|
||||
<h3>How it scales</h3><ul><li><b>A new game</b> gets a profile on "Just events", unfollowed, the same thing <code>knownGames</code> does today, but in one place.</li><li><b>A new built-in routine</b> starts off; for readers on a preset that includes its kind, a one-line "New for ZZZ: …" offer appears on that game's page instead of it turning on silently.</li><li><b>A new kind of system</b> (say, a seasonal pass) is one more group on the game page and one more rule in the presets. No new global switch.</li><li>Migration is one pass: work out each game's profile from the old fifteen fields, then drop them.</li></ul>
|
||||
<h3>Tradeoffs</h3><ul><li>Changing things across many games means visiting each one. There's no "turn off monthlies everywhere" here. A bulk row could be added, but it reintroduces a global switch.</li><li>Settings becomes a real page, which is one more place to learn. The gears and the onboarding map are there to soften that.</li><li>The rail stays quieter by collapsing tasks to one line per game. Ticking a task takes one extra tap to expand the game.</li><li>"Only when I focus on it" is a new idea readers have to learn.</li></ul>`;
|
||||
}
|
||||
|
||||
function render(){
|
||||
mockbar();
|
||||
const app=document.getElementById("app");
|
||||
if(S.view==="onboard"){ app.innerHTML=`<div class="shell">${onboardView()}</div>`; return; }
|
||||
const body = S.view==="settings"?settingsView():S.view==="archive"?archiveView():S.view==="timeline"?`<p class="mutednote" style="padding:40px 16px">The timeline isn't part of this mockup. It reads the same game profiles as the checklist, so a game set to "Just events" shows only its event bars.</p>`:mainView();
|
||||
app.innerHTML = `<div class="shell">${header()}${body}</div>${sheet()}`;
|
||||
}
|
||||
|
||||
/* ---------- interaction ---------- */
|
||||
document.addEventListener("click",ev=>{
|
||||
const j=ev.target.closest("[data-jump]");
|
||||
if(j){ const k=j.dataset.jump;
|
||||
if(k==="why"){document.getElementById("why").classList.toggle("open");return;}
|
||||
if(k==="reset"){S=fresh("main");}
|
||||
else if(k==="main"){S.view="main";S.returning=false;S.sheet=null;S.callout=false;}
|
||||
else if(k==="returning"){S=fresh("returning");}
|
||||
else if(k==="settings"){S.view="settings";S.sheet=null;}
|
||||
else if(k==="archive"){S.view="archive";S.sheet=null;}
|
||||
else if(k==="add"){S.view="main";S.sheet={kind:"add",game:"zzz"};}
|
||||
else if(k==="onboard"){S=fresh("main");S.view="onboard";}
|
||||
S.mode=k; render();return; }
|
||||
const v=ev.target.closest("[data-view]");
|
||||
if(v){S.view=v.dataset.view;S.sheet=null;S.callout=false;S.addOpen=false;render();window.scrollTo(0,0);return;}
|
||||
const a=ev.target.closest("[data-act]"); if(!a) return;
|
||||
if(a.dataset.act==="close" && ev.target.closest("[data-stop]") && a.classList.contains("scrim")) return;
|
||||
const act=a.dataset.act, g=a.dataset.game, id=a.dataset.id;
|
||||
const p=S.profiles[S.sheet?.kind==="add"?S.sheet.game:S.sel];
|
||||
switch(act){
|
||||
case "gear": S.view="settings";S.sel=g;S.sheet=null;S.addOpen=false;break;
|
||||
case "chore": S.chores[g]=!S.chores[g];break;
|
||||
case "expand": S.open[g]=!S.open[g];break;
|
||||
case "tickr": S.prog[id]=val(id)>=1?0:1;break;
|
||||
case "focus": S.focus=g;break;
|
||||
case "event": S.sheet={kind:"event",id};break;
|
||||
case "arch": S.archive[id]={k:a.dataset.k,at:"Oct 16"};S.sheet=null;break;
|
||||
case "close": S.sheet=null;break;
|
||||
case "quickadd": S.sheet={kind:"add",game:g};break;
|
||||
case "sel": S.sel=g;S.addOpen=false;if(G[g])S.view="settings";break;
|
||||
case "allgames": S.allGames=true;break;
|
||||
case "follow": P(g).followed=!P(g).followed; if(!P(g).followed&&S.focus===g)S.focus="all"; break;
|
||||
case "preset": applyPreset(p,a.dataset.p);break;
|
||||
case "chores": p.chores=!p.chores;break;
|
||||
case "rt": p.routines[id]=!p.routines[id]; if(S.sheet?.kind==="add") S.open[p.game]=true; break;
|
||||
case "std": p.standardPulls=!p.standardPulls; for(const r of routinesOf(p.game)) if(r.pulls==="standard") p.routines[r.id]=p.standardPulls; break;
|
||||
case "place": p.placement=a.dataset.v;break;
|
||||
case "addtask": S.addOpen=true;break;
|
||||
case "canceltask": S.addOpen=false;break;
|
||||
case "savetask": { const t=document.getElementById("tt").value.trim(); if(!t) return; S.tasks.push({id:"mytask:"+Math.random().toString(36).slice(2,8),game:g,title:t,unit:document.getElementById("tu").value,kind:"days",target:1}); S.addOpen=false; S.open[g]=true; if(S.sheet) S.sheet=null; break; }
|
||||
case "deltask": S.tasks=S.tasks.filter(t=>t.id!==id);break;
|
||||
case "restore": delete S.archive[id];break;
|
||||
case "af": S.archFilter=a.dataset.v;break;
|
||||
case "ag": S.archGame=g;break;
|
||||
case "showme": S.returning=false;S.callout=true;break;
|
||||
case "dismiss": S.returning=false;break;
|
||||
case "callout-off": S.callout=false;S.view="settings";break;
|
||||
case "obpick": { const l=S.ob.picked; S.ob.picked=l.includes(g)?l.filter(x=>x!==g):[...l,g]; break; }
|
||||
case "ob1": S.ob.step=2;S.ob.idx=0;break;
|
||||
case "obpre": S.ob.choice[S.ob.picked[S.ob.idx]]=a.dataset.p;break;
|
||||
case "ob2": if(S.ob.idx<S.ob.picked.length-1)S.ob.idx++;else S.ob.step=3;break;
|
||||
case "obsame": { const c=S.ob.choice[S.ob.picked[S.ob.idx]]||"events"; S.ob.picked.slice(S.ob.idx).forEach(x=>S.ob.choice[x]=c); S.ob.step=3; break; }
|
||||
case "obback": if(S.ob.step===3){S.ob.step=2;S.ob.idx=S.ob.picked.length-1;} else if(S.ob.idx>0)S.ob.idx--; else S.ob.step=1; break;
|
||||
case "ob3": { for(const gm of GAMES){ const pp=S.profiles[gm.id]; pp.followed=S.ob.picked.includes(gm.id); pp.placement="rail"; applyPreset(pp,S.ob.choice[gm.id]||"events"); }
|
||||
S.tasks=[];S.prog={};S.chores={};S.archive={};S.open={};S.view="main";S.focus="all";S.sel=S.ob.picked[0]; break; }
|
||||
default: return;
|
||||
}
|
||||
render();
|
||||
if(act==="gear"){ const pf=document.getElementById("profile"); pf&&pf.classList.add("flash"); }
|
||||
if(act==="addtask"){ document.getElementById("tt")?.focus(); }
|
||||
});
|
||||
document.addEventListener("change",ev=>{
|
||||
const t=ev.target; if(t.dataset.act==="amt"){ S.prog[t.dataset.id]=Math.max(0,parseInt(t.value.replace(/\D/g,""))||0); render(); }
|
||||
});
|
||||
document.addEventListener("keydown",ev=>{ if(ev.key==="Escape"&&S.sheet){S.sheet=null;render();} });
|
||||
why(); render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
{"reason":"stop-server.sh","timestamp":1792128694}
|
||||
@@ -56,7 +56,7 @@ falling back to checked-in fixtures.
|
||||
## Three constraints that shape everything
|
||||
|
||||
1. **No accounts, no logins, no user records.** Completion state lives in the browser's
|
||||
`localStorage`, keyed by event ID. There is no user table and no session. Any request implying
|
||||
`localStorage`, in a v2 envelope keyed by local game/server account and unchanged event ID. There is no user table and no session. Any request implying
|
||||
"sync across devices" is solved with export/import JSON, not a server-side user.
|
||||
2. **No LLM in the pipeline.** Event data is extracted by deterministic code-based parsers only.
|
||||
There is no Anthropic dependency, no API key, and no per-run inference cost. A source that
|
||||
@@ -133,6 +133,15 @@ cycle:
|
||||
run `bun run build:feed`, which builds from snapshots and fixtures) before serving. A worktree
|
||||
also needs its own `bun install`.
|
||||
|
||||
**Browser verification must include an ordinary HTTP network hostname, not only localhost.**
|
||||
Localhost is a secure context even over HTTP, so it conceals API restrictions that affect LAN and
|
||||
WireGuard addresses. On 2026-10-16 the redesign called `crypto.randomUUID()` during first-visit
|
||||
migration: it was undefined on HTTP `10.20.20.2`, and startup mislabeled that TypeError as a saved-data
|
||||
failure. Browser-local IDs now come from `state/reader/ids.ts` using 128-bit `crypto.getRandomValues`,
|
||||
which works on HTTP too. Use that helper for accounts, operations, clock revisions and backups;
|
||||
never remint existing identities. A Chrome test hostname mapped to loopback reproduces the HTTP
|
||||
security boundary without contacting another machine. Test startup and subsequent writes there.
|
||||
|
||||
**`build:js` passes `--production`, and that flag is load-bearing.** Without it Bun bundles React's
|
||||
*development* build: 566 KB rather than 344 KB (164 KB vs 103 KB gzipped), every element creation
|
||||
validated at runtime, and `StrictMode` double-invoking effects — which in this app means
|
||||
@@ -167,8 +176,9 @@ src/ingest/ html.ts, dates.ts (sixteen formats), merge.ts, sanitize.ts, ro
|
||||
parsers/ game8.ts, wikigg.ts, akwiki.ts, fandom.ts, bawiki.ts, holodori.ts, iopwiki.ts,
|
||||
stellasora.ts, arustats.ts, karendar.ts — keyed by SITE, not game
|
||||
adapters/ index.ts — SOURCES registry binding url+game+parser, and the sanitize seam
|
||||
src/client/ React app, service worker, manifest
|
||||
state/ progress, daily log, ignores, prefs, sort — all localStorage
|
||||
src/client/ ReaderApp (shared interfaces), components/redesign/, service worker, manifest
|
||||
state/reader/ active v2 envelope, accounts/clocks, migration, archive, backup/restore
|
||||
state/ legacy v1 helpers, progress validators, prefs normalization, sort
|
||||
gameOrder.ts — the reader's game order, and the A–Z fallback; pure
|
||||
useCustom.ts — the reader's own games and events (PRD F13)
|
||||
lens.ts — who sees which rows (focus, outstanding, next-to-expire); pure
|
||||
@@ -1228,48 +1238,19 @@ to an open page). Four things hold it up:
|
||||
keeps a retired source's slot warm rather than pruning it. `games` itself is left in feed order on
|
||||
purpose: `adoptNewLanes` diffs it and `knownGames` is seeded from it, so ordering it at source
|
||||
would let a display preference reach the code that hides a reader's games.
|
||||
- **Reordering is a settings-only affordance.** The focus bar and the dailies strip are the fastest
|
||||
tap targets in the app, and a drag target on top of a tick target costs somebody a streak the first
|
||||
time it misfires — so the live surfaces are never draggable. Both a handle and ↑↓ buttons ship,
|
||||
because touch fires no drag events at all: the arrows are the mechanism and the drag is the pointer
|
||||
fast path, and being ordinary buttons is what makes the whole thing reachable by keyboard and
|
||||
screen reader without a second implementation of the same interaction.
|
||||
- **The settings panel is groups that state their own answer, and they ship closed.** Six
|
||||
`<details>` groups — games, server region, appearance, what you see, your own, your progress — each
|
||||
with its current state on the summary line (`Europe`, `17 of 18 on · A–Z`, `plus finished, not
|
||||
started`). It was one open block in two columns, which is readable at four games and not at
|
||||
eighteen, where the game list is eighteen rows of four controls sitting above the checkbox somebody
|
||||
came to tick. The state line is the half that makes collapsing honest: without it every question
|
||||
about how the app is set up costs a click, so the closed panel has to be a report and not a menu.
|
||||
Three rules follow. **Do not default a group open** to make something inside it findable — the empty
|
||||
states name the group as well as the switch, which is what makes that unnecessary. The summaries are
|
||||
**derived from `prefs` at render**, never stored, so they cannot drift from the controls they
|
||||
describe. And **one group answers one question**, which is what the sixth group is: the region and
|
||||
the theme were a group called `Reading`, grouped on "both are how do I read this?" and summarising
|
||||
as `Europe · Dark` — two unrelated answers joined by a dot, under a name for neither of them, in a
|
||||
panel whose whole premise is that a closed line answers *its* group. The region is not a reading
|
||||
preference: region-scoped ends and every daily reset are cut on that server's clock (§ Domain
|
||||
rules), so it is the one control here that can make a countdown wrong, and it is now a line of its
|
||||
own rather than filed behind a word for the theme.
|
||||
Native `<details>` for the reason the reorder arrows are ordinary buttons: keyboard and screen
|
||||
reader reach it without a second implementation. `summary` is not an `a`, `button` or `[tabindex]`,
|
||||
so it needs its own `:focus-visible` rule in `styles.css` — the shared one does not reach it.
|
||||
- **The panel is one column of groups, and it uses the page's width.** Those are two claims and only
|
||||
the first survived a desktop read: the rows were held to `max-w-3xl` inside a shell a third wider,
|
||||
so every rule inside the panel stopped short of the panel's own, and it was the one block on a wide
|
||||
page not using the width — between a two-column checklist and a three-column footer that both do.
|
||||
A name at one end of a row and its state at the other is what "running now" already does with a
|
||||
title and its countdown, so full width is the house pattern rather than a stretch. Prose inside a
|
||||
group keeps its own measure, and the game list — the one thing tall enough to need it — takes **two
|
||||
columns of its own past `lg` once there are nine or more games**, flowing down then across so the
|
||||
numbers still read in a straight line and an arrow still swaps a row with its neighbour. Eighteen
|
||||
rows in one column was a 1,000px ribbon with two thirds of the width empty beside it.
|
||||
- **A game's dailies stay together, and grouping never re-sorts inside a group.** `dailyGroups`
|
||||
(`Dailies.tsx`) emits a game's standing chore followed by that game's repeating events, in the
|
||||
order those arrived; it replaced `[...chores, ...repeating]`, which put a game's chore and its own
|
||||
login event at opposite ends of the strip. Collapsed, the grouping is **adjacency only** — no
|
||||
per-game headings, because that strip is the part of the page answerable in ten seconds and a
|
||||
heading per game makes it the tallest block on it, pushing "next to expire" down the page.
|
||||
- **Reordering is a settings-only affordance.** Your games provides ordinary ↑↓ buttons for
|
||||
keyboard, screen reader and touch. Live focus/daily tick targets are never draggable.
|
||||
- **Settings uses named pages.** General, Your games, Appearance, Lists, Region & time,
|
||||
Data & sources, Backup, About Event Clock and Changelog. Keep the complete colophon in About;
|
||||
Changelog has its own page and no footer link. The footer always states data age and links About.
|
||||
Reset overrides live per game/server; display timezone is a separate preference.
|
||||
- **Both interfaces share the reader.** Structured (default, Round 6 B rail) and Relaxed (Round 7)
|
||||
are CSS/layout choices over one feature tree. Theme, palette and row density are independent.
|
||||
Do not mount the old v1 persistence hooks alongside ReaderApp's v2 store. See DATA-MODEL for
|
||||
account IDs, clock revisions, migration recovery, backups and export validation.
|
||||
- **A game's routines stay together.** The separate daily panel keeps direct ticks; grouped
|
||||
routines use per-game/server summaries. Collapse policy is configurable and focus can expand a
|
||||
group without changing that saved policy. Timeline reset strips sit next to their game's events.
|
||||
- **A day already gone is still tickable, and the window bounds display and never data.** People play
|
||||
at midnight and tick at breakfast, so `catchUpDays` (`src/shared/daily.ts`) offers the last
|
||||
`CATCH_UP_DAYS` game-days for a standing chore and for an event whose end was never announced —
|
||||
|
||||
@@ -10,9 +10,12 @@ note only says where to start.
|
||||
([spec](docs/superpowers/specs/2026-10-14-built-in-routines-design.md)), every-N-days cycles for
|
||||
tasks and built-ins ([spec](docs/superpowers/specs/2026-10-16-cycle-routines-design.md)), and
|
||||
battle pass windows as feed events ([spec](docs/superpowers/specs/2026-10-16-pass-windows-design.md),
|
||||
see its § Outcome). Plans are beside them in `docs/superpowers/plans/`. Nothing is in flight: no
|
||||
open branches or worktrees.
|
||||
- **No next task has been chosen.** The open items below are the candidates; the owner decides.
|
||||
see its § Outcome). Plans are beside them in `docs/superpowers/plans/`. The redesign attempt is uncommitted and was rejected for substantial markup divergence.
|
||||
**Start with [the replacement-agent handoff](docs/handoffs/2026-10-16-redesign-fidelity-handoff.md).**
|
||||
The existing plan/ledger contain superseded completion claims; no extra branch/worktree.
|
||||
- **Redesign rejected for markup divergence; presentation correction pending.** See docs/design-reviews/2026-10-16-redesign-implementation.md.
|
||||
The assigned follow-up is to faithfully port the approved markup and finish the redesign.
|
||||
Unrelated open items below remain separate.
|
||||
The ones that need the owner in the game first are marked as checks.
|
||||
- **How the owner works** (their standing instructions, also in their personal Claude settings):
|
||||
once they approve a design, carry it through spec → plan → execution with no extra review stops,
|
||||
@@ -30,6 +33,17 @@ note only says where to start.
|
||||
|
||||
## Open items
|
||||
|
||||
- [ ] **Redesign Settings → Your own events and games** (owner, 2026-10-17: “the split-up from
|
||||
the mockup is nice, but I guess it needs a bigger design”). Deferred on purpose: it works as well
|
||||
as it does today, which is enough for now. Direction from the owner: a heading per game you
|
||||
created, with that game's events listed under it, taking inspiration from the Routines tab —
|
||||
possibly reusing the routine editor (`components/redesign/RoutineDrawer.tsx`, the right-docked
|
||||
create/edit shelf) as the base for an event editor. Not in the Round 6/7 mockups, so it needs its
|
||||
own short design (brainstorm → spec → plan; owner approves once, then carry it through). Keep the
|
||||
current behaviour (`components/YourOwn.tsx`) working until the replacement is accepted.
|
||||
- Owner, same day, an alternative to weigh in that design: drop the separate settings page and
|
||||
put an **Add** button on the Settings sidebar's “Your games” list, opening an event editor
|
||||
there (a new game, or a new event under a game).
|
||||
- [ ] Make it clear which game each *This week & month* chip belongs to (owner, 2026-10-16: "it can
|
||||
become a bit unclear"). With built-ins for seven games, that row now holds many chips whose
|
||||
titles do not name their game — "Weekly Routine", "Weekly Challenge", "City Stamina",
|
||||
@@ -235,3 +249,20 @@ note only says where to start.
|
||||
`EventForm`, `src/client/components/TaskForm.tsx`). Review them as a reader would — field
|
||||
order, defaults, copy, validation messages, how a cadence and a measure are chosen, phone
|
||||
width — against `docs/DESIGN.md` and `docs/PRODUCT.md`.
|
||||
- [ ] Follow up the 2026-10-18 pre-ship review of the v1→v2 reader migration. A schema-guardian review
|
||||
found nothing blocking: event IDs, day keys and the feed contract are unchanged, and v1 keys are
|
||||
never removed, so a rollback finds them byte-identical. These gaps remain, all low severity:
|
||||
- Orphaned v1 entries (marks or amounts for a deleted `myevent:`/`mytask:`, or any entry whose game
|
||||
can't be resolved) are not carried into v2 account data (`state/reader/migration.ts`,
|
||||
`gameOf`). They survive in `legacyRaw`, `gacha-tracker:v2:migration-recovery` and the untouched v1
|
||||
keys. Consider a catch-all bucket so a restored custom item finds them.
|
||||
- Importing a v1 file whose region differs from the reader's account lands its marks in a new
|
||||
`account:import-N`, not the visible account (`state/reader/import.ts`, `export.ts`). Kept, but
|
||||
out of sight.
|
||||
- `initializeReader` reconciles and writes back outside `withReaderLock`, with no compare-and-set
|
||||
(`state/reader/store.ts`). A second tab's write in that window can be overwritten.
|
||||
- Storage pressure: every save carries `legacyRaw`, and every automatic backup embeds the state,
|
||||
`legacyRaw` and the v1 keys again, with no size cap (only manual backups are capped). Heavy
|
||||
readers could reach the ~5 MB quota. Failures are reported, not silent.
|
||||
- After a rollback, anything done in the new build lives only in v2 and isn't shown by the old app.
|
||||
It's stranded, not lost.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Event Clock · Three layouts</title><link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@600;700&family=Barlow:wght@400;500;600&display=swap" rel="stylesheet"><style>
|
||||
:root{color-scheme:dark;--ground:#0e1117;--panel:#141923;--rule:#323b4e;--ink:#e4e7ef;--muted:#afb8c7;--accent:#8eaff6}*{box-sizing:border-box}body{margin:0;background:var(--ground);color:var(--ink);font:16px/1.65 Barlow,system-ui,sans-serif}main{max-width:1000px;margin:0 auto;padding:64px 28px}h1{font:600 44px/1.1 "Barlow Condensed",sans-serif;margin:0 0 20px}h2{font:600 28px/1.2 "Barlow Condensed",sans-serif;margin:0 0 10px}p{max-width:70ch;color:var(--muted);margin:12px 0}a{color:var(--accent);text-underline-offset:4px}:focus-visible{outline:2px solid var(--accent);outline-offset:5px}.design{display:grid;grid-template-columns:54px 1fr auto;gap:24px;align-items:start;padding:32px 0;border-top:1px solid var(--rule)}.letter{font:600 34px "Barlow Condensed",sans-serif;color:var(--muted)}.design .open{align-self:center;padding:10px 16px;border:1px solid var(--rule);text-decoration:none;white-space:nowrap}.tradeoff{font-size:14px}.designs{margin:36px 0}.notes{border-top:1px solid var(--rule);padding-top:24px;font-size:14px}.notes h2{font-size:23px}@media(max-width:650px){main{padding:36px 20px}h1{font-size:36px}.design{grid-template-columns:32px 1fr;gap:16px}.design .open{grid-column:2;justify-self:start}.design h2{font-size:26px}}
|
||||
</style></head><body><main><h1>Give the events room.</h1><p>Three complete layouts for Event Clock. Each keeps the agreed routines, Archive, settings and onboarding, while moving focus and freshness controls out of the way of the event list.</p><p>Compare the same busy calendar in each. The top review bar can simulate older data, open onboarding and reset the sample state.</p><div class="designs">
|
||||
<section class="design"><span class="letter">A</span><div><h2>Inline desk</h2><p>Game focus beside the list heading. A small freshness line with Details. Your one-click dailies and pinned routines stay in the familiar left rail.</p><p class="tradeoff">The smallest change to the layout you already liked; retains two columns.</p></div><a class="open" href="a-inline.html">Open A</a></section>
|
||||
<section class="design"><span class="letter">B</span><div><h2>Game desk</h2><p>Game focus, daily ticks and source status share the left rail. The event heading stays clear, with more attention on the list itself.</p><p class="tradeoff">Controls have one predictable home; the rail holds more, and stacks before events on a phone.</p></div><a class="open" href="b-rail.html">Open B</a></section>
|
||||
<section class="design"><span class="letter">C</span><div><h2>Open ledger</h2><p>A wide event list with a quick daily strip. Other pinned routines open above the list, with your choice of collapsed, expanded or always open.</p><p class="tradeoff">Most space for event titles; daily and routine panels scroll with the list.</p></div><a class="open" href="c-open.html">Open C</a></section>
|
||||
</div><section class="notes"><h2>Shared changes to try</h2><p>Routines → Checklist display now controls the daily panel and group expansion. Numeric fields keep their native one-step arrows.</p><p>Settings includes Changelog as its own page, custom interface palettes, per-game source links and thanks, reset-time overrides with Automatic, and clearly labelled backup previews. Upcoming events is part of What do you want to track.</p><p>Returning user opens a guided tour in your preferred view. Take it or keep browsing; your progress stays intact.</p><p>This is a review prototype. Codes, source health and historical data are illustrative. The production app and earlier mockups are unchanged.</p><p><a href="../round5/converged.html">Compare with Round 5</a></p></section></main></body></html>
|
||||
@@ -364,3 +364,56 @@ other, so `test/update.test.tsx` pins both ends of the `skip-waiting` handshake
|
||||
cleanly because state is in SQLite.
|
||||
- **No server-side rendering.** The feed is small and cacheable; a static SPA is enough.
|
||||
- **No websockets.** Events change on a scale of hours; a 5-minute cache is more than adequate.
|
||||
|
||||
## Reader application (2026-10-16 redesign)
|
||||
|
||||
`App.tsx` exports `ReaderApp`; only the v2 reader store writes production progress. Legacy hooks
|
||||
remain as compatibility helpers, validators and tests, not mounted writers. ReaderContent coordinates
|
||||
shared Checklist/Timeline/Routines/Archive/Settings, account-aware detail adapters and timed archive
|
||||
transitions. `components/redesign/` contains the feature pages. `redesign.css` scopes Relaxed and
|
||||
Structured via data-interface, with one application tree and independent theme/density.
|
||||
|
||||
`state/reader/` owns migration, schema, reducer, store, clocks, accounts, archive, backups, restore,
|
||||
imports, routines and event selectors. Operations re-read the persisted envelope under Web Locks;
|
||||
an IndexedDB readwrite transaction serializes browsers without Web Locks. Failed locking/storage
|
||||
never publishes an optimistic success. Storage events update other tabs. Field timestamps protect
|
||||
newer edits and tombstones; account writes rebase changed routine choices. Legacy deltas reconcile
|
||||
on startup into the original migrated accounts; raw keys remain preserved.
|
||||
|
||||
Clock revisions freeze automatic metadata and overrides. Imported historical clocks are excluded
|
||||
from active clock resolution; history resolves each explicit revision. Display timezone lives in
|
||||
DisplayTimeContext and never changes the feed.
|
||||
|
||||
Events and redeem codes use separate validated static feeds. `build-codes.ts` reads pinned snapshots
|
||||
or fixtures offline and preserves firstObservedAt in snapshots/codes-first-seen.json. The refresh
|
||||
runner combines event adapters and count-only code sources while sharing robots, floors, per-host
|
||||
spacing and snapshot gates. No fake GachaEvents represent codes. Both feeds are network-first in
|
||||
the worker; manual refresh bypasses the browser cache. Shell update waiting/activation is unchanged.
|
||||
|
||||
Backups are outside the reader envelope. Restore validates first, writes a recovery export, prepares
|
||||
a pending transaction, then replaces live state. Startup finishes an interrupted transaction before
|
||||
opening the reader. Browser UI restore uses the same cross-tab lock as ordinary edits.
|
||||
|
||||
|
||||
### Ordinary HTTP / WireGuard hosts
|
||||
|
||||
Reader identity minting uses `state/reader/ids.ts`: 128-bit `crypto.getRandomValues` tokens for
|
||||
accounts, operations, reset revisions and backups. The redesign's initial `randomUUID` calls were a
|
||||
startup bug on ordinary HTTP network hosts; unlike localhost, these are not browser secure contexts,
|
||||
even when reached through a WireGuard tunnel. Existing IDs are unchanged and valid. Initialization
|
||||
preserves migration recovery before minting, so the failed first visit did not discard legacy bytes.
|
||||
|
||||
Regression verification uses a Chrome test hostname mapped to loopback with isSecureContext false,
|
||||
randomUUID unavailable and actual getRandomValues/IndexedDB available. Existing-save migration,
|
||||
new setup, editing, additional servers, reset override/revert, backup and reload all pass there.
|
||||
|
||||
|
||||
### Presentation correction (2026-10-17)
|
||||
|
||||
`ChecklistEvent`/`NextDeadline` render the approved list hierarchy; `TimelinePage` owns the shared
|
||||
axis for event spans and routine periods. `RoutinesPage` groups by reset or account, with native
|
||||
`RoutineEditor` dialogs. `ArchiveDetail` reads saved archive snapshots and never routes historical
|
||||
progress through the live event editor. Settings keeps one shared tree for both interfaces, with
|
||||
separate default-view/custom-content pages and direct game profiles. Structured is the absent-value
|
||||
default at prepaint, preference normalization, setup and appearance application. No state keys,
|
||||
event identities or stored clock revisions changed for this presentation correction.
|
||||
|
||||
@@ -244,7 +244,10 @@ CREATE TABLE snapshots (
|
||||
`region_ends` and `payload` hold JSON as TEXT; parse them through the Zod schema on read so a
|
||||
malformed row surfaces at the boundary rather than deep in the UI.
|
||||
|
||||
## Client-side storage
|
||||
## Legacy client-side storage (v1 compatibility)
|
||||
|
||||
Production uses the [active v2 reader envelope](#active-reader-state-v2-2026-10-16). The keys and
|
||||
merge rules in this section describe migration input and retained compatibility helpers.
|
||||
|
||||
Namespaced, versioned, and small. Nothing here ever goes to the server.
|
||||
|
||||
@@ -772,3 +775,48 @@ Two timestamp fields distinguish crawler activity from site updates: `lastConfir
|
||||
the crawler last successfully contacted and verified the source (HTTP 200 or 304 Not Modified), and
|
||||
`contentChangedAt` records when the served bytes last changed. Both default to `null` so feeds built
|
||||
before their introduction continue to validate.
|
||||
|
||||
## Active reader state (v2, 2026-10-16)
|
||||
|
||||
The localStorage/export descriptions above document legacy v1 compatibility. Production mounts
|
||||
only `ReaderApp` and persists `gacha-tracker:v2:reader`, validated by state/reader/schema.ts.
|
||||
One stable `account:<id>` represents one tracked game/server; it is browser-local, not a login.
|
||||
Original event/task/daily IDs remain unchanged inside account data. Account data contains progress,
|
||||
ignored, and daily/amount maps nested by clock revision. A revision freezes effectiveAt, region,
|
||||
reset override and automaticBase (offset/hour/minute). Changes append revisions; imported clocks
|
||||
can be historicalOnly and never replace the local active reset.
|
||||
|
||||
The envelope includes accounts, data, archive, seenEvents, prefs, customGames/customEvents/tasks,
|
||||
codeSeen (per game), codeRedeemed (per account), and eventSeen (per account). Archive/seen keys are
|
||||
JSON.stringify([accountId,eventId]); archive snapshots retain event, outcome/time, resolved end,
|
||||
clockRevision, progress, days and amounts. Account/revision cross-references are validated before
|
||||
restore. Field timestamps and operation IDs rebase writes and preserve deletion tombstones. Unknown
|
||||
fields pass through exports; progress imports retain opaque root/account extensions in
|
||||
importedExtensions as well as mergeable known fields. Existing same-server settings remain local.
|
||||
|
||||
Migration preserves raw v1 keys in place and in migration recovery, creates one account per game at
|
||||
the saved region and maps old follows/pins/logs. Newly offered games remain off. Startup compares
|
||||
legacyRaw with surviving v1 keys to carry older-tab deltas into the original account without
|
||||
resurrecting newer v2 deletions. Parser aliases carry progress, ticks, amounts and archive/seen IDs.
|
||||
|
||||
Keys outside the envelope:
|
||||
|
||||
- gacha-tracker:v2:migration-recovery — raw pre-migration bytes.
|
||||
- gacha-tracker:v2:backups — at most 7 daily + 4 weekly + 6 monthly UTC bucket snapshots.
|
||||
- gacha-tracker:v2:restore-recovery — the complete pre-restore export.
|
||||
- gacha-tracker:v2:restore-pending — prepared replacement, resumed before mounting on startup.
|
||||
|
||||
Exports use format gacha-tracker-export, version 2, exportedAt, reader and legacy. Progress exports
|
||||
mark scope progress; full imports preview replacement, progress imports preview a merge. Backups
|
||||
are never recursively included. Original v1 export envelopes remain convertible.
|
||||
|
||||
Prefs add interface (structured/default or relaxed), density (standard/default or compact), scheme
|
||||
(slate/warm/plum/custom), separate light/dark customPalette, per-game gameColors, timezone
|
||||
(null/Automatic or IANA), checklist daily placement/expansion and redesignTourSeen. Main view remains
|
||||
soon or timeline; utility navigation is transient. Future/unreadable records remain exportable even
|
||||
when this build cannot offer editing controls.
|
||||
|
||||
|
||||
The fidelity correction adds `prefs.routineGroup` (`reset`, the absent-value default, or `game`)
|
||||
for the Routines page's grouping. It changes no routine IDs or placement keys. Structured is the
|
||||
interface default as of the owner's 2026-10-17 instruction; explicit saved Relaxed is preserved.
|
||||
|
||||
@@ -2,84 +2,84 @@
|
||||
name: Event Clock
|
||||
description: Live and upcoming gacha events, sorted by what expires next.
|
||||
colors:
|
||||
ground: "#12141c"
|
||||
surface: "#1b1e2a"
|
||||
raised: "#232739"
|
||||
hairline: "#2c3145"
|
||||
ink: "#e9ebf3"
|
||||
ground: "#1a1c24"
|
||||
surface: "#22252f"
|
||||
raised: "#30333c"
|
||||
hairline: "#43454c"
|
||||
ink: "#eeeae3"
|
||||
ink-strong: "#ffffff"
|
||||
muted: "#888fa6"
|
||||
faint: "#5a6178"
|
||||
muted: "#b1afad"
|
||||
faint: "#9c9b9b"
|
||||
scrim: "rgb(18 20 28 / 0.8)"
|
||||
glow: "#1a1e2c"
|
||||
calm: "#55607f"
|
||||
near: "#5b8def"
|
||||
soon: "#e8b33c"
|
||||
critical: "#ff4d6a"
|
||||
ground-light: "#edf0f7"
|
||||
surface-light: "#ffffff"
|
||||
raised-light: "#dfe4f0"
|
||||
hairline-light: "#c9d1e2"
|
||||
ink-light: "#151824"
|
||||
near: "#8eb3f4"
|
||||
soon: "#e9b95b"
|
||||
critical: "#ff8598"
|
||||
ground-light: "#f3f1ec"
|
||||
surface-light: "#fffdf8"
|
||||
raised-light: "#f0eeea"
|
||||
hairline-light: "#dddbd8"
|
||||
ink-light: "#28272d"
|
||||
ink-strong-light: "#000000"
|
||||
muted-light: "#535b73"
|
||||
faint-light: "#767e94"
|
||||
muted-light: "#69676a"
|
||||
faint-light: "#6e6d6e"
|
||||
scrim-light: "rgb(26 31 48 / 0.42)"
|
||||
glow-light: "#ffffff"
|
||||
calm-light: "#8a92a8"
|
||||
near-light: "#2b62c6"
|
||||
soon-light: "#8a6008"
|
||||
critical-light: "#c81e3c"
|
||||
near-light: "#315fa5"
|
||||
soon-light: "#805a12"
|
||||
critical-light: "#b12947"
|
||||
typography:
|
||||
numeral:
|
||||
fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif"
|
||||
fontFamily: "Bricolage Grotesque, ui-sans-serif, system-ui, sans-serif"
|
||||
fontSize: "2.75rem"
|
||||
fontWeight: 700
|
||||
lineHeight: 1
|
||||
letterSpacing: "-0.025em"
|
||||
fontFeature: "\"tnum\" 1"
|
||||
display:
|
||||
fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif"
|
||||
fontFamily: "Bricolage Grotesque, ui-sans-serif, system-ui, sans-serif"
|
||||
fontSize: "1.75rem"
|
||||
fontWeight: 600
|
||||
lineHeight: 1.15
|
||||
letterSpacing: "-0.025em"
|
||||
wordmark:
|
||||
fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif"
|
||||
fontFamily: "Bricolage Grotesque, ui-sans-serif, system-ui, sans-serif"
|
||||
fontSize: "0.9375rem"
|
||||
fontWeight: 700
|
||||
letterSpacing: "0.02em"
|
||||
reading:
|
||||
fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif"
|
||||
fontFamily: "Bricolage Grotesque, ui-sans-serif, system-ui, sans-serif"
|
||||
fontSize: "0.875rem"
|
||||
fontWeight: 600
|
||||
fontFeature: "\"tnum\" 1"
|
||||
title:
|
||||
fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif"
|
||||
fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif"
|
||||
fontSize: "0.9375rem"
|
||||
fontWeight: 500
|
||||
lineHeight: 1.375
|
||||
body:
|
||||
fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif"
|
||||
fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif"
|
||||
fontSize: "0.875rem"
|
||||
fontWeight: 400
|
||||
lineHeight: 1.625
|
||||
summary:
|
||||
fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif"
|
||||
fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif"
|
||||
fontSize: "0.8125rem"
|
||||
fontWeight: 400
|
||||
lineHeight: 1.375
|
||||
control:
|
||||
fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif"
|
||||
fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif"
|
||||
fontSize: "0.75rem"
|
||||
fontWeight: 500
|
||||
label:
|
||||
fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif"
|
||||
fontFamily: "Bricolage Grotesque, ui-sans-serif, system-ui, sans-serif"
|
||||
fontSize: "0.6875rem"
|
||||
fontWeight: 600
|
||||
letterSpacing: "0.14em"
|
||||
tag:
|
||||
fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif"
|
||||
fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif"
|
||||
fontSize: "0.625rem"
|
||||
fontWeight: 500
|
||||
rounded:
|
||||
@@ -170,7 +170,11 @@ components:
|
||||
|
||||
# Design System: Event Clock
|
||||
|
||||
## Overview
|
||||
The active Relaxed/Structured layout and palette contract is recorded in
|
||||
[Active redesign system](#active-redesign-system-2026-10-16). The component rationale below describes
|
||||
the preceding instrument-panel system; use the active section and live token resolver for new work.
|
||||
|
||||
## Original component rationale
|
||||
|
||||
**Creative North Star: "The Lit Instrument Panel"**
|
||||
|
||||
@@ -528,3 +532,105 @@ rather than being toned down.
|
||||
chooses.
|
||||
- **Don't** put a second control inside a row, or a drag target on the focus bar or the dailies
|
||||
strip.
|
||||
|
||||
## Shipped redesign system (2026-10-18)
|
||||
|
||||
The owner accepted this build as the initial redesign on 2026-10-18, after screen-by-screen review
|
||||
against the approved mockups (`design/mockups/round6/b-rail.html` for Structured,
|
||||
`design/mockups/round7/converged.html` for Relaxed) and the corrections recorded round by round in
|
||||
`docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md`. This section supersedes the original
|
||||
shell/type/palette/layout descriptions above. Those older sections remain the rationale for the detail,
|
||||
timeline and uncertainty components that carried over. Overrides live in `src/client/redesign.css`
|
||||
and `src/client/state/appearance.ts`.
|
||||
|
||||
### Shell and layout
|
||||
|
||||
- **Interfaces.** Settings → Appearance offers Structured (the Round 6 B rail; the default for new
|
||||
installs and missing preferences) and Relaxed (Round 7). A saved Relaxed choice stays Relaxed.
|
||||
Structured uses Barlow Condensed/Barlow and adjoining ruled sections. Relaxed uses Bricolage
|
||||
Grotesque/Figtree with separated panels. Fonts keep system fallbacks offline.
|
||||
- **Navigation.** The tabs are Checklist, Timeline, Routines and Archive, with Settings on the right.
|
||||
The EVENTCLOCK logo returns to the default view. Each page is in the address bar (`#checklist`,
|
||||
`#settings/games/genshin`, …), and changing tab starts at the top of the page.
|
||||
- **Width.** The page fills up to 1920px (Relaxed 1840px) and centres beyond that.
|
||||
- **Two insets on `.reader-ui`.** `--edge` (16px) between content and the page's outer edges.
|
||||
`--gutter` (24px) where a side column (Checklist rail, Settings nav, Archive sidebar) meets the main
|
||||
content. At ≤700px, where everything stacks, both are the edge.
|
||||
- **Checklist rail.** 360px wide (Relaxed 380px; 280/300px at medium widths). Top to bottom: the
|
||||
focus pills, Next to expire (headline, meter and the next three), Today's dailies, then pinned
|
||||
routines grouped by game.
|
||||
|
||||
### Urgency and colour
|
||||
|
||||
- **Two axes, never crossed.** Game hue answers whose event: the 3px strip on the left of each
|
||||
Standard row, the Compact game cell, the pills. The urgency ramp answers how long: countdowns and
|
||||
meter ticks.
|
||||
- **The urgency ramp is live's.** Grey for a week or more, blue under a week, yellow under 3 days, red
|
||||
under a day: `--color-urgency-{calm,near,soon,critical}`, set by `urgencyTokens`. It is separate
|
||||
from the accent, so a chosen accent never changes urgency.
|
||||
- **Reader-editable.** Settings → Appearance → Urgency colours sets each step per theme
|
||||
(`prefs.urgencyPalette`), with any scheme. Values are nudged only as far as 3:1 contrast, the
|
||||
large-text bar.
|
||||
- **Fallback for the live UI.** `URGENCY_COLOR` falls back to the old `--color-*` tokens, so the
|
||||
live UI is unchanged.
|
||||
- **Schemes.** Slate, Warm, Plum and Custom shift surfaces. Custom stores ground/surface/text/accent
|
||||
per theme. Text and accent derive at least 4.5:1 contrast on ground and surface. Game colours are
|
||||
set per game in Your games.
|
||||
|
||||
Default Slate tokens:
|
||||
|
||||
| Role | Structured dark | Structured light | Relaxed dark | Relaxed light |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Ground | #0e1117 | #eef0f5 | #1a1c24 | #f3f1ec |
|
||||
| Surface | #141923 | #ffffff | #22252f | #fffdf8 |
|
||||
| Primary text | #eeeae3 | #28272d | #eeeae3 | #28272d |
|
||||
| Accent | #8eb3f4 | #315fa5 | #8eb3f4 | #315fa5 |
|
||||
|
||||
### Checklist
|
||||
|
||||
- **Heading.** "Running now" sits with the sort and density buttons on one row. Under them is one
|
||||
line: the count on the left and live's legend on the right ("bars = time left · colour = ends in a
|
||||
week+ / under a week / under 3 days / under a day").
|
||||
- **Standard rows.** Game, title, optional description, then the countdown, date and meter, with the
|
||||
chevron 10px from the date. Every row has a meter, with or without a description; the mockup hid
|
||||
it on rows without one, and the owner chose consistency.
|
||||
- **Compact rows.** A ledger with Game / Event / Time left / Ends in columns.
|
||||
- **"Not started yet."** It opens a second section: a 48px gap, a 2px accent rule and a raised band
|
||||
with a "Coming up" eyebrow. In Compact its columns read Duration / Starts in.
|
||||
|
||||
### Focus pills
|
||||
|
||||
They have live's chip feel: 30px tall, 12px text, a 30% hue border unpressed, the full hue with a 14%
|
||||
wash when pressed. State changes ease over 130ms. Hover adds a hue ring and a 1px lift, a press
|
||||
scales to 0.96, and games with nothing outstanding dim to 50%. The "Focus" label is a 12px body-face
|
||||
legend on the same baseline as "Next: …".
|
||||
|
||||
### Ticking things off
|
||||
|
||||
- **Today's dailies.** The whole row is the target. A tick washes the row in its game's hue, pops the
|
||||
box and draws the check. A pip bar and "N of M done" sit under the heading. A check-in goal (for
|
||||
example "5/5 wk") counts as done once met, with a dotted check, and shows its goal count instead of
|
||||
a streak.
|
||||
- **Pinned routines** use the same card language. A box ticks a check-in routine, or completes an
|
||||
amount routine in one click. "+1" steps an amount, and the count opens an editor. Typing a value at
|
||||
or over the target completes it after a 450ms pause.
|
||||
- **Completion animation.** The bar fills, the row warms, then the box pops. Unticking undoes it:
|
||||
the amount from before goes back, or the editor opens when that value isn't known.
|
||||
- **Celebration.** A single completion fires live's small `Fireworks` in the row. Finishing a whole
|
||||
section (every daily, or every routine in a game's group) fires `Fireworks grand`: seven bursts in
|
||||
two waves with the game's hue mixed in, a light sweep, and the "All done" line popping.
|
||||
- **When it fires.** Only an action taken now that completes the item or section, never arriving
|
||||
complete. Reduced motion removes all of it.
|
||||
|
||||
### Other pages
|
||||
|
||||
- **Routines.** Bands by reset (Dailies, Weeklies, Monthlies, Cycles) or by game. The table has tick
|
||||
and click-to-edit amount cells. The library and create/edit drawers are shelves.
|
||||
- **Timeline.** Two looks (new and classic) chosen in Appearance. Dailies are not on the timeline.
|
||||
- **Settings.** About, Changelog, Data & sources (list options first), Region & time, Appearance
|
||||
(interface, theme, scheme, urgency colours, default view, game order, timeline design, focus bar,
|
||||
checklist group), Your own events and games, Backup (tiers plus a manual "Your backups" list), and
|
||||
per-game Setup / Codes / About.
|
||||
- **Returning-user tour.** Routines → Checklist → Timeline → Redeem codes → Archive → Settings.
|
||||
Continue stays in a fixed place, Back sits beside it, and Skip is apart. "Replay the introduction"
|
||||
starts it at step 1.
|
||||
|
||||
@@ -640,3 +640,22 @@ fixture is the regression test proving the parser still handles the previous for
|
||||
guessing, impossible calendar dates are rejected, ranges crossing New Year roll the start year back,
|
||||
and abbreviated months parse. `test/merge.test.ts` covers cross-source agreement, disagreement, and
|
||||
rerun disambiguation. These are the last line of defense before a wrong date reaches a user.
|
||||
|
||||
|
||||
## Redeem codes source (2026-10-16)
|
||||
|
||||
Genshin codes use https://game8.co/games/Genshin-Impact/archives/304759, separately from events.
|
||||
One descriptive-agent robots request and one page request returned HTTP 200 during capture; no
|
||||
access workaround or repeated source fetch was used. The raw page and capture metadata are pinned
|
||||
as fixtures/genshin/game8-codes-2026-10-16.*, with ten independently counted current codes. The
|
||||
parser reads actual code inputs/current reward rows and excludes expired programme sections; a
|
||||
future-dated programme fixture variant verifies that still-current programmes are admitted.
|
||||
Unannounced expiry remains null. A printed calendar date is expiresOn, never a fabricated exact
|
||||
expiresAt instant. Attribution is retained on every code.
|
||||
|
||||
src/ingest/codes/registry.ts registers a count-only refresh source. The existing runner shares robots,
|
||||
six-hour floors, conditional requests, per-host spacing and snapshot gates across event/code sources.
|
||||
Event adapter schemas are unchanged. scripts/build-codes.ts builds data/codes.v1.json offline from
|
||||
snapshots or fixtures and preserves observation timestamps in snapshots/codes-first-seen.json.
|
||||
Code newness is per game; redemption is per local game/server account and never clears newness by
|
||||
itself. No code row is synthesized into the event feed.
|
||||
|
||||
@@ -111,14 +111,9 @@ few pixels apart the board is being asked the one question it exists to answer a
|
||||
reader to squint. It also gives a one-day event a bar to read and to tap rather than the 34px
|
||||
minimum every short bar collapses to.
|
||||
|
||||
**What has not started yet is shown to a new reader, and either can switch it** — `prefs.showUpcoming`,
|
||||
which governs **both views**: the board plots them, and the checklist keeps its "Not started yet"
|
||||
section (F2). One question, asked once, because they are the same events. It was off by default,
|
||||
because this app answers *what expires next* and a reader with fourteen lanes has a next patch queued
|
||||
behind every one of them. Since 2026-10-13 (repository owner) first-run setup turns it **on**: a new
|
||||
reader should see what is coming before deciding they do not want it. The stored default is still
|
||||
`false`, so an existing install whose saved prefs predate the field keeps today's answer rather than
|
||||
having it switched on underneath its reader.
|
||||
**Upcoming events are optional and off initially.** New setup asks “Show events that haven't
|
||||
started” under “What do you want to track?”. The same saved answer governs Checklist and Timeline;
|
||||
Checklist separates them under “Upcoming events”. Existing saved choices survive migration.
|
||||
|
||||
On the board that is structural as well as editorial: the window is drawn from what is plotted, so
|
||||
showing the future stretches the right edge weeks past today and squeezes the running bars the
|
||||
@@ -129,10 +124,8 @@ quiet fortnight, and this app does not leave a reader to infer what it is not sh
|
||||
page header states `N live · N upcoming` whatever the setting says, and either view left with
|
||||
nothing to show says so in words and names the setting rather than rendering an empty column.
|
||||
|
||||
The switch is in settings (F4), with the two other answers to *what am I allowed to look at* —
|
||||
`showCompleted` and `showIgnored` — and **not** in the board's own header beside the stacking and
|
||||
scale controls. That is the line those two draw: they reshape what is already on the board, which is
|
||||
why they are reached for while reading it, and this one decides what is on it at all.
|
||||
The switch lives in Settings → Lists, because it decides which events enter both main views.
|
||||
Done and ignored events live in Archive; Lists has no redundant Archive link or reveal switches.
|
||||
|
||||
**Switched on, the board says in words where they begin.** A bar drawn to the right of the "now"
|
||||
rule is only *implicitly* in the future, and implicitly is not a standard this product holds itself
|
||||
@@ -198,42 +191,17 @@ an explicit "show all N". This truncates the *view* only: the order is untouched
|
||||
the cut are still counted in the header, still on the timeline, and one tap away.
|
||||
|
||||
**F3 — Mark completed.**
|
||||
A toggle on every event, in both views. State is written to `localStorage` immediately and
|
||||
optimistically — there is no server round trip and no failure case. Completed events stay visible
|
||||
but de-emphasized; a filter toggles them out entirely.
|
||||
Done writes durable progress and a metadata snapshot immediately. A confirmation remains for about
|
||||
1.5 seconds before the row fades/collapses into Archive. Undo is available for eight seconds;
|
||||
Archive Restore remains available afterwards. A failed write is disclosed and never presented as
|
||||
saved. Reduced motion removes the fade.
|
||||
|
||||
**F4 — Filters.**
|
||||
Filter by game (multi-select, persisted) and by event type. Hiding a game hides it from both views.
|
||||
Preferences persist in `localStorage`.
|
||||
|
||||
The panel holding them is **six collapsed groups, each stating its own answer on the summary line**
|
||||
— games, server region, appearance, what you see, your own games and events, your progress. It was
|
||||
one open block of everything, laid out in two columns on a wide screen, which read fine at four games
|
||||
and stopped working at eighteen: the game list alone is eighteen rows of four controls, and it sat
|
||||
above the checkbox a reader had come down here to tick. Collapsing on its own would only trade that
|
||||
friction for another, though — a group that shows nothing but its name turns *what is my region set
|
||||
to?* into a click — so each summary answers its group's question, and the closed panel is a six-line
|
||||
report of how the app is configured. Opening one is for changing an answer, not for reading it.
|
||||
|
||||
Three things follow. **The groups ship closed**, including the one holding the filters that other
|
||||
copy points at: the empty states name both the switch and the group it is in, which is more findable
|
||||
than a checkbox in a wall was. **The summary states are derived, never stored** — they are a reading
|
||||
of `prefs`, so they cannot disagree with the controls inside. And **a group answers one question**.
|
||||
The region and the theme began as one group called *Reading*, on the argument that both are "how do I
|
||||
read this?" — which summarised as `Europe · Dark`, two unrelated answers joined by a dot, under a
|
||||
name for neither. The region is not a reading preference: it is which server the reader's account is
|
||||
on, and region-scoped ends, undated deadlines and every daily streak are read off that server's
|
||||
clock, so it is the one control in this panel that can make a countdown wrong. It gets its own line,
|
||||
and says as much when opened.
|
||||
|
||||
**The panel uses the page's width**, like every other block on it. The rows were held to a narrower
|
||||
measure on the argument that a name and its state stop reading as one line across a wide screen, but
|
||||
the panel's own rule spans the shell, so the rules inside it stopped a third of the column short and
|
||||
the panel became the one thing on a desktop page not using the width — sitting between a two-column
|
||||
checklist and a three-column footer that both do. A title at one end of a full-width row and its
|
||||
countdown at the other is what "running now" does directly above it. Prose inside a group keeps a
|
||||
readable measure, and the game list takes two columns of its own on a wide screen once there are nine
|
||||
or more games, because eighteen rows in one column is a thousand pixels of ribbon down the left.
|
||||
**F4 — Settings and visibility.**
|
||||
Settings has named pages: General, Your games, Appearance, Lists, Region & time, Data & sources,
|
||||
Backup, About Event Clock, and Changelog. Your games controls visibility/order, tracking choices,
|
||||
servers, reset overrides and colours; Lists controls upcoming events and timeline grouping.
|
||||
Routines owns follows and independent Checklist/Timeline pins. Utility navigation does not replace
|
||||
the saved default main view. Empty states name where to change the relevant choice.
|
||||
|
||||
**F4a — Focus one game at a time.**
|
||||
Switching games on and off says *which games the reader plays*, and is set once. It is the wrong
|
||||
@@ -253,15 +221,26 @@ has left the feed is ignored rather than obeyed, so it can never strand the read
|
||||
whose cause is elsewhere. Each chip carries that game's outstanding count, so a game with nothing
|
||||
waiting says so before it is visited.
|
||||
|
||||
**F5 — Region selection.**
|
||||
A user picks Asia / America / Europe once. For events where `regionScoped` is true, all displayed
|
||||
end times resolve to that region's server reset. This is stored in `localStorage` and defaults to a
|
||||
guess from the browser timezone, shown as a dismissible "showing America server times — change".
|
||||
**F5 — Servers and display timezone.**
|
||||
Default server is chosen in setup and Region & time. Each locally tracked game/server has its own
|
||||
stable account identity and progress. “Add another server” is for simultaneously playing multiple
|
||||
servers: it starts fresh tracking choices and never clones marks. Server/reset changes append clock
|
||||
revisions so history stays on its original clock. Overrides can revert to Automatic. Display timezone
|
||||
is Automatic by default or a searchable IANA zone; it changes printed dates and manual date entry,
|
||||
never a source's exact timestamps or game-day keys.
|
||||
|
||||
**F6 — Export / import.**
|
||||
Because there are no accounts, moving between devices is manual: download a JSON file of completed
|
||||
IDs and preferences, upload it elsewhere. Import merges rather than replaces, and never removes a
|
||||
completion the user already has.
|
||||
**F6 — Export / import and backup.**
|
||||
No login is involved. Export progress / Import progress merges per game/server, retaining local
|
||||
preferences and active reset configuration. Day logs union; newer records/amounts win conflicts.
|
||||
Imported clock revisions remain accessible as historical clocks. Unknown extensions are retained in
|
||||
the envelope. Export all / Import all restores the complete validated envelope through a preview,
|
||||
with a recovery copy before replacement. Undo last restore reverses it. Version-1 exports remain
|
||||
convertible; original v1 browser keys are retained after migration.
|
||||
|
||||
Local backup history keeps seven daily, four weekly and six monthly snapshots while the app is
|
||||
used. UTC buckets keep their latest snapshot; backups never contain other backups. View backup
|
||||
opens the preview; Restore this backup is the separate committing action. Storage failures are
|
||||
visible. Browser deletion removes local history; export is the way to keep an external copy.
|
||||
|
||||
**F12 — Record your own progress and effort.**
|
||||
Three states, not two: untouched, doing it, done. Plus an optional effort estimate — quick, short,
|
||||
@@ -418,15 +397,16 @@ the one after it — because they answer "what ends soonest". The timeline draws
|
||||
every occurrence in view, because it answers "what is the rhythm".
|
||||
|
||||
**F8 — First-run setup.**
|
||||
Before any events are shown, the reader picks which games they play, and how they want to read
|
||||
them. A calendar full of games they
|
||||
don't play is worse than an empty one — it buries the thing they came for. Nothing is preselected
|
||||
and the button stays disabled until something is chosen; guessing on their behalf and hoping they
|
||||
notice is worse than asking. **The games are listed alphabetically by name**, not in feed order:
|
||||
everywhere else a lane's position carries meaning, but here the reader is scanning for the two or
|
||||
three names they already know, and a list ordered by which game happened to hold the first event row
|
||||
gives them nothing to scan by. The choice is stored as *hidden* games, the inverse — which is a storage
|
||||
shape, not a policy: what happens to a game added later is decided separately, below.
|
||||
Before events are shown, a four-step wizard asks for games, default server and preferred appearance,
|
||||
tracking kinds, and the default main view. Games are alphabetical, nothing selected. Events are the
|
||||
only initially selected tracking kind. Dailies, weekly/monthly and cycles have short descriptions;
|
||||
unavailable kinds remain selectable and say nothing is currently detected. Upcoming visibility
|
||||
belongs to that tracking step and starts off. Server and appearance can be changed later.
|
||||
|
||||
Returning readers get a one-time introduction and optional four-step tour. Skipping or finishing
|
||||
preserves progress and the saved main view; General can replay it.
|
||||
|
||||
|
||||
|
||||
**A game added later arrives switched off.** Adding a source is our decision, not the reader's, and
|
||||
someone who plays two games did not ask for the other twelve; a calendar that fills itself up is the
|
||||
@@ -447,10 +427,13 @@ The view question (F2) sits under it, with each option drawn rather than only de
|
||||
it arrives already answered, and the screen says where to change it afterwards: the tabs are small
|
||||
text in a corner, which is the one control a first-time reader will not find on their own.
|
||||
|
||||
**F9 — Ignore an event.**
|
||||
Distinct from completing one. "Done" keeps an event visible and counted; "not interested" removes it
|
||||
from both views entirely. Ignored events stay recoverable: a count and a reveal toggle appear in
|
||||
settings once there is something to reveal.
|
||||
**F9 — Ignore and Archive.**
|
||||
Ignore saves immediately and follows the same confirmation/Undo transition as Done. Both move to
|
||||
Archive with event metadata, notes, goals, logs, resolved end and clock revision. Previously seen
|
||||
followed events that expire unfinished also enter Archive; unknown-end events never expire by a
|
||||
guessed date. Per-game history is default, with an alternate chronological history. Story statistics
|
||||
include completion, check-ins and shortfalls where known; ignored events are excluded from success
|
||||
rates. Restore recovers saved progress without pretending an expired event is live.
|
||||
|
||||
**F10 — Works offline.**
|
||||
The reader's question is answered entirely by data already on the device, and countdowns run off the
|
||||
@@ -459,9 +442,9 @@ the last feed it downloaded. Offline is disclosed in the header and above the fo
|
||||
data must never be presented as current.
|
||||
|
||||
**F11 — Credit and disclaimer.**
|
||||
The sources that compile these calendars, and the studios that make the games, are named on the same
|
||||
screen as the data rather than one navigation step away. The page states plainly that it is
|
||||
unofficial and unaffiliated, and that the source page is the authority when the two disagree.
|
||||
Settings → About Event Clock preserves the full colophon, source credits, unofficial/unaffiliated
|
||||
disclaimer and feedback links. The footer links About. Each game's About lists the real data sources,
|
||||
links and thanks; event detail links its authoritative source.
|
||||
|
||||
**F14 — Say when a new version of the app is ready.**
|
||||
F10 caches the shell so the app survives losing signal, and the same cache is why a reader who never
|
||||
@@ -483,42 +466,29 @@ arrive without a reload, and calling that a new version would train readers to d
|
||||
unread.
|
||||
|
||||
**F7 — Freshness disclosure.**
|
||||
The footer shows when the feed was last updated, per game. If a game's data is more than 48 hours
|
||||
stale, its lane carries a warning badge. Never present stale data as current — the whole value
|
||||
proposition is trust in the dates.
|
||||
The footer always states data age, never build age. Only tracked games with stale data produce a
|
||||
compact “Event data may be out of date” row in the shared updates area. New events and new codes
|
||||
remain distinct notices. Data & sources and event detail expose the last successful check, last
|
||||
content change, source links and recorded reasons. A successful 304 confirms a check without
|
||||
claiming the wiki changed.
|
||||
|
||||
**The reader can ask for the newest data.** A reload is not enough — the browser may keep the feed
|
||||
for minutes — so *Check for new data* beside the age asks the server past that copy and swaps a newer
|
||||
feed in without a reload. It says what happened: updated, nothing newer published yet (sources are
|
||||
read about twice a day, and the button cannot make that sooner), or the server could not be reached.
|
||||
"Newer" is judged on events and source health, never on the build stamp. Disabled offline; nothing
|
||||
polls on its own.
|
||||
Check for new data revalidates event and code feeds, compares their content rather than generatedAt,
|
||||
and reports updated, unchanged or failure. It does not scrape source sites from the browser.
|
||||
|
||||
**The staleness notice names only the games the reader has switched on, and says that it does.**
|
||||
The headline age is about the feed and stays whole; the notice under it is an *instruction*, and the
|
||||
only remedy it offers is "go and check that game's source page" — which a reader cannot act on for a
|
||||
game they turned off and cannot see a single row of. With nineteen lanes and two switched on, naming
|
||||
eleven they do not play buries the one they do, and a warning about games nobody reads is a warning
|
||||
nobody reads (F8: a game we add is a game they never asked for, and arrives switched off, so an
|
||||
untouched install would be warned about the whole catalogue).
|
||||
**F15 — Appearance.**
|
||||
Relaxed (Round 7) and Structured (Round 6 B rail) share one component/state tree. Structured is the default (owner decision, 2026-10-17); an explicitly saved Relaxed preference is preserved.
|
||||
Interface choice, Standard/Compact density, Dark/Light/System theme and Slate/Warm/Plum/Custom
|
||||
scheme are independent. Custom stores ground/surface/text/accent separately for light and dark;
|
||||
rendered colours derive readable contrast without overwriting the saved choices. Per-game colours
|
||||
have a picker, hex field, preview and reset. Checklist dailies can occupy a separate direct-tick panel
|
||||
or grouped routines, with collapsed/expanded/always-expanded policies; these controls also live in
|
||||
Routines. Numeric routines use native step-one inputs.
|
||||
|
||||
Both halves of that are load-bearing. Scoping it silently would be the worse half: "nothing has
|
||||
refreshed" is a claim about the whole calendar, so a reader with fourteen of eighteen lanes off would
|
||||
read a sentence about four as one about all of them. The notice therefore states whose games it
|
||||
counted, in the same words the empty list uses — *the games you have switched on*. Credit in the
|
||||
column beside it is owed to every source we read and is not scoped; nor is the "last refreshed" line,
|
||||
which reports the feed and feeds the bug form.
|
||||
|
||||
**The staleness notice differentiates crawler checks from site updates.** If our crawler successfully
|
||||
checked the wiki recently (e.g. confirming HTTP 304) but the site's content has not changed in over
|
||||
two days, the notice explicitly distinguishes the two: "data pulled X ago, site updated Y ago", with
|
||||
an expandable breakdown detailing each source's check and content update times.
|
||||
|
||||
**F15 — Light mode, with dark still the default.**
|
||||
The earlier theme rationale below still applies to both interfaces.
|
||||
The app is a lit instrument panel and that is what it should be on first sight, but it is also read
|
||||
on a train in daylight and by people who find a dark UI harder rather than moodier. A public fork
|
||||
worked a theme toggle out first and is credited for the idea in the colophon. So the ground is
|
||||
the reader's choice: **Dark**, **Light**, or **System** to follow the device, in settings next to the
|
||||
the reader's choice: **Dark**, **Light**, or **System** to follow the device, in Settings → Appearance, separately from the
|
||||
region — both are "how do I read this?" and neither changes what the page knows.
|
||||
|
||||
Dark is what it ships as, and deliberately not `System`. A reader whose laptop is in light mode has
|
||||
@@ -571,7 +541,15 @@ placed. It also arrives switched off (F8), so settings is where they meet it, wh
|
||||
would move it anyway. **A game's dailies stay together** — a repeating event the reader marked sits
|
||||
with the standing chore of the game it came from, because the reader thinks in games and not in kinds.
|
||||
|
||||
**F17 — Weekly and monthly tasks.**
|
||||
**F17 — Routines.**
|
||||
Routines is the library and management page for daily, weekly, monthly and cycle tasks. Follow each
|
||||
per server; pin independently to Checklist and Timeline. Built-ins offer Make it mine, copying only
|
||||
that account's logs; custom routines remain editable. Catch up & recorded history exposes logs on
|
||||
their original reset revisions. A separate Timeline reset strip sits directly after its game's events;
|
||||
with strips off, Ending soonest remains a single cross-game deadline queue.
|
||||
|
||||
The cadence and goal semantics below remain the domain contract; the redesign moves their controls
|
||||
from the former Your own section to Routines.
|
||||
Some chores roll weekly or monthly rather than daily — earning ZZZ's weekly activity points,
|
||||
finishing Endfield's weekly routine — and no source publishes them, so they were nowhere in the app.
|
||||
A reader can now set them up per game in *Your own*: a title, every week (from a weekday they pick),
|
||||
@@ -624,7 +602,7 @@ instant on every server rolls on that server's reset for every reader.
|
||||
The app now says what changed, in two tiers, both tied to git. **Short entries** — one line each,
|
||||
tagged New, Fixed, or Games & sources — appear once as *What's new*, above the lists, after a new
|
||||
version loads; *Got it* dismisses them. **Release notes** — a few paragraphs per feature or
|
||||
milestone, summarised from the history and the design specs — are in the footer's *Changelog*, each
|
||||
milestone, summarised from the history and the design specs — are in Settings → *Changelog*, each
|
||||
linked to the commits it covers and the specs it draws on. Data refreshes are not changes in this
|
||||
sense; the footer already states the data's age.
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ away for more coverage (see Product Principles).
|
||||
|
||||
## Capabilities and Constraints
|
||||
|
||||
**Capabilities.** The full feature set is specified as F1–F16 in `docs/PRD.md`:
|
||||
**Capabilities.** The full feature set is specified as F1–F18 in `docs/PRD.md`:
|
||||
|
||||
- A timeline: one lane per game, or a single queue sorted by soonest end.
|
||||
- A checklist of everything running, sorted by soonest end.
|
||||
@@ -79,16 +79,17 @@ away for more coverage (see Product Principles).
|
||||
- Daily checklists, with past days that can still be filled in.
|
||||
- Ignoring an event, and filters by game or type.
|
||||
- Focusing on one game at a time.
|
||||
- Region selection.
|
||||
- JSON export and import.
|
||||
- Separate game/server tracking, reset corrections and Automatic or named display timezone.
|
||||
- JSON export/import, recoverable restore and 7 daily / 4 weekly / 6 monthly local backups.
|
||||
- The reader's own games and events, one-off or repeating.
|
||||
- First-run setup.
|
||||
- Events-first new setup and a one-time returning-reader introduction.
|
||||
- Offline use.
|
||||
- A notice when a new version of the app is ready.
|
||||
- Dark or light mode, plus a "follow the system" option.
|
||||
- The reader's own game order.
|
||||
- The reader's own game order, per-game colours, Relaxed/Structured interfaces and custom schemes.
|
||||
- A Routines library with independent view pins, an Archive with story statistics, and sourced redeem codes.
|
||||
|
||||
The app currently covers twenty games from twenty-two sources.
|
||||
The app covers twenty games; source registries are the authoritative current count.
|
||||
|
||||
**Hard constraints** (`AGENTS.md` § Three constraints):
|
||||
|
||||
@@ -112,7 +113,7 @@ The app currently covers twenty games from twenty-two sources.
|
||||
**Terminology:**
|
||||
|
||||
- The user is a **reader**.
|
||||
- The two views are **Checklist** (stored id `"soon"`) and **Timeline**.
|
||||
- The main views are **Checklist** (stored id `"soon"`) and **Timeline**; Routines, Archive and Settings are utility pages.
|
||||
- An event's states are **untouched**, **doing it** and **done**.
|
||||
- **Ignore** is not the same as done.
|
||||
- A **lane** is one game's row or source.
|
||||
|
||||
@@ -976,3 +976,22 @@ Facts stated above that a future reader may want to re-check, with how they were
|
||||
| `marisaimpact.com` states no year on any calendar | stripped tags from `/calendar89` and `/calendar90` and grepped the text for `20\d\d` — zero matches on either |
|
||||
| Its calendar URL is per-version, with no stable alias | `/calendar90` is `200` and covers `Aug 20 - Oct 22`; `/valk/calendar90` — the scheme its own `og:url` uses — is `404`; the nav links only `/calendar88` and `/calendar89` |
|
||||
| Its schedule is not behind an API | `GET /calendar90/__data.json` returns 144 bytes: an `ogImage` URL and nothing else |
|
||||
|
||||
|
||||
## Redeem codes source (2026-10-16)
|
||||
|
||||
Genshin codes use https://game8.co/games/Genshin-Impact/archives/304759, separately from events.
|
||||
One descriptive-agent robots request and one page request returned HTTP 200 during capture; no
|
||||
access workaround or repeated source fetch was used. The raw page and capture metadata are pinned
|
||||
as fixtures/genshin/game8-codes-2026-10-16.*, with ten independently counted current codes. The
|
||||
parser reads actual code inputs/current reward rows and excludes expired programme sections; a
|
||||
future-dated programme fixture variant verifies that still-current programmes are admitted.
|
||||
Unannounced expiry remains null. A printed calendar date is expiresOn, never a fabricated exact
|
||||
expiresAt instant. Attribution is retained on every code.
|
||||
|
||||
src/ingest/codes/registry.ts registers a count-only refresh source. The existing runner shares robots,
|
||||
six-hour floors, conditional requests, per-host spacing and snapshot gates across event/code sources.
|
||||
Event adapter schemas are unchanged. scripts/build-codes.ts builds data/codes.v1.json offline from
|
||||
snapshots or fixtures and preserves observation timestamps in snapshots/codes-first-seen.json.
|
||||
Code newness is per game; redemption is per local game/server account and never clears newness by
|
||||
itself. No code row is synthesized into the event feed.
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# Approved redesign — implementation review, 2026-10-16
|
||||
|
||||
**Status: rejected by the owner for substantial divergence from the approved Round 7 markup.**
|
||||
The completion claims below describe the earlier implementation report and are withdrawn as
|
||||
evidence of design acceptance. Functional tests and overflow checks did not establish visual or
|
||||
interaction fidelity. A faithful markup-to-component correction is still required.
|
||||
|
||||
The approved [spec](../superpowers/specs/2026-10-16-ui-redesign-design.md) and
|
||||
[16-task plan](../superpowers/plans/2026-10-16-ui-redesign.md) are implemented in the working tree.
|
||||
Execution was Native. Nothing has been committed, pushed or published.
|
||||
|
||||
## Delivered
|
||||
|
||||
Relaxed (Round 7, default) and Structured (Round 6 B rail) share one reader/component tree; Appearance
|
||||
switches between them independently of theme, palette and Standard/Compact density. The redesign
|
||||
includes Routines and view pins, direct daily ticks, configurable grouping, multi-server progress,
|
||||
reset override/revert with accessible clock history, game colours/source credits, custom palettes,
|
||||
Automatic/IANA timezone, new and returning onboarding, compact source/new-event/code notices,
|
||||
Archive/story statistics, timed Done/Ignore/Undo, local rolling backups and recoverable restore.
|
||||
Settings About preserves the colophon; Changelog has its own page and no footer link.
|
||||
|
||||
The codes feed contains ten real Genshin codes from the pinned Game8 capture. Source fetching remains
|
||||
deterministic and shares the existing conduct gates; no additional community requests were needed
|
||||
while implementing or reviewing. Unstated expiry stays unknown. Codes newness and per-server
|
||||
redemption are separate. Manual refresh checks both feeds and discloses partial code-feed failure.
|
||||
|
||||
## Verification
|
||||
|
||||
- **1,587 tests pass**, zero failures, across 74 files. Strict TypeScript and whitespace checks pass.
|
||||
- The suite also runs without `public/` or `docs/` in the Docker COPY layout: 1,586 pass, one
|
||||
intentional README-only skip, zero failures. No tests depend on a preceding build.
|
||||
- Full offline build passes: 209 events across 20 games, ten codes, production JS/CSS/static shell.
|
||||
Final shell build `f5b25675010f` was loaded on a fresh origin (port 41880).
|
||||
- Chrome checks cover both interfaces, both themes, 1440/390 widths, Standard/Compact, reduced motion
|
||||
and an effective 200% CSS viewport. No horizontal page overflow or page errors in that matrix.
|
||||
- Browser journeys pass for migration, saved preferred Timeline, events-only setup, selectable empty
|
||||
tracking kinds, upcoming placement, isolation from unselected games, extra server with fresh state,
|
||||
direct ticks, reset override/revert/history, per-server redemption, Done/Ignore/Undo, unknown-end
|
||||
custom creation, real export download/upload/restore, backup preview/restore/Undo, custom palette,
|
||||
timezone selection/search, offline reload and the real service-worker update/reload handshake.
|
||||
- Two real browser tabs save concurrently with Web Locks explicitly disabled: IndexedDB serialization
|
||||
preserves both edits. Unit regressions cover stale progress fields and independent routine pins.
|
||||
- A fresh independent reviewer completed whole-change review and focused re-reviews. All important
|
||||
findings were fixed with reproductions; final review reports no remaining important findings.
|
||||
|
||||
## Review fixes and durable decisions
|
||||
|
||||
Deleted custom definitions no longer reappear from seen snapshots. Progress imports preserve local
|
||||
active reset configuration, retain imported clocks as history, and reuse those revisions on repeated
|
||||
imports. Opaque extensions survive subsequent transfers without duplicate accumulation. Timeline
|
||||
focus applies to routines as well as events; Ending soonest uses one global queue when reset strips
|
||||
are off. The browser fallback uses an IndexedDB transaction rather than unlocked whole-envelope
|
||||
writes. Account rebase preserves independent routine/pin edits and explicit retirement removal.
|
||||
|
||||
Original v1 bytes remain available after migration. Restore validates account/clock references and
|
||||
saves recovery before replacement. Setup completes before seen-history capture. Source diagnostics
|
||||
state what the feed records; absent failure reasons are described as unavailable, never guessed.
|
||||
|
||||
## Practical limits
|
||||
|
||||
Backups are browser-local and created while the app is used; they cannot recover storage a reader
|
||||
clears. Older completion IDs whose metadata was never retained remain recoverable raw data, but
|
||||
cannot produce invented archive descriptions or historical statistics. Codes currently have one
|
||||
registered game/source. External fonts have system fallbacks offline. Calendar-date code expiry is
|
||||
shown as a date without an invented instant. No accounts, remote sync or push notifications added.
|
||||
|
||||
## Captures inspected
|
||||
|
||||
[Relaxed, light, desktop](assets/2026-10-16-redesign/relaxed-light-1440.png) ·
|
||||
[Structured, dark, desktop](assets/2026-10-16-redesign/structured-dark-1440.png) ·
|
||||
[Structured, dark, phone](assets/2026-10-16-redesign/structured-dark-390.png)
|
||||
|
||||
Additional transient browser logs and captures are in `/tmp/redesign-browser-*` and
|
||||
`/tmp/redesign-final-*`; the facts above and representative images are recorded here for handoff.
|
||||
|
||||
|
||||
## Follow-up: first-visit HTTP/WireGuard startup failure
|
||||
|
||||
The owner reported the recovery screen on first visit through WireGuard `10.20.20.2`. The precise
|
||||
failure reproduced in Chrome at an ordinary HTTP hostname: `crypto.randomUUID` was undefined,
|
||||
initial account minting threw, and initialization returned the generic unavailable/saved-data error.
|
||||
Localhost-only verification had missed this because localhost is a browser secure context.
|
||||
|
||||
All reader ID consumers now use 128-bit `crypto.getRandomValues` through `state/reader/ids.ts`.
|
||||
Three new regressions failed before the fix and pass afterwards. Browser verification on an actual
|
||||
insecure HTTP origin passes both existing-save migration and fresh setup, ordinary edits, adding a
|
||||
server, reset override/revert, backups and reload; the original v1 bytes remain unchanged. Focused
|
||||
independent review found no important issues. The full suite now passes **1,590 tests** across 75
|
||||
files. Typecheck, production rebuild and whitespace checks pass. Rebuilt shell: `9752bd6bf1d2`.
|
||||
|
||||
The existing preview serves the rebuilt files; hard-refreshing bypasses the browser's cached JS.
|
||||
No storage deletion or identity remapping was performed.
|
||||
|
||||
|
||||
## Owner rejection: markup fidelity
|
||||
|
||||
Direct comparison confirms substantial divergence despite matching outer gutters/panels/fonts:
|
||||
|
||||
- Round 7's event-entry structure separates event content, a large deadline with supporting text,
|
||||
and chevron. Production reused EventRow's prior internal structure and decorated it with CSS.
|
||||
- Round 7's Routines uses game bands and aligned columns for progress, cycle/reset, pins and menus.
|
||||
Production renders a flattened library with checkbox options and no equivalent table structure.
|
||||
- Round 7's settings sidebar includes individual game entries with descriptions, leading into their
|
||||
profiles. Production substitutes a generic Your games page and game selector.
|
||||
- Round 7's Timeline places routine periods/progress/reset markers on the shared time axis.
|
||||
Production appends ordinary routine controls below event timelines and uses textual reset strips.
|
||||
- Round 7's Archive story includes interactive per-game outcome breakdowns. Production substitutes
|
||||
per-game text totals.
|
||||
- The approved header has navigation counts and a dedicated Settings placement. Production uses
|
||||
the simpler generic navigation and a different header summary.
|
||||
|
||||
References: design/mockups/round7/converged.html (final CSS from line 1003 onward and final renderer
|
||||
overrides); production ReaderApp.tsx, EventRow.tsx, TimelinePage.tsx, RoutinesPage.tsx,
|
||||
SettingsLayout.tsx, GameSettings.tsx, ArchiveStory.tsx. Earlier render functions/CSS in the mockup
|
||||
are superseded by later overrides and must not be used in isolation.
|
||||
|
||||
Root delivery error: the implementation preserved/reused existing presentation components and
|
||||
added selected reference styling instead of faithfully translating the approved rendered markup.
|
||||
Verification checked functionality and overflow without comparing equivalent reference/production
|
||||
screens and interactions. The final review concentrated on data preservation and did not close that
|
||||
design-fidelity gap. The owner should not have had to identify it after a completion claim.
|
||||
|
||||
No implementation edits or data resets were made during this rejection audit. Existing storage and
|
||||
HTTP fixes remain in place. Required correction: use the approved rendered markup, hierarchy and
|
||||
interactions as the presentation contract, then connect real reader data and compare matching states.
|
||||
@@ -0,0 +1,167 @@
|
||||
# Routine redesign: Round 4 review
|
||||
|
||||
This records the owner's decisions from three mockup rounds and the subsequent review on
|
||||
2026-10-16. These are proposed product changes, **not shipped behaviour**. The implementation spec
|
||||
and migration plan come after review of this combined prototype.
|
||||
|
||||
## Review artifact
|
||||
|
||||
Open [the standalone Round 4 prototype](../../design/mockups/round4/converged.html) in a browser.
|
||||
The review copy is also placed in `~/gacha-mockups/round4/converged.html`, linked from the existing
|
||||
Samba index. Earlier rounds remain intact there.
|
||||
|
||||
The prototype extends the Round 3 archive variant, keeping its ruled timetable visual language and
|
||||
routine library. It is deliberately separate from the production app. Its storage namespace is
|
||||
`event-clock:mockup:round4:v1`, never a production `gacha-tracker` key. Reset demo restores sample
|
||||
state; reloading otherwise keeps density, choices and progress where browser storage is available.
|
||||
|
||||
## Decisions to preserve
|
||||
|
||||
- Checklist and Timeline retain their names. Routines and Archive have their own tabs, as does
|
||||
Settings. Settings uses a sidebar with General first.
|
||||
- Routines owns follows, individual editing and independent Checklist/Timeline pins. Built-ins do
|
||||
not appear under Your own events and games. The library puts Create your own routine first.
|
||||
- Checklist keeps compact per-game routine summaries in its rail. Focusing a game expands its
|
||||
routines; All collapses them. Remove the unrelated Your own events callout from the rail.
|
||||
- The main event list has two densities: Standard by default, with wrapping titles, breathing room
|
||||
and supporting copy, and Compact using the dense table. Density is remembered and independent of
|
||||
routine pins, filters, sort order and Timeline. Both densities open the same event detail.
|
||||
- Game setup offers composable Events / Dailies / Weeklies & monthlies / Cycles shortcuts; individual
|
||||
follows remain editable in Routines. Events is a visibility choice, rather than a routine.
|
||||
- Each game can have multiple servers, with separate routine and daily progress. An additional
|
||||
server starts with events only and fresh routine choices; it does not copy the first server's
|
||||
follows, pins or ticks.
|
||||
- Onboarding begins with events only. Explain each type in plain language, especially cycles:
|
||||
routines following the game's own cadence, such as a mode resetting every fourteen days. Ask
|
||||
whether Checklist or Timeline should open first. Defer pin placement until readers have seen the
|
||||
views.
|
||||
- Returning readers receive an introduction to the new layout in either main view, with existing
|
||||
choices and progress preserved. Changelog remains accessible afterward.
|
||||
- Codes and About live beside Setup in the game's settings area. In-app notices appear in both
|
||||
main views. New means discovered since the reader last viewed that game's codes, not unredeemed.
|
||||
Viewing clears the new indicator without marking redemption. Redemption is separate per server.
|
||||
- Archive defaults to per-game groups, with the ledger's story panel and small statistics above
|
||||
them. History timeline is an additional view. Records remain in the save, with description,
|
||||
source and progress available after the live source changes.
|
||||
- Done and Ignore immediately save an archive record, then retain the active row for about 1.5
|
||||
seconds with Completed/Ignored · Moved to Archive before fading and closing the gap. Undo remains
|
||||
available afterward; Restore also lives in Archive. Reduced motion keeps the confirmation delay
|
||||
and removes the fade. Unknown end dates stay unknown in archived records.
|
||||
- Expired unfinished events belong in Archive as unfinished, rather than silently disappearing.
|
||||
- Timeline uses full rows for routines. It offers Ending soonest and By game. Separate reset strips
|
||||
are optional and sit directly below their own game's events on the same axis. In separate-strip
|
||||
mode, Ending soonest orders game groups by their next deadline to preserve this adjacency.
|
||||
|
||||
## What to review
|
||||
|
||||
1. Compare the same 21 events in Standard and Compact; focus Genshin and inspect its routine rail.
|
||||
2. Complete or ignore an event, observe confirmation/fade, Undo, then open its saved Archive detail
|
||||
and Restore it. Try an event with no announced end.
|
||||
3. Follow/unfollow a routine from the library and pin it independently to each view.
|
||||
4. Add a server in game settings and confirm the other server's choices and ticks stay separate.
|
||||
5. Open Codes, mark one redeemed on a single server, and simulate a new arrival from the demo bar.
|
||||
6. Compare the per-game Archive with History timeline, and check both timeline reset layouts.
|
||||
7. Walk through New user, then Returning user with Timeline as the default.
|
||||
|
||||
## Owner's feedback after Round 4
|
||||
|
||||
The combined prototype was received positively. Further requirements:
|
||||
|
||||
- Restore the richer statistics from Round 3 H's Ledger with a story. Round 4 reduced that panel
|
||||
to completed/ignored/unfinished counts, which lost what the owner liked. Keep the per-game
|
||||
archive as the default body, but restore the completion percentage for the current month with
|
||||
an overall comparison, current/best completion run, logged event check-in days, and unfinished
|
||||
events with near-miss detail. Retain the compact per-game outcome breakdown from that panel.
|
||||
Ignored events must not be counted as failures in the completion percentage. Definitions and
|
||||
scope labels must be explicit: recorded archive history is not all play history, event check-in
|
||||
totals are not unique calendar days, and incomplete/unknown progress cannot support a fabricated
|
||||
near-miss claim. These statistics should use saved progress and dates, not invented telemetry.
|
||||
|
||||
- Keep the colophon, potentially in Settings, and add an app-level About section distinct from a
|
||||
game's About tab. Preserve source/studio credits, author and contributor acknowledgements and
|
||||
useful project links. Moving credits must not hide the feed-age and stale-source disclosures
|
||||
that readers need while looking at deadlines.
|
||||
- The grouped routine summaries are easier to scan but add friction to ticking daily chores.
|
||||
Explore a separate daily panel with directly available one-click ticks, selectable as an
|
||||
alternative to grouping dailies with the other routines. Keep server progress separate and
|
||||
avoid showing the same tick in both panels. Placement and default are not yet settled.
|
||||
- Appearance should support colour schemes in addition to dark/light/system. The choice between
|
||||
curated palettes and reader-edited colours remains open. Any scheme must preserve game identity,
|
||||
urgency meanings, and readable contrast in both modes.
|
||||
- Each game's Settings → Setup includes a custom game-colour control, a preview and Reset to
|
||||
default. Remember the override and apply it consistently wherever that game appears, including
|
||||
Checklist, Timeline, Routines and Archive. It belongs to the game, shared across its servers,
|
||||
separately from the global interface colour scheme. Use the existing readable-hue approach to
|
||||
preserve legibility in dark and light appearances without changing the reader's saved base
|
||||
colour. Game colour expresses identity; urgency colours retain their meaning. Export/import
|
||||
must retain these overrides in the eventual implementation.
|
||||
- Clarify the additional-server action as simultaneous account tracking. Use “Track another
|
||||
server” with helper text: “Play on more than one server? Add your other server to track both
|
||||
accounts separately.” Keep changing the existing server as a distinct control. The added-server
|
||||
setup still starts with fresh routine choices and separate progress, as previously agreed.
|
||||
- Standard event rows without descriptions currently feel too empty. Size rows to their actual
|
||||
content: reserve no description line when absent, reduce vertical padding for title-only rows,
|
||||
and keep the countdown/meter stack from imposing a description-sized minimum height. Preserve
|
||||
wrapping titles and comfortable tap targets; Standard remains roomier than Compact without
|
||||
forcing every event to the same tall height. Do not invent fallback description text to fill
|
||||
the space.
|
||||
- Region & time must let the reader select their display timezone. Automatic, using the browser's
|
||||
timezone, is the default; a manual selection is remembered and can be returned to Automatic.
|
||||
Display named IANA zones with their current UTC offsets, so daylight-saving changes follow the
|
||||
selected zone rather than a fixed offset. This affects displayed dates/times, not the server
|
||||
clocks that determine deadlines, reset boundaries and stored game-day keys. Reader-authored
|
||||
event entry must explicitly identify the timezone used to interpret its input.
|
||||
- In onboarding's What do you want to track step, all type choices stay selectable even when a
|
||||
game has no current matching routines or windows. Explain the empty state beside the choice;
|
||||
distinguish no current active window from no supported built-in routine. An empty result is not
|
||||
a disabled control. Retain the reader's selected tracking intent rather than deriving it solely
|
||||
from which routines happen to exist today. The implementation spec must reconcile this intent
|
||||
with the earlier shortcut-only composition model and define how newly supported routines are
|
||||
offered without silently expanding what existing readers track.
|
||||
- Initial setup also asks for default game server, preferred appearance and whether to show
|
||||
events that have not started. The selected server initializes game setup and individual games
|
||||
can override it. Appearance includes Dark / Light / System and the agreed colour-scheme choices
|
||||
once those are settled. Upcoming events remain off initially, consistent with the existing
|
||||
default, until the reader selects otherwise. The previously agreed Checklist/Timeline default
|
||||
question remains. Present these as a short preferences step rather than crowding the tracking
|
||||
matrix with unrelated controls.
|
||||
|
||||
## Prototype boundaries and implementation work still ahead
|
||||
|
||||
- “Now” and reset clocks are fixed for review. The final app must use the existing per-game reset
|
||||
rules and actual region-specific end times.
|
||||
- Most event content was carried over from the earlier prototypes. Codes, historical archive
|
||||
records, version groups and some descriptions are illustrative. No new source fetch runs here.
|
||||
Live-list source links are matched by title against the existing local feed; unmatched sample
|
||||
events receive no invented source URL.
|
||||
Game8's supplied Genshin code page is a source-shape reference, not an implemented adapter.
|
||||
- Only in-app code notices are proposed. Push, email and background notifications remain outside
|
||||
the product scope.
|
||||
- Some inherited controls remain placeholders: backup and portions of own-event/routine editing.
|
||||
- The archive snapshot works within this prototype's save. Production needs an explicit snapshot
|
||||
retention/export format and a policy for capturing events before they drop from the feed.
|
||||
- Production needs a reviewed migration for existing progress into per-server storage; no existing
|
||||
event ID, daily ID or day key may be changed casually. The archive and code state also need a
|
||||
documented export/import contract. This artifact is not a storage implementation to copy.
|
||||
- `docs/PRD.md`, `docs/DATA-MODEL.md`, `docs/ARCHITECTURE.md`, `docs/DESIGN.md` and the matching
|
||||
AGENTS guidance must be updated with the eventual implementation. They describe the shipped app
|
||||
today, so this review record does not rewrite them as though the redesign had landed.
|
||||
|
||||
## Verification
|
||||
|
||||
Browser verification and screenshots are performed with scratch-installed puppeteer-core and
|
||||
Chrome, at 1440px and 390px. The prototype needs no Bun build or production feed changes. The test
|
||||
journeys cover density parity, navigation, completion/ignore delay and Undo, archived detail and
|
||||
Restore, separate code visibility/redemption, server setup, reset-strip adjacency, onboarding,
|
||||
returning users, persistence, reduced motion and unknown end dates.
|
||||
|
||||
Both width passes completed with no JavaScript errors and no page-level horizontal overflow in
|
||||
the checked screens. The initial mobile pass caught a confirmation notice intercepting Archive
|
||||
row taps; informational notice surfaces now let pointer events through while Undo remains a real
|
||||
button. An early verification-script failure also tried to open a Star Rail event after onboarding
|
||||
had selected only Genshin; the scenario now resets sample state before testing an unknown end.
|
||||
|
||||
Impeccable's mechanical detector reports palette, typography and size differences against the
|
||||
**shipped** DESIGN.md, plus inherited stripe treatments. The new Barlow timetable appearance is
|
||||
intentional and selected in earlier rounds; this prototype does not rewrite the shipped design
|
||||
reference or add detector suppressions. Its visual language remains subject to this review.
|
||||
@@ -0,0 +1,112 @@
|
||||
# Routine redesign: Round 5 review
|
||||
|
||||
This is a review prototype, not a production release or an approved implementation spec. It
|
||||
combines the owner's feedback after Round 4, recorded in
|
||||
[the preceding review](2026-10-16-redesign-round4.md), and the subsequent freshness, backup and
|
||||
focus-bar requests.
|
||||
|
||||
Open [Round 5](../../design/mockups/round5/converged.html), or its copy at
|
||||
`~/gacha-mockups/round5/converged.html` through the shared index. Earlier rounds remain unchanged.
|
||||
|
||||
## What changed
|
||||
|
||||
- **One-click dailies:** an optional panel exposes daily ticks directly, separately per server.
|
||||
The alternative groups dailies with the routine summaries. A daily appears in only one place;
|
||||
pins still decide which dailies appear. The separate panel is the prototype's starting choice,
|
||||
to test the owner's request for immediate access; this default remains reviewable.
|
||||
- **Focus alternatives:** a labelled game toolbar is the starting choice. The review bar's Focus
|
||||
selector also offers a compact game switcher and a picker at the top of the Checklist rail.
|
||||
Each changes the same focus state and counts, and preserves auto-expanding focused routines.
|
||||
Timeline retains a visible toolbar/switcher rather than acquiring a Checklist rail.
|
||||
- **Content-sized Standard rows:** events lacking descriptions use less vertical padding and
|
||||
omit the segmented meter. They retain a wrapping title, countdown and tap target. No filler
|
||||
descriptions are added.
|
||||
- **About and colophon:** Settings → About Event Clock retains the author, source communities,
|
||||
studio credits, acknowledgements, reset-clock reference, disclaimer and project links. The
|
||||
footer links there. Source credits come from the existing local feed, not the selected games.
|
||||
- **Freshness:** Checklist and Timeline visibly state data age and warn about older schedules for
|
||||
the games switched on. Source breakdown distinguishes last successful check from last content
|
||||
change and explains unchanged pages, overdue pulls and never-fetched data. Unknown causes are
|
||||
described as unreported, not invented. Settings → Data & sources exposes the same information.
|
||||
The review bar offers Current / Some stale / All stale / Never fetched / Offline. These are
|
||||
labelled illustrative states; no real fetch runs. Check for new data demonstrates a no-newer-feed
|
||||
result and is disabled offline. The headline data timestamp covers the demo feed as a whole;
|
||||
warnings and named source rows cover selected games. No build timestamp is used.
|
||||
- **Appearance:** Dark / Light / System plus three curated scheme previews, Slate / Warm / Plum.
|
||||
This answers the palette request for review, not the unresolved possibility of a fully editable
|
||||
interface palette. Semantic deadline colours remain separate from game identity.
|
||||
- **Game colour:** Setup adds a colour picker, hex input, preview and Reset to default. An override
|
||||
applies to all the game's servers and all its views. The stored base colour remains unchanged
|
||||
while its rendered colour is adjusted for contrast in the chosen appearance.
|
||||
- **Timezone:** Automatic is the default; named zones can be selected and searched in Region &
|
||||
time. Dates use the selected zone, including daylight saving, without changing server reset
|
||||
boundaries. The prototype's per-game reset clocks remain simplified.
|
||||
- **Additional servers:** “Track another server” explains simultaneous separate account tracking.
|
||||
Added servers still start with fresh routine choices and progress.
|
||||
- **Onboarding:** initial preferences now ask default server, mode/scheme, timezone and whether
|
||||
upcoming events should appear. Game selection, tracking types and Checklist/Timeline default
|
||||
follow. All tracking types remain selectable even if no matching built-in exists. Availability
|
||||
is explained, and selected intent is saved separately from actual follows. The no-matching-types
|
||||
message does not incorrectly call that selection events only. New supported built-ins are
|
||||
described as offers for review rather than automatic extra work for existing readers.
|
||||
- **Archive story:** restores monthly/overall completion percentages, current/best completion
|
||||
run, logged event check-ins and unfinished/near-miss counts, plus per-game outcome bars, above
|
||||
the default per-game archive. Ignored events are excluded from completion-rate denominators.
|
||||
Progress and statistics derive from illustrative saved archive records, and scope is labelled.
|
||||
History timeline remains an alternate view.
|
||||
- **Automatic backup history:** Backup shows seven daily, four weekly and six monthly snapshots,
|
||||
with latest-per-period compression. The retained historical examples are labelled sample; today's
|
||||
snapshot updates from the demo's own state. Restore previews game/routine/archive/progress counts
|
||||
and preserves current data as a recovery copy first. Undo last restore is available. Manual
|
||||
Back up now and demo JSON export/import live alongside the automatic history.
|
||||
|
||||
## Backup semantics used for this review
|
||||
|
||||
The most recent seven days retain one latest snapshot per day. Older snapshots fold into four
|
||||
consecutive seven-day windows. Older records then fold into six calendar-month buckets, keeping
|
||||
the latest snapshot in each. This is up to seventeen retained snapshots, plus a recovery copy from
|
||||
the last restore. No snapshot is invented for a day the reader never opened the app.
|
||||
|
||||
The demo clock is fixed and its bucket calculations use UTC. Production needs to settle and
|
||||
document the stable calendar basis independently of display-timezone changes, handle storage
|
||||
capacity/failures, validate restore files, and use the real export contract. Backups are browser
|
||||
local and are created while the app is used; they cannot be promised for days when it stays closed.
|
||||
Export remains the way to keep a copy outside that browser or move to another device.
|
||||
|
||||
## How to inspect
|
||||
|
||||
1. Compare focus options in the review bar. Focus Genshin; tick EU and Asia dailies separately.
|
||||
2. Compare Standard/Compact, especially rows without descriptions, and switch the daily layout.
|
||||
3. Use the Data selector to inspect every freshness state, then expand Why the data is older.
|
||||
4. Open About, Data & sources, Region & time and Appearance. Change timezone, scheme and game colour.
|
||||
5. Open Archive to inspect richer stats and History timeline.
|
||||
6. Open Backups, review a monthly sample, restore it, then Undo last restore. Demo export files are
|
||||
separate from the production app's format and storage.
|
||||
7. Open New user. Choose preferences and Genshin, then select Cycles despite no matching built-in.
|
||||
8. Set Timeline as the default and use Returning user to check the introduction appears there.
|
||||
|
||||
## Verification and boundaries
|
||||
|
||||
Chrome journeys at 1440px and 390px exercised direct daily ticks, layout choices, focus alternatives,
|
||||
freshness variants, credits, timezone persistence, light/dark/system and schemes, game-colour reset,
|
||||
fresh server setup, Archive stats, backup restore/recovery, onboarding with an empty type,
|
||||
returning-reader introduction and completion/Undo. Both passes had no JavaScript errors and no
|
||||
page-level horizontal overflow in the checked screens. A follow-up checked latest-per-period
|
||||
retention across 400 simulated days, all-games empty-type toggles, and desktop sticky toolbar/rail
|
||||
separation.
|
||||
|
||||
The mechanical design detector reports 30 findings, chiefly differences from the shipped palette,
|
||||
Barlow typography and size/radius ramp, plus inherited archive stripe treatments. These are review
|
||||
artifacts in the owner-selected replacement visual direction; no production design reference or
|
||||
detector suppression was changed to make the prototype appear to match the shipped app.
|
||||
|
||||
An inherited Round 4 Reset handler initially discarded the new state fields. Round 5 now owns
|
||||
reset initialization and its browser check passes. The active prototype data namespace is
|
||||
`event-clock:mockup:round5:v1`; production localStorage keys and progress are untouched.
|
||||
|
||||
The existing demo library/create form, independent pins, completion confirmation/fade/Undo,
|
||||
Archive restore and code-newness/redemption distinctions remain. Some inherited own-event editors
|
||||
are still placeholders. Codes, old Archive history, backup history and source health are sample
|
||||
content. Real parsers, source refreshes, app storage migrations and production backup machinery are
|
||||
not implemented by this artifact. The shipped specification and design documents remain about the
|
||||
shipped app until the redesign's spec and implementation are reviewed.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Routine redesign: three quieter layouts
|
||||
|
||||
Review artifacts, not a production release. This round responds to the owner's concern that
|
||||
Round 5's focus toolbar and stale-data notice occupied too much space, and incorporates the
|
||||
additional feedback received while building the alternatives.
|
||||
|
||||
Open [the comparison index](../../design/mockups/round6/index.html). The shared copy lives at
|
||||
`~/gacha-mockups/round6/index.html`; earlier rounds stay available. Each design is a complete
|
||||
standalone HTML prototype with the same sample event data and the shared features below.
|
||||
|
||||
## The alternatives
|
||||
|
||||
- **A — Inline desk:** retains the left routine/daily rail. Game focus is a small selector beside
|
||||
the event heading, and freshness is a single understated line. Source reasons are one click away.
|
||||
- **B — Game desk:** puts game focus, daily ticks and a short source-status report in the rail.
|
||||
There is no full-width freshness or focus band above the event list. It reserves more attention
|
||||
for the list; its tradeoff is a more populated rail that stacks before events on mobile.
|
||||
- **C — Open ledger:** replaces the rail with a full-width event list, a brief next-deadline row,
|
||||
direct daily ticks in a horizontal strip, and a disclosure for other pinned routines. Game focus
|
||||
and a short freshness report sit with the event heading. It gives titles the most width; the
|
||||
routine controls scroll with the list.
|
||||
|
||||
The chosen timetable appearance is preserved. These compare structure and interaction rather than
|
||||
asking the owner to pick a new visual identity again.
|
||||
|
||||
## Shared decisions implemented for review
|
||||
|
||||
- **Changelog is its own Settings page.** The owner explicitly chose this over a main app tab,
|
||||
and explicitly declined a footer link. Remove the awkward Changelog button from About, remove
|
||||
it from the footer, and render release notes in the page rather than the old overlay. Existing
|
||||
returning-reader news links lead to that Settings page.
|
||||
- **Freshness remains honest and available.** Keep the full data timestamp and age in the footer,
|
||||
scope warnings to selected games, and expose successful-check/content-change times and reasons
|
||||
in Data & sources. Older source data is also explained in the event's detail. Unknown causes
|
||||
remain unreported, not invented. The review bar's Data selector tests the illustrative scenarios.
|
||||
- **Dailies on Checklist belongs in Routines as well.** Routines → Checklist display controls the
|
||||
same preference as the daily panel shortcut and Appearance. Choose Separate panel or Grouped
|
||||
with routines. Independent pins still govern whether a routine appears.
|
||||
- **Checklist routine groups have three behaviours:** Collapsed by default, Expanded by default
|
||||
(still collapsible), and Always expanded (no collapse buttons). Focusing a game expands its
|
||||
routines. The wide layout also honours this for its enclosing routine section.
|
||||
- **Numeric fields show the native stepper.** Browser increase/decrease controls remain visible
|
||||
and use step one, as requested. They are not replaced by a separate custom spinner.
|
||||
- **Appearance includes Custom.** Edit background, surface, text and interface accent separately
|
||||
for light and dark. Stored choices are retained while preview text/surfaces are adjusted for
|
||||
contrast. Game identities and deadline urgency remain separate from the custom interface accent.
|
||||
Individual game colours retain their own control and reset.
|
||||
- **Reset timing can be overridden per game/server.** Game Setup and Region & time expose Automatic
|
||||
versus a custom server-local hour/minute, with Revert to Automatic. The prototype applies the
|
||||
override consistently to daily/routine periods and reset summaries. Reader-authored routines
|
||||
with their own explicit reset retain it. The automatic source clocks remain simplified here;
|
||||
this is not a production reset migration.
|
||||
- **A game's About names its sources.** Links come from that game's entries in the existing local
|
||||
feed, including separate event/banner/pass sources. Use readable schedule labels and thank the
|
||||
editors. This differs from the global About's full, unfiltered colophon.
|
||||
- **Remove Done and ignored events from Lists.** That row only linked to Archive and was not a
|
||||
setting. Archive keeps its own main navigation tab.
|
||||
- **Backup rows say View backup.** They open a preview headed Backup from [date], showing the
|
||||
snapshot's contents. Restore this backup is the explicit action there; the current state is
|
||||
preserved as a recovery copy. The seven-day/four-week/six-month history is retained.
|
||||
- **Returning readers get a guided introduction.** The review bar's Returning user control opens
|
||||
a short welcome in whichever view is preferred, with Take the quick tour and Keep browsing.
|
||||
Four steps visit Routines, Checklist, Archive and Settings. Back and Skip are available; finishing
|
||||
returns to the preferred view. Games, follows and progress remain intact. Production will bind
|
||||
this to a saved release marker, so it is offered once rather than every load.
|
||||
- **Upcoming events moves to What do you want to track?** It is no longer in the initial preference
|
||||
step. The initial preferences retain default server, appearance and timezone; all type choices
|
||||
remain selectable and explained even when no matching built-in exists.
|
||||
|
||||
Standard/Compact density, richer Archive statistics and the history timeline, separate server
|
||||
progress, code-newness versus redemption, completion confirmation/fade/Undo, timezone and the
|
||||
existing routine library/create form remain available in every alternative.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Each alternative uses its own `event-clock:mockup:round6:<letter>:v1` browser namespace. No production
|
||||
keys, parsers or feed data are changed. Source-health states, codes, archived history and historical
|
||||
backups are illustrative. Clock overrides affect the demo's period calculations, not any real stored
|
||||
game-day keys. Production must review migration and the preservation of historical ticks before
|
||||
introducing reader overrides. These are disposable prototypes, not code to transplant into the app.
|
||||
|
||||
## Verification record
|
||||
|
||||
The browser pass checks all three designs at 1440px and 390px: focus and daily controls, native
|
||||
numeric stepping, all disclosure modes, source reasons in event detail, the Changelog page and its
|
||||
absence from About/footer, custom-palette contrast, reset override/revert, per-game source credits,
|
||||
Lists cleanup, backup labels, the moved upcoming option, and the returning-user tour preserving
|
||||
progress. Screenshots cover each main layout plus the new shared pages.
|
||||
|
||||
The first mobile pass found a backup-preview smooth scroll continuing after navigation, allowing a
|
||||
subsequent automated tap to hit the fixed review bar. The preview now scrolls instantly, avoiding a
|
||||
pending animation against a different screen.
|
||||
|
||||
The confirming pass completed all six combinations of design and viewport with no JavaScript
|
||||
errors or page-level horizontal overflow in the checked screens. Custom-palette checks included
|
||||
equal chosen background/text colours and confirmed the rendered text was adjusted to at least
|
||||
4.5:1. Per-server override checks exercised 05:30, verified the 90-minute change from 04:00, then
|
||||
returned to Automatic. The tour returned a Timeline-default reader to Timeline with saved games,
|
||||
follows and progress unchanged.
|
||||
|
||||
Design-hook triage: retain the owner-selected Barlow typography. Narrow value exceptions for that
|
||||
font are scoped to `design/mockups/round6/index.html` only, with evidence in `.impeccable/config.json`.
|
||||
No whole-file or whole-rule exclusions are added. The shipped DESIGN.md remains the production
|
||||
reference; prototype differences in palette/size and inherited archive treatments remain review
|
||||
findings rather than being suppressed broadly.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Routine redesign: one airier design
|
||||
|
||||
**Later owner decision:** retain two interfaces after all: Round 7 and Round 6 **B**, selected
|
||||
in Settings. The earlier single-interface direction below records the mockup review, not the
|
||||
final implementation decision. Replace “Older schedules” with “Event data may be out of date”.
|
||||
See the [production redesign specification](../superpowers/specs/2026-10-16-ui-redesign-design.md).
|
||||
|
||||
Review prototype, not shipped behaviour. Open
|
||||
[the unified mockup](../../design/mockups/round7/converged.html), also copied to
|
||||
`~/gacha-mockups/round7/converged.html` through the shared index.
|
||||
|
||||
## The owner's correction
|
||||
|
||||
Round 6's three alternatives were rejected. The owner asked for:
|
||||
|
||||
- Older-data notices in the same structure as New since you last looked, appearing only when
|
||||
games have older data.
|
||||
- The current production game's focus-pill structure, placed at the top of the left rail as in B.
|
||||
- A more airy, easy-to-read design informed by the original
|
||||
`~/gacha-mockups/c-one-due-list.html`.
|
||||
|
||||
The initial idea of a Simple mode or a spacing toggle was then explicitly reconsidered: the owner
|
||||
does not want two distinct UIs. Carry the spirit of C into one unified interface instead.
|
||||
|
||||
## What this mockup does
|
||||
|
||||
**A shared updates area.** New code notices and Older schedules use two compact rows in one
|
||||
component above the Checklist's event list and above Timeline. The older row lists affected games
|
||||
and links to Details & reasons; no full-width health banner or rail status panel remains. It is
|
||||
absent when the selected games have current sources. If there are no new codes either, the whole
|
||||
updates area disappears. Source reasons remain available in Data & sources and event detail; the
|
||||
full feed timestamp stays in the footer on every load. Scope is the games the reader has switched
|
||||
on, rather than only the temporary focus. Repetitive older-data labels on individual list rows are
|
||||
removed because the updates row already names those games.
|
||||
|
||||
**The existing focus behaviour.** `src/client/components/GameFocus.tsx` is the reference: All and
|
||||
game pills, game hues, outstanding counts, and Next progressing through games and then back to All.
|
||||
Tapping the focused pill returns to All. Pills wrap in their own panel at the top of the Checklist
|
||||
rail; they are absent for a single-game setup. Timeline uses the same pills above its lanes. Focus
|
||||
never changes which games are followed.
|
||||
|
||||
**One visual world.** The original C informs separated panels, quieter rules, softened surfaces,
|
||||
more room around controls and a warmer reading rhythm. Bricolage Grotesque and Figtree replace the
|
||||
compressed timetable typography consistently across the prototype, including settings and forms.
|
||||
The rail and event list have a real gutter; focus, deadline, dailies and routines have separated
|
||||
surfaces. Light mode uses warm paper and dark mode a softer charcoal. No Simple/Detailed mode or
|
||||
global spacing toggle is added. Standard/Compact remain two row densities of this same appearance.
|
||||
Rows with no description remain shorter; comfortable layout is not a fixed tall event height.
|
||||
|
||||
Direct daily ticks, routine disclosure choices, independent pins, custom palettes, per-game colours
|
||||
and reset overrides, source links and thanks, Changelog in Settings, Archive statistics and history,
|
||||
backup history/recovery, timezone and both onboarding flows remain available. The daily-panel layout
|
||||
shortcut is removed from the Checklist rail to reduce controls there; its Routines and Appearance
|
||||
settings continue to edit the same preference.
|
||||
|
||||
## Validation
|
||||
|
||||
Chrome journeys at 1440px and 390px passed with no JavaScript errors and no page-level horizontal
|
||||
overflow in the checked views. They cover conditional/scope-aware notices, empty updates, focus
|
||||
toggle/advance, spacing, routine expansion, reset override/revert, source credit links, custom
|
||||
scheme controls, light-theme game-colour contrast on ground and panel, Changelog location, backup
|
||||
history, Archive stats, Timeline, completion/fade/Undo and onboarding. Contrast adjustment now
|
||||
considers both panel and page grounds, while retaining saved base game colours.
|
||||
|
||||
Screenshots were inspected for Checklist in both appearances, Routines, Data & sources, Archive
|
||||
and Timeline. The first verification script used a nonexistent review-bar Settings shortcut; it
|
||||
was corrected to use the actual Settings tab. No product change was needed for that script error.
|
||||
|
||||
The mechanical design check flags differences from the **shipped** DESIGN.md in palette, type
|
||||
size, radius and inherited archive treatments. Those remain review findings, not broad suppressions.
|
||||
The intentional Bricolage font exception is scoped to this one prototype file, with rationale in
|
||||
`.impeccable/config.json`. The production visual reference is not rewritten before implementation.
|
||||
|
||||
## Boundaries
|
||||
|
||||
The namespace is `event-clock:mockup:round7:v1`. Earlier mockups and production progress remain
|
||||
unchanged. Historical records, codes and source health are illustrative; clocks are fixed and
|
||||
automatic reset defaults simplified. No source requests or production parser/storage changes run
|
||||
here. This is a disposable visual review artifact; the redesign still needs a reviewed production
|
||||
specification and migration plan.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Redesign fidelity correction — 2026-10-17
|
||||
|
||||
> **Owner rejected this implementation after browsing it:** “This is still not even close to the
|
||||
> mockup.” The completion claim is withdrawn. Screenshots and review verdicts below are historical
|
||||
> evidence of the rejected attempt, not acceptance. Continue from
|
||||
> [the second-rejection handoff](../handoffs/2026-10-17-redesign-second-rejection-handoff.md):
|
||||
> launch the current server, then ask the owner for the biggest differences before further UI edits.
|
||||
|
||||
The owner asked to repair the rejected implementation against Round 7 Relaxed and Round 6 B
|
||||
Structured, then explicitly made **Structured the default**. Existing saved Relaxed choices remain
|
||||
valid. This correction is uncommitted in the existing checkout; no branch, publication or data-source
|
||||
refresh was performed.
|
||||
|
||||
## What changed
|
||||
|
||||
- Checklist uses dedicated reference-shaped event rows and next-deadline panel, with separate copy,
|
||||
countdown, supporting date, segmented meter and detail chevron. Header counts and Settings placement
|
||||
follow the reference. Both interfaces retain the owner-approved focus pills.
|
||||
- Routines has By reset/By game bands, aligned game/routine/progress/view-pin/menu columns, live totals,
|
||||
direct daily status, amount bars/inputs, library and a native custom-routine drawer. Bulk pins combine
|
||||
writes per account. Multi-server row identity includes both account and routine.
|
||||
- Timeline plots event spans and routine periods on one axis with frozen labels, progress fill and
|
||||
reset markers. In-progress bar labels remain visible after scrolling. Deadline ordering includes
|
||||
routine resets, and separate reset strips remain available.
|
||||
- Settings restores separate Default view and Your own events and games pages, game profiles, tracking
|
||||
summaries and Other games. Profiles follow tracking → servers → colour → reset timing. A server
|
||||
selector above tracking keeps choices independent without duplicating tall account panels.
|
||||
- Archive restores the story, interactive outcome bars, game sidebar, saved rows and history timeline.
|
||||
Its detail dialog reads saved metadata, original clock region, notes, days and amounts. It does not
|
||||
open the live progress editor or replace historical state with the current clock.
|
||||
- Structured defaults agree across prepaint, normalization, setup and applied appearance. No IDs,
|
||||
localStorage key spaces, migrations or ingestion rules were changed by this correction.
|
||||
|
||||
## Reference comparisons
|
||||
|
||||
Images are in [the evidence directory](assets/2026-10-17-fidelity/). Representative matched views:
|
||||
|
||||
| View | App | Approved reference |
|
||||
| --- | --- | --- |
|
||||
| Structured Checklist | [Desktop](assets/2026-10-17-fidelity/app-structured-dark-1440.png) | [Desktop](assets/2026-10-17-fidelity/reference-structured-dark-1440.png) |
|
||||
| Relaxed Checklist | [Desktop](assets/2026-10-17-fidelity/app-relaxed-dark-1440.png) | [Desktop](assets/2026-10-17-fidelity/reference-relaxed-dark-1440.png) |
|
||||
| Relaxed mobile | [390px](assets/2026-10-17-fidelity/app-relaxed-dark-390.png) | [390px](assets/2026-10-17-fidelity/reference-relaxed-dark-390.png) |
|
||||
| Routines | [Structured](assets/2026-10-17-fidelity/app-structured-routines.png) | [Structured](assets/2026-10-17-fidelity/reference-structured-routines.png) |
|
||||
| Timeline | [Structured](assets/2026-10-17-fidelity/app-structured-timeline.png) | [Structured](assets/2026-10-17-fidelity/reference-structured-timeline.png) |
|
||||
| Game Setup | [Relaxed](assets/2026-10-17-fidelity/app-relaxed-settings.png) | [Relaxed](assets/2026-10-17-fidelity/reference-relaxed-settings.png) |
|
||||
| Archive | [Structured](assets/2026-10-17-fidelity/app-structured-archive.png) | [Structured](assets/2026-10-17-fidelity/reference-structured-archive.png) |
|
||||
|
||||
Additional captures cover both themes at 1440px/390px, populated mobile utility pages, compact rows,
|
||||
event/archive details, custom-routine drawers, archive history and the Settings pages.
|
||||
|
||||
The comparison harness freezes time at 2026-10-16 12:00 UTC and intercepts the event-feed request
|
||||
with the prototype's **synthetic demonstration events**. It seeds disposable browser storage with
|
||||
matching routine choices and 38 archive snapshots. These fixtures are review evidence only: they
|
||||
are not imported by production, copied into public, or written into source snapshots. Actual setup,
|
||||
HTTP migration and offline checks use the built app and its real feed separately.
|
||||
|
||||
Expected semantic differences: real account rows include Asia events, so totals differ from the
|
||||
prototype's game-only event list. One prototype custom task was not seeded. Production has no
|
||||
version metadata for archive events, so it groups saved records without inventing version names.
|
||||
Real reset clocks and available routines govern progress. Code/release notices reflect actual data;
|
||||
Checklist comparisons hide these notices to align the first viewport. The Structured prototype's
|
||||
filter dropdown is intentionally superseded by the owner's focus-pill decision.
|
||||
|
||||
Reproduce with [fidelity-capture.mjs](fidelity-capture.mjs), from the repository root, setting
|
||||
`PUPPETEER_MODULE` to the scratch installation's ESM entry and `APP_URL` / `MOCKUP_URL` to fresh
|
||||
running origins. Chrome is `/usr/bin/google-chrome-stable`; the harness maps event-clock.test to
|
||||
loopback. Rebuild CSS/JS/static without fetching the feed, and use a new app port after each rebuild.
|
||||
|
||||
## Independent review
|
||||
|
||||
The first reviewer invocation hit a session usage limit; a retry succeeded. Its initial material
|
||||
findings were invisible scrolled timeline labels, substituted Settings navigation, routine column/
|
||||
status hierarchy, multi-account row-key collisions, and insufficient populated Archive evidence.
|
||||
One follow-up also required correcting the game-profile hierarchy. Each finding was fixed and the
|
||||
same screenshot paths recaptured. Final independent verdict: **“ship the reviewed corrections.”**
|
||||
All reported material findings were resolved. This verdict is scoped to the compared screens and
|
||||
reported fixes; it is not owner acceptance or blanket proof of every interaction.
|
||||
|
||||
The Impeccable detector ran once and returned `[]` (no findings).
|
||||
|
||||
## Verification
|
||||
|
||||
Final browser build: `a28021bbe1a8`, production React/JSX transform, 117 bundled modules.
|
||||
|
||||
- TypeScript passes. Full offline suite: **1,594 passed, 0 failed**, 5,410 assertions.
|
||||
- Docker-equivalent copied file layout, without docs/design/public: 1,593 passed, one intentional
|
||||
docs-dependent skip, no failures. This validates the COPY-layout assumption; no Docker image was built.
|
||||
- Eight interface/theme/width combinations: no document overflow or page exceptions. Both densities,
|
||||
reduced motion/effective 200% viewport, custom palette and timezone selection checked.
|
||||
- Ordinary insecure HTTP hostname: new setup, existing v1 migration, daily writes, second server,
|
||||
reset override/revert, backup and reload pass; original legacy bytes retained. Structured is the
|
||||
missing-choice default and saved Relaxed survives reload.
|
||||
- Real two-tab simultaneous writes with Web Locks disabled pass using the existing IndexedDB lock.
|
||||
Offline reload passes on localhost's service-worker-capable origin.
|
||||
- Final-build interaction checks: multi-server bulk pins preserve the other view's placement; numeric
|
||||
edits and custom-routine creation persist; completion, Undo, archive detail, Restore and Escape pass.
|
||||
Final profile's server/reset controls were rechecked on insecure HTTP after layout changes.
|
||||
- `git diff --check` passes. No new runtime dependency, source fetch, commit or publication.
|
||||
|
||||
The earlier rejection is preserved in the 2026-10-16 report. Passing tests did not establish its
|
||||
visual fidelity; the side-by-side evidence and scoped review above are the correction's design record.
|
||||
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 37 KiB |