aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/2026-07-02-timer-panel-prototype-3.html
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-05 07:21:07 -0500
committerCraig Jennings <c@cjennings.net>2026-07-05 07:21:07 -0500
commit721185f10d1e389ae3816734b7b8174d33900314 (patch)
tree0e2a09a50e74e17200efc287e841d9e2bee8d8a3 /docs/prototypes/2026-07-02-timer-panel-prototype-3.html
parent0af88d35a6f22c11e346970bec356bae0b74d4a2 (diff)
downloadarchsetup-721185f10d1e389ae3816734b7b8174d33900314.tar.gz
archsetup-721185f10d1e389ae3816734b7b8174d33900314.zip
docs(timer): record the shipped redesign and flip spec to IMPLEMENTEDHEADmain
The timer-panel UI/UX redesign built and shipped to dotfiles across five phased commits. This captures the archsetup-side records. The three design prototypes (the three-directions study, the hero-rack iteration, and the final) land under docs/prototypes, which the spec's Prototype iterations section links. The spec flips DOING to IMPLEMENTED with a history line summarizing the build. The manual-testing checklist is rebuilt around the redesigned panel (repeat timers, recurring alarms with snooze and a ringing state, the configurable pomodoro cycle, the stopwatch sweep dial, locked presets, and bar-tooltip parity), and the two obsolete fuzzel-dialog tests are marked superseded. A dated entry under the closed feature task records the redesign.
Diffstat (limited to 'docs/prototypes/2026-07-02-timer-panel-prototype-3.html')
-rw-r--r--docs/prototypes/2026-07-02-timer-panel-prototype-3.html556
1 files changed, 556 insertions, 0 deletions
diff --git a/docs/prototypes/2026-07-02-timer-panel-prototype-3.html b/docs/prototypes/2026-07-02-timer-panel-prototype-3.html
new file mode 100644
index 0000000..98778fa
--- /dev/null
+++ b/docs/prototypes/2026-07-02-timer-panel-prototype-3.html
@@ -0,0 +1,556 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Timer panel — iteration 3 (waybar + hero-right) · dupre instrument console</title>
+<style>
+:root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5;
+ --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d;
+ --wash:#2c2f32; --pass:#74932f; --fail:#cb6b4d; --sage:#8a9a5b;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono","Symbols Nerd Font",monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html{background:var(--ground)}
+body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 5rem;line-height:1.45;
+ background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)}
+.wrap{max-width:1120px;margin:0 auto}
+.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase}
+h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem}
+.masthead p{color:var(--dim);font-size:.86rem;max-width:94ch}
+.masthead p b{color:var(--silver)}
+.cols{display:flex;gap:34px;align-items:flex-start;margin-top:1.4rem;flex-wrap:wrap}
+.side{flex:1;min-width:300px}
+.side h2{color:var(--steel);font-size:.74rem;letter-spacing:.22em;text-transform:uppercase;margin:0 0 .5rem;display:flex;align-items:center;gap:10px}
+.side h2::after{content:"";height:1px;background:var(--wash);flex:1}
+.side ul{list-style:none;font-size:.8rem;color:var(--dim);display:flex;flex-direction:column;gap:7px}
+.side li{display:flex;gap:9px}.side li::before{content:"›";color:var(--gold);flex:none}
+.side li b{color:var(--silver);font-weight:400}
+
+/* ---------- waybar preview ---------- */
+.barcap{color:var(--steel);font-size:.58rem;letter-spacing:.26em;text-transform:uppercase;margin-bottom:7px;display:flex;align-items:center;gap:9px}
+.barcap::after{content:"";height:1px;background:var(--wash);flex:1}
+.wbar{width:420px;background:linear-gradient(180deg,#141312,#0e0d0c);border:1px solid #262320;border-radius:16px;
+ padding:6px 10px;display:flex;align-items:center;gap:8px;box-shadow:0 8px 20px rgba(0,0,0,.5);margin-bottom:6px}
+.wbar .fillspace{flex:1;color:var(--dim);font-size:10px;letter-spacing:.1em;padding-left:4px}
+.wmod{display:inline-flex;align-items:center;gap:7px;color:var(--silver);background:transparent;border:1.5px solid var(--gold);
+ border-radius:14px;padding:4px 12px;cursor:pointer;font-size:12.5px;white-space:nowrap;min-width:78px;justify-content:center}
+.wmod:hover{background:var(--wash)}
+.wmod .wg{font-size:16px;line-height:1}
+.wmod .wt{font-variant-numeric:tabular-nums;font-weight:700}
+.wmod .wp{color:var(--dim);font-size:11px}
+.wmod.urgent{color:var(--fail)} .wmod.paused{color:var(--dim)}
+.wmod.pomodoro-work{color:var(--gold)} .wmod.pomodoro-break{color:var(--sage)}
+.wmod.idle{color:var(--silver);min-width:0;border-color:#4a463c}
+.wtip{width:420px;background:var(--well);border:1px solid #201d17;border-radius:8px;padding:7px 10px;font-size:11px;color:var(--dim);margin-bottom:16px}
+.wtip .th{color:var(--steel);letter-spacing:.14em;text-transform:uppercase;font-size:.56rem;margin-bottom:3px}
+.wtip .tl{color:var(--silver);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+
+/* ---------- faceplate ---------- */
+.panel{width:420px;flex:none;background:linear-gradient(180deg,var(--raise),var(--panel));border:1px solid #262320;
+ border-radius:14px;padding:15px;position:relative;box-shadow:inset 0 1px 0 rgba(255,255,255,.05),0 14px 34px rgba(0,0,0,.55)}
+.phead{display:flex;align-items:center;gap:10px;margin-bottom:12px}
+.phead .brand{color:var(--gold);font-size:.72rem;letter-spacing:.24em;text-transform:uppercase}
+.phead .pcount{margin-left:auto;color:var(--dim);font-size:.66rem;letter-spacing:.14em}
+.phead .pcount b{color:var(--cream)}
+.x-btn{margin-left:6px;color:var(--dim);border:0;background:transparent;font:inherit;font-size:1rem;cursor:pointer;border-radius:50%;width:26px;height:26px;line-height:1;flex:0 0 auto}
+.x-btn:hover{background:var(--wash);color:var(--silver)}
+.panel.closed{display:none}
+.wbar.reopen{outline:1px dashed var(--slate);outline-offset:2px}
+
+/* ---------- primitives ---------- */
+.lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);box-shadow:0 0 6px 1px rgba(116,147,47,.55);flex:none}
+.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(218,181,61,.6)}
+.lamp.red{background:var(--fail);box-shadow:0 0 6px 1px rgba(203,107,77,.55)}
+.lamp.off{background:var(--wash);box-shadow:none}
+.key{font:inherit;font-size:11.5px;letter-spacing:.05em;color:var(--silver);cursor:pointer;
+ background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-bottom-color:#0c0b0a;
+ border-radius:8px;padding:7px 11px;box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4)}
+.key:hover{color:var(--gold);border-color:var(--gold)}.key:active{transform:translateY(1px)}.key:disabled{opacity:.4;cursor:default}
+.key.on{color:var(--panel);background:linear-gradient(180deg,#f0d879,var(--gold));border-color:var(--gold-hi);font-weight:700}
+.key.red{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)}
+.key.sm{padding:5px 8px;font-size:10.5px;border-radius:7px}
+.key.icon{padding:6px 9px;font-size:14px;line-height:1}
+.preset{color:var(--dim);cursor:pointer;border:1px solid #2a2723;background:#141210;border-radius:14px;font-size:11.5px;padding:4px 10px;letter-spacing:.02em;display:inline-flex;align-items:center}
+.preset:hover{color:var(--silver);border-color:var(--slate)}
+.preset.on{color:var(--panel);background:linear-gradient(180deg,#f0d879,var(--gold));border-color:var(--gold-hi);font-weight:700}
+.preset .x{color:inherit;opacity:.5;margin-left:6px;font-size:13px}.preset .x:hover{opacity:1;color:var(--fail)}
+.badge{font-size:.6rem;letter-spacing:.16em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px;text-transform:uppercase}
+.badge.red{background:var(--fail);color:var(--cream)}.badge.dim{background:var(--wash);color:var(--steel)}.badge.sage{background:var(--sage);color:var(--panel)}
+.seg{display:flex;border:1px solid #33302b;border-radius:8px;overflow:hidden}
+.seg button{font:inherit;font-size:11px;color:var(--silver);background:#191715;border:0;border-right:1px solid #33302b;padding:7px 0;cursor:pointer;flex:1;letter-spacing:.02em}
+.seg button:last-child{border-right:0}
+.seg button.on{background:linear-gradient(180deg,#f0d879,var(--gold));color:var(--panel);font-weight:700}
+.switch{width:38px;height:20px;border-radius:11px;background:var(--wash);border:1px solid var(--slate);position:relative;cursor:pointer;flex:none}
+.switch::after{content:"";position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:var(--dim);transition:left .15s}
+.switch.on{background:var(--slate);border-color:var(--gold)}.switch.on::after{left:20px;background:var(--gold)}
+.engrave{color:var(--steel);font-size:.58rem;letter-spacing:.26em;text-transform:uppercase;display:flex;align-items:center;gap:9px;margin:2px 0}
+.engrave::before,.engrave::after{content:"";height:1px;background:var(--wash);flex:1}.engrave::before{max-width:10px}.engrave .cnt{color:var(--dim);letter-spacing:.1em;text-transform:none}
+.tin{font:inherit;font-size:12px;color:var(--cream);background:#0d0f10;border:1px solid #231f18;border-radius:7px;padding:7px 9px;width:100%;outline:none}
+.tin:focus{border-color:var(--gold)}.tin::placeholder{color:var(--dim)}.tin.bad{border-color:var(--fail);color:var(--fail)}
+.numin{font:inherit;font-size:12px;color:var(--cream);background:#0d0f10;border:1px solid #231f18;border-radius:6px;padding:5px 4px;width:46px;text-align:center;outline:none;font-variant-numeric:tabular-nums}
+.numin:focus{border-color:var(--gold)}
+@keyframes fieldflash{0%{border-color:var(--gold-hi);background:rgba(218,181,61,.22)}100%{border-color:#231f18;background:#0d0f10}}
+.tin.flash,.numin.flash{animation:fieldflash .7s ease}
+.arm{font:inherit;font-size:11px;color:var(--silver);cursor:pointer;background:#191715;border:1px solid #33302b;border-radius:7px;padding:6px 9px}
+.arm.armed{background:rgba(203,107,77,.14);border-color:var(--fail);color:var(--fail)}
+.ring{border-radius:50%;background:conic-gradient(var(--gold) calc(var(--p)*1%),var(--wash) 0);display:grid;place-items:center;position:relative}
+.ring.warn{background:conic-gradient(var(--fail) calc(var(--p)*1%),var(--wash) 0)}
+.ring::before{content:"";position:absolute;inset:7px;border-radius:50%;background:var(--well)}
+.ring b{position:relative;z-index:1;text-align:center}
+.dots{display:flex;gap:4px;align-items:center}
+.dots i{width:7px;height:7px;border-radius:50%;background:var(--wash);flex:none}
+.dots i.on{background:var(--steel)}.dots i.now{background:var(--gold);box-shadow:0 0 6px 1px rgba(218,181,61,.6)}.dots i.long{outline:1px solid var(--gold);outline-offset:1px}
+.days7{display:flex;gap:4px}
+.days7 button{font:inherit;font-size:10px;width:22px;height:22px;border-radius:50%;border:1px solid #33302b;background:#141210;color:var(--dim);cursor:pointer;padding:0}
+.days7 button.on{background:linear-gradient(180deg,#f0d879,var(--gold));color:var(--panel);border-color:var(--gold-hi);font-weight:700}
+
+/* ---------- HERO (info left, donut right) ---------- */
+.hero{background:var(--well);border:1px solid #201d17;border-radius:11px;padding:15px;display:flex;flex-direction:column;gap:13px;margin-bottom:12px}
+.hero .htop{display:flex;gap:15px;align-items:center}
+.hero.fire{animation:firef .6s ease-in-out 3}
+.hero.ringing{animation:ringf .9s ease-in-out infinite;border-color:var(--fail)}
+@keyframes firef{50%{background:rgba(203,107,77,.22)}}
+@keyframes ringf{50%{background:rgba(203,107,77,.16)}}
+.hero .rhs{min-width:0;flex:1;display:flex;flex-direction:column;gap:5px}
+.hero .htype{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
+.hero .htype .g{color:var(--gold);font-size:17px}
+.hero .hlabel{color:var(--cream);font-size:15px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.hero .hbig{color:var(--cream);font-size:38px;line-height:1;font-weight:700;font-variant-numeric:tabular-nums;display:flex;align-items:center;gap:11px}
+.hero.paused .hbig{color:var(--steel)}
+.hero .hsub{color:var(--dim);font-size:11px;letter-spacing:.05em}
+.hero .lapbadge{font-size:11px;letter-spacing:.12em;color:var(--silver);border:1px solid var(--slate);border-radius:6px;
+ padding:2px 8px;font-weight:400;font-variant-numeric:tabular-nums;background:transparent;line-height:1;align-self:center}
+/* stopwatch sweep dial — analog second-hand, one revolution per minute */
+.swdial{width:86px;height:86px;border-radius:50%;background:var(--well);border:1px solid #201d17;position:relative;display:block}
+.swdial::before{content:"";position:absolute;inset:7px;border-radius:50%;border:2px solid var(--wash)}
+.swtick{position:absolute;top:5px;left:50%;width:2px;height:8px;margin-left:-1px;background:var(--steel);border-radius:1px;transform-origin:50% 38px}
+.swtick.q{transform:rotate(90deg)}.swtick.h{transform:rotate(180deg)}.swtick.t{transform:rotate(270deg)}
+.swhand{position:absolute;left:calc(50% - 1px);bottom:50%;width:2px;height:31px;background:var(--gold-hi);
+ transform-origin:50% 100%;border-radius:1px;box-shadow:0 0 5px rgba(255,215,95,.5)}
+.swhub{position:absolute;left:50%;top:50%;width:9px;height:9px;margin:-4.5px 0 0 -4.5px;border-radius:50%;
+ background:var(--gold);box-shadow:0 0 0 2px var(--well),0 0 6px rgba(218,181,61,.5)}
+.hero .donut{flex:none}
+.transport{display:flex;gap:7px;flex-wrap:wrap;justify-content:flex-start}
+
+/* ---------- CREATE (middle) ---------- */
+.create{background:var(--well);border:1px solid #201d17;border-radius:10px;padding:11px;margin-bottom:12px}
+.create .row{display:flex;gap:7px;align-items:center;margin-top:9px;flex-wrap:wrap}
+.presets{display:flex;gap:6px;flex-wrap:wrap;margin-top:9px}
+.cfg{margin-top:9px;display:flex;flex-direction:column;gap:7px}
+.cfg .crow{display:flex;align-items:center;gap:8px}
+.cfg .crow .lbl{width:58px;color:var(--steel);font-size:.58rem;letter-spacing:.14em;text-transform:uppercase;flex:none}
+.cfg .crow .u{color:var(--dim);font-size:10px}
+.cfg .crow .sl{color:var(--steel);font-size:.58rem;letter-spacing:.1em;text-transform:uppercase;width:9px}
+
+/* ---------- LIST (bottom) ---------- */
+.qlist{display:flex;flex-direction:column;gap:8px}
+.qrow{display:flex;align-items:center;gap:10px;background:#141210;border:1px solid #201d17;border-radius:9px;padding:8px 10px}
+.qrow.fire{animation:firef .6s ease-in-out 3}.qrow.ringing{animation:ringf .9s ease-in-out infinite;border-color:var(--fail)}
+.qrow .g{color:var(--gold);font-size:16px;width:19px;text-align:center;flex:none}
+.qrow .meta{min-width:0;display:flex;flex-direction:column;gap:2px}
+.qrow .meta b{color:var(--cream);font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:104px}
+.qrow .meta .ty{color:var(--dim);font-size:.56rem;letter-spacing:.1em;text-transform:uppercase}
+.qrow .rd{margin-left:auto;font-size:18px;color:var(--cream);font-weight:700;font-variant-numeric:tabular-nums;white-space:nowrap}
+.qrow.paused .rd{color:var(--steel)}
+.qrow .ctrls{display:flex;gap:5px;flex:none}
+.empty{color:var(--dim);font-size:12px;text-align:center;padding:14px 6px}
+.toasts{position:absolute;left:12px;right:12px;bottom:10px;display:flex;flex-direction:column;gap:6px;pointer-events:none;z-index:5}
+.toast{font-size:11px;color:var(--cream);background:var(--slate);border-radius:7px;padding:6px 10px;box-shadow:0 4px 12px rgba(0,0,0,.5);animation:tin .2s ease}
+.toast.red{background:linear-gradient(180deg,#b25c43,#8f3f2c)}.toast.gold{background:linear-gradient(180deg,#b79a34,#8a7524);color:var(--panel)}
+@keyframes tin{from{opacity:0;transform:translateY(6px)}}
+@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
+</style>
+</head>
+<body>
+<div class="wrap">
+<header class="masthead">
+ <div class="eyebrow">archsetup · dupre panel family · timer · iteration 3</div>
+ <h1>Timer panel — waybar + hero-right</h1>
+ <p>Third pass. The <b>hero donut moved to the right</b> of the readout; the redundant "bar slot" badge is gone
+ (the hero <i>is</i> the bar slot). Above the panel sits a <b>live, accurate preview of the actual waybar module</b> —
+ the same glyph + countdown + "+N" and state colours <code>wtimer render</code> emits, with its hover tooltip.
+ Presets (renamed from "chips") <b>flash the fields</b> on load instead of toasting, alarms gain a <b>half-past</b> preset,
+ and each type picked up create-strip ideas from its category's best apps: timers <b>repeat</b>, alarms carry
+ <b>recurring days + snooze</b> (with a ringing state), pomodoro keeps its configurable cycle.</p>
+</header>
+
+<div class="cols">
+ <div>
+ <div class="barcap">the waybar module · live</div>
+ <div class="wbar" id="wbar"></div>
+ <div class="wtip" id="wtip"></div>
+ <div class="panel" id="panel"></div>
+ </div>
+ <div class="side">
+ <h2>This pass</h2>
+ <ul>
+ <li><b>Donut on the right</b> of the hero info; <b>no "bar slot" label</b>.</li>
+ <li><b>Live waybar preview</b> — mirrors <code>wtimer render</code>: glyph + countdown + "+N", state colour, tooltip.</li>
+ <li><b>"Preset"</b> replaces "chip"; loading one <b>flashes the field(s)</b>, no toast.</li>
+ <li><b>Half-past</b> alarm preset — next X:30, the sibling of top-of-hour's X:00.</li>
+ </ul>
+ <h2 style="margin-top:1.5rem">Borrowed per category</h2>
+ <ul>
+ <li><b>Timer</b> (MultiTimer, Multi Timer): auto-<b>repeat</b> — restart on finish. Toggle in the create row.</li>
+ <li><b>Alarm</b> (Alarm Clock Xtreme, Alarmy): <b>recurring weekdays</b> + <b>snooze</b>; fires into a ringing state with SNOOZE / DISMISS.</li>
+ <li><b>Stopwatch</b> (Stopwatch Timer): sweep dial + infinite <b>laps</b> with the last lap beside the count; run-save deferred to a vNext.</li>
+ <li><b>Pomodoro</b> (Pomofocus): configurable work/rest short+long, long-break interval, auto-advance, cycle dots.</li>
+ </ul>
+ </div>
+</div>
+</div>
+
+<script>
+"use strict";
+const reduced=matchMedia('(prefers-reduced-motion: reduce)').matches;
+const el=(t,c,h)=>{const n=document.createElement(t);if(c)n.className=c;if(h!=null)n.innerHTML=h;return n;};
+
+const GL={ timer:'\u{F051B}', alarm:'\u{F0020}', stopwatch:'\u{F13AB}', idle:'\u{F051B}',
+ pomo_work:'\u{F051C}', pomo_break:'\u{F0176}', paused:'\u{F03E4}',
+ play:'\u{F040A}', promote:'\u{F0143}', cancel:'\u{F0156}', add:'\u{F0415}', clear:'\u{F0A79}', repeat:'\u{F0456}', bell:'\u{F0020}' };
+
+function parseDuration(v){ if(v==null)return null;v=String(v).trim().toLowerCase();if(v==='')return null;
+ if(/^\d+$/.test(v))return parseInt(v,10)*60; if(!/^(\s*\d+\s*[hms])+$/.test(v))return null;
+ let m,tot=0;const re=/(\d+)\s*([hms])/g; while((m=re.exec(v)))tot+=m[2]==='h'?+m[1]*3600:m[2]==='m'?+m[1]*60:+m[1]; return tot>0?tot:null; }
+function resolveAlarm(v,now){ v=String(v||'').trim().toLowerCase();
+ if(v.startsWith('+')){const s=parseDuration(v.slice(1));return s==null?null:now+s;}
+ if(v==='@hour'||v==='top of hour'){const d=new Date(now*1000);d.setMinutes(0,0,0);d.setHours(d.getHours()+1);return d.getTime()/1000;}
+ if(v==='@half'||v==='half past'||v==='half-past'){const d=new Date(now*1000);d.setSeconds(0,0);d.setMinutes(30);let e=d.getTime()/1000;if(e<=now){d.setHours(d.getHours()+1);e=d.getTime()/1000;}return e;}
+ const t=v.match(/^(\d{1,2}):(\d{2})$/); if(t){const hh=+t[1],mm=+t[2];if(hh>23||mm>59)return null;const d=new Date(now*1000);d.setHours(hh,mm,0,0);let e=d.getTime()/1000;if(e<=now)e+=86400;return e;} return null; }
+function nextAlarm(hh,mm,days,now){ const base=new Date(now*1000);
+ for(let d=0;d<=7;d++){const c=new Date(base);c.setDate(base.getDate()+d);c.setHours(hh,mm,0,0);const e=c.getTime()/1000;
+ if(e<=now)continue; if(!days.length||days.includes(c.getDay()))return e;} return now+86400; }
+const fmtTime=s=>{s=Math.max(0,Math.floor(s));const h=Math.floor(s/3600),m=Math.floor((s%3600)/60),x=s%60;
+ return h?`${h}:${String(m).padStart(2,'0')}:${String(x).padStart(2,'0')}`:`${m}:${String(x).padStart(2,'0')}`;};
+const fmtClock=e=>{const d=new Date(e*1000);return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;};
+const DAYNAMES=['S','M','T','W','T','F','S'];
+
+const DEFAULT_PRESETS=()=>({
+ timer:[{label:'5m',value:'5m',locked:true},{label:'25m',value:'25m',locked:true},{label:'10m',value:'10m',locked:true},
+ {label:'15m',value:'15m',locked:true},{label:'30m',value:'30m',locked:true},{label:'60m',value:'60m',locked:true},{label:'2h',value:'2h',locked:true}],
+ alarm:[{label:'+30m',value:'+30m',locked:true},{label:'top of hour',value:'@hour',locked:true},{label:'half-past',value:'@half',locked:true},{label:'07:00',value:'07:00',locked:true}],
+ stopwatch:[]
+});
+const POMO_PRESETS=[{label:'Classic',ws:25,wl:50,rs:5,rl:15,iv:4,locked:true},{label:'Deep',ws:50,wl:50,rs:10,rl:30,iv:3,locked:true},{label:'Sprint',ws:15,wl:25,rs:3,rl:10,iv:4,locked:true}];
+const POMO_DEFAULT={ws:25,wl:50,rs:5,rl:15,iv:4,auto:true};
+const TYPES=['timer','alarm','stopwatch','pomodoro'];
+const COUNTDOWN=['timer','alarm','pomodoro'];
+const MAX=10;
+
+class Engine{
+ constructor(){this.items=[];this.seq=0;this.primary=null;this.presets=DEFAULT_PRESETS();this.pomoPresets=POMO_PRESETS.map(p=>({...p}));}
+ now(){return Date.now()/1000;}
+ count(){return this.items.length;}
+ full(){return this.items.length>=MAX;}
+ add(type,value,label,opts){
+ opts=opts||{};
+ if(this.full())return {ok:false,reason:`queue full (${MAX}/${MAX})`};
+ const now=this.now();this.seq++;const id='t'+this.seq;const it={id,type,label:label||''};
+ if(type==='timer'){const s=parseDuration(value);if(s==null)return {ok:false,reason:`bad duration: “${value}”`};it.target=now+s;it.total=s;it.repeat=!!opts.repeat;}
+ else if(type==='alarm'){const e=resolveAlarm(value,now);if(e==null)return {ok:false,reason:`bad time: “${value}”`};
+ const d=new Date(e*1000);it.hh=d.getHours();it.mm=d.getMinutes();it.days=(opts.days||[]).slice();it.snooze=opts.snooze||9;
+ it.target=it.days.length?nextAlarm(it.hh,it.mm,it.days,now):e;it.total=Math.max(1,it.target-now);}
+ else if(type==='pomodoro'){const c=opts.ws?opts:POMO_DEFAULT;it.cfg={ws:c.ws*60,wl:c.wl*60,rs:c.rs*60,rl:c.rl*60,iv:Math.max(1,c.iv),auto:c.auto!==false};
+ it.phase='work';it.cycle=1;const deep=(1%it.cfg.iv===0);const len=deep?it.cfg.wl:it.cfg.ws;it.target=now+len;it.total=len;}
+ else if(type==='stopwatch'){it.start=now;it.laps=[];}
+ else return {ok:false,reason:`unknown type: ${type}`};
+ this.items.push(it);if(!this.primary)this.primary=id;return {ok:true,id};
+ }
+ find(id){return this.items.find(i=>i.id===id);}
+ isPaused(it){return it.type==='stopwatch'?it.paused_elapsed!=null:it.paused_left!=null;}
+ remaining(it,ref){ref=ref==null?this.now():ref;if(it.type==='stopwatch')return this.isPaused(it)?it.paused_elapsed:ref-it.start;return this.isPaused(it)?it.paused_left:it.target-ref;}
+ toggle(id){const it=this.find(id);if(!it)return;const now=this.now();
+ if(it.type==='stopwatch'){if(this.isPaused(it)){it.start=now-it.paused_elapsed;it.paused_elapsed=null;}else it.paused_elapsed=now-it.start;}
+ else{if(this.isPaused(it)){it.target=now+it.paused_left;it.paused_left=null;it.awaiting=false;}else it.paused_left=it.target-now;}}
+ cancel(id){const i=this.items.findIndex(x=>x.id===id);if(i<0)return;this.items.splice(i,1);if(this.primary===id)this.primary=null;}
+ cancelAll(){this.items=[];this.primary=null;}
+ promote(id){if(this.find(id))this.primary=id;}
+ cycle(dir){const ids=this.items.map(i=>i.id);if(!ids.length)return;let c=ids.indexOf(this.effectivePrimary());c=c<0?0:c;this.primary=ids[dir==='prev'?(c-1+ids.length)%ids.length:(c+1)%ids.length];}
+ lap(id){const it=this.find(id);if(!it||it.type!=='stopwatch')return;it.laps.push({t:this.remaining(it)});}
+ snooze(id){const it=this.find(id);if(!it)return;it.ringing=false;const m=it.snooze>0?it.snooze:9;it.target=this.now()+m*60;it.total=m*60;}
+ dismiss(id){const it=this.find(id);if(!it)return;if(it.days&&it.days.length){it.ringing=false;it.target=nextAlarm(it.hh,it.mm,it.days,this.now());it.total=Math.max(1,it.target-this.now());}else this.cancel(id);}
+ effectivePrimary(){const items=this.items;if(!items.length)return null;const ids=items.map(i=>i.id);
+ const ring=items.find(i=>i.ringing);if(ring)return ring.id;
+ if(ids.includes(this.primary))return this.primary;const now=this.now();
+ const acd=items.filter(i=>COUNTDOWN.includes(i.type)&&!this.isPaused(i));
+ if(acd.length)return acd.reduce((a,b)=>this.remaining(a,now)<=this.remaining(b,now)?a:b).id;
+ const asw=items.filter(i=>i.type==='stopwatch'&&!this.isPaused(i));if(asw.length)return asw[0].id;return ids[0];}
+ sortKey(it){const now=this.now(),p=this.isPaused(it),sw=it.type==='stopwatch',r=this.remaining(it,now);
+ if(it.ringing)return [-1,0,+it.id.slice(1)]; return sw?[p?3:2,-r,+it.id.slice(1)]:[p?1:0,r,+it.id.slice(1)];}
+ rows(){const prim=this.effectivePrimary(),now=this.now();
+ return this.items.slice().sort((a,b)=>{const ka=this.sortKey(a),kb=this.sortKey(b);for(let i=0;i<ka.length;i++){if(ka[i]<kb[i])return -1;if(ka[i]>kb[i])return 1;}return 0;}).map(it=>this.row(it,prim,now));}
+ daysLabel(it){ if(!it.days||!it.days.length)return 'once'; if(it.days.length===7)return 'daily';
+ const wk=[1,2,3,4,5],we=[0,6];
+ if(wk.every(d=>it.days.includes(d))&&it.days.length===5)return 'weekdays';
+ if(we.every(d=>it.days.includes(d))&&it.days.length===2)return 'weekends';
+ return it.days.slice().sort().map(d=>DAYNAMES[d]).join(''); }
+ row(it,prim,now){
+ const rem=this.remaining(it,now),paused=this.isPaused(it);
+ let disp,sub='',warn=false,prog=null,glyph,pomo=null,ringing=!!it.ringing,badges=[],lastLap=null,sweep=null;
+ if(it.type==='alarm'){
+ if(ringing){disp='RING';sub='alarm ringing';warn=true;prog=1;glyph=GL.alarm;}
+ else{disp=fmtClock(it.target);sub=`${this.daysLabel(it)} · fires ${fmtClock(it.target)}`;prog=Math.max(0,Math.min(1,rem/it.total));glyph=GL.alarm;}
+ if(it.days&&it.days.length)badges.push({t:this.daysLabel(it),c:'sage'});
+ }
+ else if(it.type==='pomodoro'){disp=fmtTime(rem);prog=Math.max(0,Math.min(1,rem/it.total));const deep=(it.cycle%it.cfg.iv===0);
+ const ph=it.phase==='work'?(deep?'long work':'work'):(deep?'long break':'short break');
+ sub=it.awaiting?`ready · start ${it.phase==='work'?'work':'break'}`:`${ph} · cycle ${it.cycle}/${it.cfg.iv}`;
+ glyph=it.phase==='work'?GL.pomo_work:GL.pomo_break;pomo={cycle:it.cycle,iv:it.cfg.iv,phase:it.phase,awaiting:!!it.awaiting};}
+ else if(it.type==='stopwatch'){disp=fmtTime(rem);lastLap=it.laps.length?it.laps[it.laps.length-1].t:null;
+ sub=it.laps.length?`${it.laps.length} lap${it.laps.length>1?'s':''}`:'running';glyph=GL.stopwatch;sweep=(Math.max(0,rem)%60)/60;}
+ else {disp=fmtTime(rem);sub=it.repeat?'timer · repeats':'timer';prog=Math.max(0,Math.min(1,rem/it.total));glyph=GL.timer;if(it.repeat)badges.push({t:'repeat',c:''});}
+ if(prog!=null&&!ringing&&rem<=Math.min(30,it.total*0.15))warn=true;
+ if(paused)glyph=GL.paused;
+ return {id:it.id,type:it.type,glyph,label:it.label||({timer:'Timer',alarm:'Alarm',stopwatch:'Stopwatch',pomodoro:'Pomodoro'})[it.type],
+ typeLabel:it.type,disp,sub,paused,ringing,primary:it.id===prim,prog,warn,pomo,badges,lastLap,sweep,laps:it.laps?it.laps.length:0};
+ }
+ /* mirror wtimer render_payload for the bar */
+ barPayload(){ const now=this.now(); const items=this.items;
+ if(!items.length)return {glyph:GL.idle,text:'',plus:0,cls:'idle',tip:['No timers']};
+ const pid=this.effectivePrimary(); const p=this.find(pid);
+ let cls; if(p.ringing)cls='urgent'; else if(this.isPaused(p))cls='paused';
+ else if(p.type==='pomodoro')cls=(p.phase==='work'?'pomodoro-work':'pomodoro-break');
+ else if((p.type==='timer'||p.type==='alarm')&&this.remaining(p,now)<60)cls='urgent'; else cls=p.type;
+ const glyph=this.isPaused(p)?GL.paused:(p.type==='pomodoro'?(p.phase==='work'?GL.pomo_work:GL.pomo_break):GL[p.type]);
+ const text=p.ringing?'RING':fmtTime(this.remaining(p,now));
+ const tip=[items.length!==1?`${items.length} active`:'1 timer'];
+ for(const i of items){const g=this.isPaused(i)?GL.paused:(i.type==='pomodoro'?(i.phase==='work'?GL.pomo_work:GL.pomo_break):GL[i.type]);
+ const lb=i.label||i.type;const st=this.isPaused(i)?' (paused)':(i.ringing?' (ringing)':'');
+ const val=i.type==='alarm'?(i.ringing?'RING':fmtClock(i.target)):(i.type==='pomodoro'?`${i.cycle}/${i.cfg.iv} ${fmtTime(this.remaining(i,now))}`:fmtTime(this.remaining(i,now)));
+ tip.push(`${g} ${lb} ${val}${st}`);}
+ return {glyph,text,plus:items.length-1,cls,tip};
+ }
+ presetsFor(t){return (this.presets[t]||[]).map(p=>({...p}));}
+ addPreset(t,label,value){if(!TYPES.includes(t)||t==='pomodoro'||t==='stopwatch')return {ok:false,reason:'no custom preset here'};
+ if(t==='timer'&&parseDuration(value)==null)return {ok:false,reason:'bad duration'};(this.presets[t]||(this.presets[t]=[])).push({label,value,locked:false});return {ok:true};}
+ deletePreset(t,label){const a=this.presets[t]||[];const i=a.findIndex(p=>p.label===label);if(i<0)return {ok:false,reason:'not found'};if(a[i].locked)return {ok:false,reason:'default — locked'};a.splice(i,1);return {ok:true};}
+}
+
+let notifPerm=(typeof Notification!=='undefined')?Notification.permission:'denied';
+function tryNotify(title,body){if(typeof Notification==='undefined')return;if(notifPerm==='granted'){try{new Notification(title,{body});}catch(e){}}else if(notifPerm==='default'){Notification.requestPermission().then(p=>notifPerm=p);}}
+function toaster(host){const wrap=el('div','toasts');host.appendChild(wrap);return (msg,kind)=>{const t=el('div','toast'+(kind?' '+kind:''),msg);wrap.appendChild(t);setTimeout(()=>{t.style.transition='opacity .3s';t.style.opacity='0';setTimeout(()=>t.remove(),300);},2600);};}
+function flash(...inputs){for(const i of inputs){if(!i)continue;i.classList.remove('flash');void i.offsetWidth;i.classList.add('flash');}}
+function dotsHTML(p){if(!p)return '';let h='<span class="dots">';const pos=(p.cycle-1)%p.iv;for(let i=0;i<p.iv;i++){const isLong=(i===p.iv-1);let cls='';if(i<pos)cls='on';if(i===pos)cls='now';if(isLong)cls+=' long';h+=`<i class="${cls.trim()}"></i>`;}return h+'</span>';}
+function badgesHTML(bs){return (bs||[]).map(b=>`<span class="badge ${b.c}">${b.t}</span>`).join(' ');}
+
+function mount(host, engine, bar, tip){
+ const toast=toaster(host);
+ const head=el('div','phead',`<span class="brand">Timer</span><span class="pcount">queue <b class="cnt">0</b>/${MAX}</span>`);
+ const clear=el('button','key sm',GL.clear+' CLEAR ALL');clear.style.marginLeft='8px';
+ clear.addEventListener('click',()=>{if(!engine.count())return;engine.cancelAll();toast('cleared all');render();});
+ head.appendChild(clear);
+ // close button — flat circular ✕ like the net/bt/audio panels (Close/Esc); the
+ // waybar module reopens it, mirroring the real on-click: timer-panel toggle.
+ const closeBtn=el('button','x-btn','✕');closeBtn.title='Close (Esc)';
+ const barcap=document.querySelector('.barcap');
+ function setClosed(c){host.classList.toggle('closed',c);if(barcap)barcap.textContent=c?'the waybar module · click it to reopen the panel':'the waybar module · live';}
+ closeBtn.addEventListener('click',()=>setClosed(true));
+ head.appendChild(closeBtn);
+ bar.addEventListener('click',()=>setClosed(!host.classList.contains('closed')));
+ document.addEventListener('keydown',e=>{if(e.key==='Escape')setClosed(true);});
+ const hero=el('div','hero');
+ const create=buildCreate();
+ const list=el('div','qlist');
+ host.append(head,hero,create,list);
+ const flashing=new Set();
+
+ function itemClick(e){const b=e.target.closest('[data-act]');if(!b)return;const id=b.dataset.id,a=b.dataset.act;
+ if(a==='toggle')engine.toggle(id);
+ else if(a==='promote'){engine.promote(id);toast('to bar slot');}
+ else if(a==='cycle')engine.cycle(b.dataset.dir);
+ else if(a==='lap'){engine.lap(id);toast('lap recorded');}
+ else if(a==='stop'){engine.cancel(id);toast('stopped');}
+ else if(a==='snooze'){engine.snooze(id);toast('snoozed','gold');}
+ else if(a==='dismiss'){engine.dismiss(id);toast('dismissed');}
+ else if(a==='cancel'){if(b.dataset.armed){engine.cancel(id);toast('cancelled');}
+ else{b.dataset.armed='1';b.classList.add('armed');b.textContent='sure?';setTimeout(()=>{if(b.isConnected){b.textContent='×';b.classList.remove('armed');delete b.dataset.armed;}},2000);return;}}
+ render();}
+ hero.addEventListener('click',itemClick);
+ list.addEventListener('click',itemClick);
+
+ function buildCreate(){
+ const box=el('div','create');
+ const seg=el('div','seg');
+ TYPES.forEach(t=>{const b=el('button',t==='timer'?'on':'',t[0].toUpperCase()+t.slice(1));b.dataset.t=t;seg.appendChild(b);});
+ const body=el('div');box.append(seg,body);
+ let selType='timer';
+ seg.addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;selType=b.dataset.t;[...seg.children].forEach(x=>x.classList.toggle('on',x===b));paintBody();});
+ function doAdd(type,val,lab,opts){const r=engine.add(type,val?val.value:'',lab.value.trim(),opts);
+ if(!r.ok){if(val)val.classList.add('bad');toast(r.reason,'red');return;}
+ if(val){val.classList.remove('bad');if(!val.disabled)val.value='';}lab.value='';toast('added '+type,'gold');render();}
+ function paintBody(){
+ body.innerHTML='';
+ if(selType==='timer'){
+ const ps=el('div','presets');
+ engine.presetsFor('timer').forEach(p=>{const c=el('span','preset'+(p.locked?'':''),p.label+(p.locked?'':` <span class="x" data-del="${encodeURIComponent(p.label)}">×</span>`));c.dataset.val=p.value;ps.appendChild(c);});
+ ps.appendChild(Object.assign(el('span','preset','+ preset'),{}) );ps.lastChild.dataset.newp='1';ps.lastChild.style.opacity='.7';
+ const row=el('div','row');
+ const val=el('input','tin');val.placeholder='5m · 1h30m · 90s';val.style.flex='2';
+ const lab=el('input','tin');lab.placeholder='label (optional)';lab.style.flex='2';
+ const rep=el('span','switch');const reptag=el('span',null,'<span style="color:var(--steel);font-size:.56rem;letter-spacing:.12em;text-transform:uppercase">repeat</span>');
+ const addk=el('button','key on',GL.add+' ADD');
+ const repwrap=el('span',null,'');repwrap.style.display='inline-flex';repwrap.style.alignItems='center';repwrap.style.gap='6px';repwrap.append(rep,reptag);
+ row.append(val,lab,repwrap,addk);
+ body.append(ps,row);
+ rep.addEventListener('click',()=>rep.classList.toggle('on'));
+ ps.addEventListener('click',e=>{const del=e.target.closest('[data-del]');
+ if(del){const r=engine.deletePreset('timer',decodeURIComponent(del.dataset.del));toast(r.ok?'preset removed':('preset: '+r.reason),r.ok?'gold':'red');paintBody();return;}
+ if(e.target.closest('[data-newp]')){const lb=prompt('Preset label:');if(!lb)return;const vv=prompt('Value for “'+lb+'”:',lb)||lb;const r=engine.addPreset('timer',lb,vv);toast(r.ok?'preset added':('preset: '+r.reason),r.ok?'gold':'red');paintBody();return;}
+ const c=e.target.closest('.preset');if(!c||c.dataset.val==null)return;val.value=c.dataset.val;flash(val);});
+ addk.addEventListener('click',()=>doAdd('timer',val,lab,{repeat:rep.classList.contains('on')}));
+ [val,lab].forEach(x=>x.addEventListener('keydown',e=>{if(e.key==='Enter')doAdd('timer',val,lab,{repeat:rep.classList.contains('on')});}));
+ }
+ else if(selType==='alarm'){
+ const ps=el('div','presets');
+ engine.presetsFor('alarm').forEach(p=>{const c=el('span','preset',p.label+(p.locked?'':` <span class="x" data-del="${encodeURIComponent(p.label)}">×</span>`));c.dataset.val=p.value;ps.appendChild(c);});
+ const row=el('div','row');
+ const val=el('input','tin');val.placeholder='HH:MM · +30m · @hour · @half';val.style.flex='2';
+ const lab=el('input','tin');lab.placeholder='label (optional)';lab.style.flex='2';
+ const addk=el('button','key on',GL.add+' ADD');
+ row.append(val,lab,addk);
+ const drow=el('div','row');const days=el('div','days7');const sel=new Set();
+ DAYNAMES.forEach((d,i)=>{const b=el('button',null,d);b.dataset.d=i;b.title=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][i];days.appendChild(b);});
+ const quick=el('span',null,'');const wkd=el('button','key sm','weekdays');const evd=el('button','key sm','daily');
+ quick.style.display='inline-flex';quick.style.gap='5px';quick.append(wkd,evd);
+ const srow=el('div','row');const slbl=el('span',null,'<span style="color:var(--steel);font-size:.56rem;letter-spacing:.12em;text-transform:uppercase">snooze</span>');
+ const sn=el('input','numin');sn.value='9';sn.inputMode='numeric';const smin=el('span',null,'<span style="color:var(--dim);font-size:10px">min</span>');
+ srow.append(slbl,sn,smin);
+ drow.append(days,quick);
+ body.append(ps,row,drow,srow);
+ function paintDays(){[...days.children].forEach(b=>b.classList.toggle('on',sel.has(+b.dataset.d)));}
+ days.addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;const d=+b.dataset.d;sel.has(d)?sel.delete(d):sel.add(d);paintDays();});
+ wkd.addEventListener('click',()=>{sel.clear();[1,2,3,4,5].forEach(d=>sel.add(d));paintDays();});
+ evd.addEventListener('click',()=>{sel.clear();[0,1,2,3,4,5,6].forEach(d=>sel.add(d));paintDays();});
+ function opts(){return {days:[...sel],snooze:parseInt(sn.value,10)||9};}
+ ps.addEventListener('click',e=>{const del=e.target.closest('[data-del]');
+ if(del){const r=engine.deletePreset('alarm',decodeURIComponent(del.dataset.del));toast(r.ok?'preset removed':('preset: '+r.reason),r.ok?'gold':'red');paintBody();return;}
+ const c=e.target.closest('.preset');if(!c||c.dataset.val==null)return;val.value=c.dataset.val;flash(val);});
+ addk.addEventListener('click',()=>doAdd('alarm',val,lab,opts()));
+ [val,lab].forEach(x=>x.addEventListener('keydown',e=>{if(e.key==='Enter')doAdd('alarm',val,lab,opts());}));
+ }
+ else if(selType==='stopwatch'){
+ const row=el('div','row');
+ const lab=el('input','tin');lab.placeholder='label (optional)';lab.style.flex='2';
+ const addk=el('button','key on',GL.add+' ADD');
+ row.append(lab,addk);
+ const note=el('div','engrave','counts up from zero · lap while running');
+ body.append(row,note);
+ addk.addEventListener('click',()=>doAdd('stopwatch',null,lab));
+ lab.addEventListener('keydown',e=>{if(e.key==='Enter')doAdd('stopwatch',null,lab);});
+ }
+ else {
+ const ps=el('div','presets');
+ engine.pomoPresets.forEach(p=>{const c=el('span','preset',p.label);c.dataset.pp=p.label;ps.appendChild(c);});
+ const cfg=el('div','cfg');const mk=v=>{const i=el('input','numin');i.value=v;i.inputMode='numeric';return i;};
+ const ws=mk(POMO_DEFAULT.ws),wl=mk(POMO_DEFAULT.wl),rs=mk(POMO_DEFAULT.rs),rl=mk(POMO_DEFAULT.rl),iv=mk(POMO_DEFAULT.iv);
+ const auto=el('span','switch on');auto.dataset.on='1';
+ const rW=el('div','crow');rW.append(el('span','lbl','Work'),el('span','sl','S'),ws,el('span','sl','L'),wl,el('span','u','min'));
+ const rR=el('div','crow');rR.append(el('span','lbl','Rest'),el('span','sl','S'),rs,el('span','sl','L'),rl,el('span','u','min'));
+ const rI=el('div','crow');rI.append(el('span','lbl','Long ev.'),iv,el('span','u','cycles → long work + long break'));
+ const rA=el('div','crow');rA.append(el('span','lbl','Auto'),auto,el('span','u','advance into the next phase'));
+ cfg.append(rW,rR,rI,rA);
+ const row=el('div','row');const lab=el('input','tin');lab.placeholder='label (optional)';lab.style.flex='2';
+ const addk=el('button','key on',GL.add+' ADD CYCLE');row.append(lab,addk);
+ body.append(ps,cfg,row);
+ auto.addEventListener('click',()=>{auto.classList.toggle('on');auto.dataset.on=auto.classList.contains('on')?'1':'';});
+ ps.addEventListener('click',e=>{const c=e.target.closest('[data-pp]');if(!c)return;const p=engine.pomoPresets.find(x=>x.label===c.dataset.pp);if(!p)return;
+ ws.value=p.ws;wl.value=p.wl;rs.value=p.rs;rl.value=p.rl;iv.value=p.iv;[...ps.children].forEach(x=>x.classList.toggle('on',x===c));flash(ws,wl,rs,rl,iv);});
+ function pnum(inp,d){const n=parseInt(inp.value,10);return (isNaN(n)||n<1)?d:n;}
+ addk.addEventListener('click',()=>{const r=engine.add('pomodoro','',lab.value.trim(),{ws:pnum(ws,25),wl:pnum(wl,50),rs:pnum(rs,5),rl:pnum(rl,15),iv:pnum(iv,4),auto:!!auto.dataset.on});
+ if(!r.ok){toast(r.reason,'red');return;}lab.value='';toast('pomodoro added','gold');render();});
+ lab.addEventListener('keydown',e=>{if(e.key==='Enter')addk.click();});
+ }
+ }
+ paintBody();
+ return box;
+ }
+
+ function renderBar(){
+ const p=engine.barPayload();
+ bar.className='wmod '+p.cls;bar.title=p.tip.join('\n');
+ bar.innerHTML=`<span class="wg">${p.glyph}</span>`+(p.text?`<span class="wt">${p.text}</span>`:'')+(p.plus>0?`<span class="wp">+${p.plus}</span>`:'');
+ tip.innerHTML=`<div class="th">hover tooltip</div>`+p.tip.map((l,i)=>`<div class="tl" style="${i===0?'color:var(--steel)':''}">${l}</div>`).join('');
+ }
+
+ function render(){
+ head.querySelector('.cnt').textContent=engine.count();
+ const rows=engine.rows(),primId=engine.effectivePrimary();
+ const h=rows.find(r=>r.id===primId);
+ hero.className='hero'+(h&&h.paused?' paused':'')+(h&&h.ringing?' ringing':'')+(h&&flashing.has(h.id)?' fire':'');
+ if(!h){hero.innerHTML='<div class="empty" style="width:100%">No timers running — add one below.</div>';}
+ else{
+ const ringP=h.prog!=null?Math.round(h.prog*100):0;
+ const donutHTML = h.type==='stopwatch'
+ ? `<span class="swdial" title="seconds sweep"><span class="swtick"></span><span class="swtick q"></span><span class="swtick h"></span><span class="swtick t"></span><span class="swhand" style="transform:rotate(${Math.round((h.sweep||0)*360)}deg)"></span><span class="swhub"></span></span>`
+ : `<span class="ring${h.warn?' warn':''}" style="--p:${ringP};width:86px;height:86px"><b style="color:var(--cream);font-size:14px">${ringP}<small style="font-size:9px;color:var(--dim)">%</small></b></span>`;
+ let transport;
+ if(h.ringing) transport=`<button class="key" data-act="snooze" data-id="${h.id}">${GL.play} SNOOZE ${engine.find(h.id).snooze}m</button><button class="key red" data-act="dismiss" data-id="${h.id}">DISMISS</button>`;
+ else if(h.type==='stopwatch') transport=`<button class="key icon" data-act="cycle" data-dir="prev" title="prev">‹</button><button class="key" data-act="toggle" data-id="${h.id}">${h.paused?GL.play+' RESUME':GL.paused+' PAUSE'}</button><button class="key" data-act="lap" data-id="${h.id}">LAP</button><button class="key red" data-act="stop" data-id="${h.id}">STOP</button><button class="key icon" data-act="cycle" data-dir="next" title="next">›</button>`;
+ else {const start=h.pomo&&h.pomo.awaiting?(GL.play+' START '+(h.pomo.phase==='work'?'WORK':'BREAK')):(h.paused?GL.play+' RESUME':GL.paused+' PAUSE');
+ transport=`<button class="key icon" data-act="cycle" data-dir="prev" title="prev">‹</button><button class="key" data-act="toggle" data-id="${h.id}">${start}</button><button class="key red icon" data-act="cancel" data-id="${h.id}" title="cancel">${GL.cancel}</button><button class="key icon" data-act="cycle" data-dir="next" title="next">›</button>`;}
+ hero.innerHTML=
+ `<div class="htop">
+ <div class="rhs">
+ <div class="htype"><span class="g">${h.glyph}</span><span class="badge ${h.paused?'dim':''}">${h.typeLabel}</span>${badgesHTML(h.badges)}${h.pomo?dotsHTML(h.pomo):''}</div>
+ <div class="hlabel">${h.label}</div>
+ <div class="hbig">${h.disp}${h.type==='stopwatch'&&h.lastLap!=null?`<span class="lapbadge">LAP ${fmtTime(h.lastLap)}</span>`:''}</div>
+ <div class="hsub">${h.sub}</div>
+ </div>
+ <div class="donut">${donutHTML}</div>
+ </div>
+ <div class="transport">${transport}</div>`;
+ }
+ list.innerHTML='';
+ const rest=rows.filter(r=>r.id!==primId);
+ list.appendChild(el('div','engrave','queue <span class="cnt">· '+rest.length+'</span>'));
+ if(!rest.length)list.appendChild(el('div','empty',h?'Only one item is queued. Add more above.':''));
+ rest.forEach(r=>{
+ const row=el('div','qrow'+(r.paused?' paused':'')+(r.ringing?' ringing':'')+(flashing.has(r.id)?' fire':''));
+ let ctrls;
+ if(r.ringing)ctrls=`<button class="key sm" data-act="snooze" data-id="${r.id}">SNOOZE</button><button class="key sm red" data-act="dismiss" data-id="${r.id}">OFF</button>`;
+ else if(r.type==='stopwatch')ctrls=`<button class="key sm" data-act="lap" data-id="${r.id}">LAP</button><button class="key sm red" data-act="stop" data-id="${r.id}">STOP</button><button class="key icon" data-act="promote" data-id="${r.id}" title="to bar slot">${GL.promote}</button>`;
+ else ctrls=`<button class="key icon" data-act="toggle" data-id="${r.id}" title="pause/resume">${r.paused?GL.play:GL.paused}</button><button class="key icon" data-act="promote" data-id="${r.id}" title="to bar slot">${GL.promote}</button><button class="arm" data-act="cancel" data-id="${r.id}" title="cancel">×</button>`;
+ row.innerHTML=`<span class="lamp ${r.ringing?'red':(r.paused?'off':(r.warn?'red':''))}"></span><span class="g">${r.glyph}</span><span class="meta"><b>${r.label}</b><span class="ty">${r.sub}</span></span><span class="rd">${r.disp}</span><span class="ctrls">${ctrls}</span>`;
+ list.appendChild(row);
+ });
+ renderBar();
+ }
+ engine._render=render;engine._flash=id=>{flashing.add(id);setTimeout(()=>flashing.delete(id),1800);};engine._toast=toast;
+ render();
+}
+
+const engine=new Engine();
+const pomo=engine.add('pomodoro','','Deep work',{ws:25,wl:50,rs:5,rl:15,iv:4,auto:true});
+engine.add('timer','45s','Egg',{repeat:false});
+engine.add('timer','5m','Tea',{repeat:true});
+const sw=engine.add('stopwatch','','Debug run');
+{const s=engine.find(sw.id);s.start=engine.now()-215;s.laps=[{t:72},{t:158}];} // ~3:35 elapsed, last lap 2:38
+engine.add('alarm','07:00','Wake',{days:[1,2,3,4,5],snooze:9});
+engine.promote(sw.id); // show the new stopwatch sweep-dial + lap badge in the hero
+mount(document.getElementById('panel'), engine, document.getElementById('wbar'), document.getElementById('wtip'));
+
+function loop(){const fired=engine.tick?engine.tick():tickEngine(engine);
+ for(const f of fired){engine._flash(f.id);engine._toast((f.kind==='done'?GL.bell+' ':'')+f.title,f.kind==='done'?'red':'gold');tryNotify(f.title,f.body);}
+ engine._render();}
+/* engine.tick lives on the class below via prototype patch to keep add()/tick together readable */
+Engine.prototype.tick=function(){const now=this.now(),fired=[];
+ for(const it of this.items.slice()){
+ if(!COUNTDOWN.includes(it.type)||this.isPaused(it)||it.ringing)continue;
+ if(it.target-now>0)continue;
+ if(it.type==='pomodoro'){const c=it.cfg;
+ if(it.phase==='work'){const deep=(it.cycle%c.iv===0);fired.push({id:it.id,kind:'pomo',title:`Pomodoro · ${deep?'long':'short'} break`,body:it.label||`cycle ${it.cycle}`});
+ it.phase='rest';const len=deep?c.rl:c.rs;it.total=len;if(c.auto)it.target=now+len;else{it.paused_left=len;it.awaiting=true;}}
+ else{it.cycle+=1;const deep=(it.cycle%c.iv===0);fired.push({id:it.id,kind:'pomo',title:'Pomodoro · back to work',body:it.label||`cycle ${it.cycle}`});
+ it.phase='work';const len=deep?c.wl:c.ws;it.total=len;if(c.auto)it.target=now+len;else{it.paused_left=len;it.awaiting=true;}}}
+ else if(it.type==='alarm'){fired.push({id:it.id,kind:'done',title:'Alarm · '+(it.label||fmtClock(it.target)),body:'alarm ringing'});it.ringing=true;}
+ else{if(it.repeat){fired.push({id:it.id,kind:'done',title:'Timer · '+(it.label||'done')+' · repeating',body:'restarted'});it.target=now+it.total;}
+ else{fired.push({id:it.id,kind:'done',title:'Timer · '+(it.label||'done'),body:'time’s up'});this.cancel(it.id);}}
+ }
+ return fired;};
+setInterval(loop, reduced?1000:250);
+</script>
+</body>
+</html>