usehour 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/hour.mjs +138 -47
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -3,14 +3,14 @@
3
3
  Your year of building with AI, as Wrapped-style cards.
4
4
 
5
5
  ```
6
- npx usehour@0.2.0
6
+ npx usehour@0.3.0
7
7
  ```
8
8
 
9
9
  - Asks before it reads anything, and lists the folders it will read.
10
10
  - Reads your Claude Code and Codex session logs on your machine, read only.
11
- - Counts your habits, and picks a few of your best prompts (anything that looks like a key, password, email or home folder hidden) so you can choose some for your profile.
11
+ - Counts your habits, and finds a few of your best prompts (anything that looks like a key, password, email or home folder already hidden). You can pick some for your profile and black out words, but not add or change any.
12
12
  - Opens your cards in your browser. Save any card as an image to post it.
13
- - Optional: make a private profile on usehour.com from the cards page. You see exactly what is sent first. The script itself never connects to the internet.
13
+ - Optional: claim usehour.com/@you from the cards page. You see exactly what is sent first. The script itself never connects to the internet.
14
14
  - No dependencies, nothing installed.
15
15
 
16
16
  It's one file: read [hour.mjs](https://usehour.com/hour.mjs) before you run it.
package/hour.mjs CHANGED
@@ -30,7 +30,7 @@ import { spawn } from "node:child_process";
30
30
  import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
31
31
  import { fileURLToPath } from "node:url";
32
32
 
33
- const VERSION = "0.2.0";
33
+ const VERSION = "0.3.0";
34
34
  const API_DEFAULT = "https://usehour.com/api";
35
35
 
36
36
  /* ------------------------------------------------------------------ patterns */
@@ -389,6 +389,7 @@ function cards(m, name) {
389
389
  a button: "Continue" checks that your handle is free (sends the handle
390
390
  only), "Create" sends the profile you saw in step 3. */
391
391
  const CLAIM_CSS = String.raw`
392
+
392
393
  .claimc .cdots{margin-top:4cqw;height:14cqw;background-image:radial-gradient(circle,rgba(244,241,234,.95) 0 .95cqw,transparent 1.05cqw);background-size:3.6cqw 3.6cqw;-webkit-mask-image:linear-gradient(115deg,transparent 8%,#000 55%,transparent 96%);mask-image:linear-gradient(115deg,transparent 8%,#000 55%,transparent 96%)}
393
394
  .claimc .pill{margin-top:4cqw;align-self:flex-start;padding:2cqw 3.6cqw;border-radius:999px;background:rgba(244,241,234,.1);box-shadow:inset 0 0 0 .3cqw rgba(244,241,234,.22);font:4.2cqw var(--mono)}.claimc .pill span{opacity:.6}.claimc .pill b{font-weight:500}
394
395
  .claimc .cta{position:relative;z-index:5;margin-top:5cqw;height:13cqw;border-radius:999px;border:0;background:#F4F1EA;color:#0B0B0A;font:600 4.6cqw var(--sans);cursor:pointer}
@@ -442,11 +443,20 @@ const CLAIM_CSS = String.raw`
442
443
  .cl-done p{position:relative;margin:14px 0 0;font-size:14px;line-height:1.45;opacity:.8}
443
444
  .cl-row{flex:none;display:flex;gap:10px;margin-top:16px}.cl-row a,.cl-row button{flex:1;display:flex;align-items:center;justify-content:center;height:52px;border-radius:999px;font:600 16px var(--sans);text-decoration:none;cursor:pointer;border:0}
444
445
  .cl-row a{background:var(--ink);color:var(--paper)}.cl-row button{background:#fff;color:var(--ink);box-shadow:inset 0 0 0 1px rgba(11,11,10,.14)}
446
+ .cl-g2{margin-top:22px}.cl-g2>*+*{margin-top:14px}
447
+ .cl-foot{flex:none;display:flex;flex-direction:column-reverse;gap:8px;margin-top:14px}.cl-foot .cl-fine{margin:0;text-align:center}
448
+ .cl-tip{margin-top:10px;font-size:12.5px;font-weight:600;opacity:.8}
449
+ .cl-toks{margin-top:8px;font-size:15.5px;line-height:1.6;max-height:300px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;background:rgba(255,255,255,.95);color:var(--ink);border-radius:12px;padding:10px 12px;cursor:auto}
450
+ .tok{font:inherit;color:inherit;background:none;border:0;padding:0 1px;margin:0;border-radius:4px;cursor:pointer}.tok:hover{background:rgba(11,11,10,.1)}.tok.on{background:var(--ink);color:var(--ink)}
451
+ .cl-pc.ed{cursor:default}.cl-narrow{width:100%;max-width:560px;margin:0 auto}
452
+ @media(min-width:900px){.cl-sheet{max-width:1040px;height:min(820px,calc(100svh - 48px));padding:26px 40px 30px}.cl-h{font-size:44px}
453
+ .cl-g2{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:28px;align-items:start}.cl-g2>*+*{margin-top:0}.cl-g2>.cl-hcard{margin-top:0}
454
+ .cl-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-content:start;align-items:start}
455
+ .cl-foot{flex-direction:row;justify-content:space-between;align-items:center;gap:20px}.cl-foot .cl-fine{text-align:left}.cl-foot .cl-go{width:auto;min-width:300px;padding:0 34px}.cl-foot .cl-skip{width:auto}}
445
456
  `;
446
- const CLAIM_HTML = `<div class="claimbar" id="claimbar"><div><b>Claim your handle</b><span>Turn these cards into your profile at usehour.com/@you.</span></div><button type="button" id="claimb">Claim your handle</button></div>
447
- <div class="cl" id="cl" hidden role="dialog" aria-modal="true" aria-label="Claim your handle"><div class="cl-sheet" id="cls"></div></div>`;
457
+ const CLAIM_HTML = `<div class="cl" id="cl" hidden role="dialog" aria-modal="true" aria-label="Claim your handle"><div class="cl-sheet" id="cls"></div></div>`;
448
458
  const CLAIM_JS = String.raw`
449
- var CS={step:1,handle:D.suggest||"",name:"",email:"",picked:[],texts:(D.examples||[]).map(function(e){return e.text}),editing:-1,pub:false,roles:false,feature:false,busy:false,err:"",data:false,done:null};
459
+ var CS={hide:(D.examples||[]).map(function(){return new Set()}),step:1,handle:D.suggest||"",name:"",email:"",picked:[],texts:(D.examples||[]).map(function(e){return e.text}),editing:-1,pub:false,roles:false,feature:false,busy:false,err:"",data:false,done:null};
450
460
  var HANDLE_RE=/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,23}$/,EMAIL_RE=/^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/;
451
461
  var PCOL=[["#DBA004","#0B0B0A"],["#8B5FD9","#FFFFFF"],["#2B5FE3","#FFFFFF"]];
452
462
  function hidHTML(t){return esc(t).replace(/\[hidden\]/g,'<span class="hid">hidden</span>')}
@@ -455,34 +465,41 @@ var h=CS.handle||"you";d.innerHTML='<div class="in"><div class="ask">One more th
455
465
  d.querySelector(".cta").onclick=function(e){e.stopPropagation();openClaim()};return d}
456
466
  function openClaim(){document.getElementById("cl").hidden=false;document.body.style.overflow="hidden";renderClaim()}
457
467
  function closeClaim(){document.getElementById("cl").hidden=true;document.body.style.overflow=""}
458
- function payload(){return{v:2,handle:CS.handle,name:CS.name.trim(),email:CS.email.trim(),linkedin:"",public:CS.pub,roles:CS.roles,feature:CS.feature,prompts:CS.picked.map(function(i){return{text:CS.texts[i].trim(),date:D.examples[i].date}}).filter(function(p){return p.text}),wrapped:D.m}}
468
+ function payload(){return{v:2,handle:CS.handle,name:CS.name.trim(),email:CS.email.trim(),linkedin:"",public:CS.pub,roles:CS.roles,feature:CS.feature,prompts:CS.picked.map(function(i){return{text:promptOut(i),date:D.examples[i].date,hidden:CS.hide[i].size}}).filter(function(p){return p.text&&p.text!=="[hidden]"}),wrapped:D.m}}
459
469
  function prog(n){return'<div class="cl-top"><button type="button" class="cl-back" data-a="back">'+(n===1?"Close":"Back")+'</button><span>'+n+' of 3</span></div><div class="cl-prog">'+[1,2,3].map(function(i){return'<i'+(i<=n?' class="on"':'')+'></i>'}).join("")+'</div>'}
460
470
  function handleNote(){var h=CS.handle;if(!h)return"Letters, numbers and dashes.";if(!HANDLE_RE.test(h))return"3 to 24 letters, numbers or single dashes.";return"Press Continue to check it's free."}
471
+ function toks(i){return CS.texts[i].split(/(\s+)/)}
472
+ function isAuto(w){return w.indexOf("[hidden]")>=0}
473
+ function promptOut(i){var h=CS.hide[i];return toks(i).map(function(w,k){return h.has(k)?"[hidden]":w}).join("").replace(/\[hidden\](\s+\[hidden\])+/g,"[hidden]").trim()}
474
+ function hidCount(i){return (CS.texts[i].match(/\[hidden\]/g)||[]).length+CS.hide[i].size}
475
+ function viewHTML(i){var h=CS.hide[i];return toks(i).map(function(w,k){if(/^\s+$/.test(w))return w;if(h.has(k))return'<span class="hid">'+esc(w)+'</span>';return hidHTML(w)}).join("")}
476
+ function editHTML(i){var h=CS.hide[i];return toks(i).map(function(w,k){if(/^\s+$/.test(w))return w;if(isAuto(w))return hidHTML(w);return'<button type="button" class="tok'+(h.has(k)?" on":"")+'" data-a="tok" data-i="'+i+'" data-k="'+k+'">'+esc(w)+'</button>'}).join("")}
461
477
  function renderClaim(){var s=document.getElementById("cls"),H="",n=CS.step;
462
478
  if(n===1){H=prog(1)+'<h2 class="cl-h">Pick your handle.</h2><p class="cl-p">It’s the address of your profile. Once it’s yours, nobody else can have it.</p>'
463
- +'<div class="cl-hcard"><label for="clh">usehour.com/</label><div class="cl-hrow"><span class="grad">@</span><input id="clh" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="24" value="'+esc(CS.handle)+'"></div><div class="cl-hst" id="clhst">'+esc(handleNote())+'</div></div>'
464
- +'<label class="cl-f">Your name<input id="cln" autocomplete="name" maxlength="80" value="'+esc(CS.name)+'"></label>'
479
+ +'<div class="cl-g2"><div class="cl-hcard"><label for="clh">usehour.com/</label><div class="cl-hrow"><span class="grad">@</span><input id="clh" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="24" value="'+esc(CS.handle)+'"></div><div class="cl-hst" id="clhst">'+esc(handleNote())+'</div></div>'
480
+ +'<div><label class="cl-f" style="margin-top:0">Your name<input id="cln" autocomplete="name" maxlength="80" value="'+esc(CS.name)+'"></label>'
465
481
  +'<label class="cl-f">Email, to keep your handle<input id="cle" type="email" autocomplete="email" maxlength="200" value="'+esc(CS.email)+'"></label>'
466
- +'<p class="cl-fine">We email you a link to your profile. That’s how you get back in. No password.</p><div class="cl-err">'+esc(CS.err)+'</div><div class="cl-sp"></div>'
467
- +'<button type="button" class="cl-go" data-a="next1"'+(CS.busy?" disabled":"")+'>'+(CS.busy?"Checking…":"Continue")+'</button><p class="cl-fine c">Continue checks the name with usehour.com. Nothing else is sent.</p>'}
468
- else if(n===2){var k=CS.picked.length;H=prog(2)+'<h2 class="cl-h">Show how you brief an agent.</h2><p class="cl-p">Your strongest prompts, found on your laptop. Pick up to 3. Edit out anything private first.</p>'
469
- +'<div class="cl-lw"><div class="cl-list" id="cll">'+D.examples.map(function(e,i){var at=CS.picked.indexOf(i),on=at>=0,c=on?PCOL[at%3]:null,ed=CS.editing===i,w=CS.texts[i].split(/\s+/).filter(Boolean).length,hidN=(CS.texts[i].match(/\[hidden\]/g)||[]).length;
470
- return'<div class="cl-pc'+(on?" on":"")+'" data-a="pick" data-i="'+i+'"'+(c?' style="background:'+c[0]+';color:'+c[1]+'"':'')+'><div class="cl-pch"><span>'+esc(e.date?new Date(e.date+"T12:00:00Z").toLocaleString("en",{month:"short",day:"numeric",timeZone:"UTC"})+" · ":"")+w+' words'+(hidN?" · "+hidN+" hidden":"")+'</span><span class="cl-pick">'+(on?"✓ Picked":"Pick")+'</span></div>'
471
- +(ed?'<textarea data-i="'+i+'" aria-label="Edit this prompt">'+esc(CS.texts[i])+'</textarea>':'<p>“'+hidHTML(CS.texts[i])+'”</p>')
472
- +'<button type="button" class="cl-edit" data-a="edit" data-i="'+i+'">'+(ed?"Done editing":"Edit")+'</button></div>'}).join("")+'</div><div class="cl-more" id="clm" hidden><span>'+(D.examples.length>3?(D.examples.length-3)+" more prompts":"More below")+'</span></div></div>'
473
- +'<div class="cl-err">'+esc(CS.err)+'</div><button type="button" class="cl-go" style="margin-top:14px" data-a="next2">'+(k?"Continue with "+k+" prompt"+(k>1?"s":""):"Continue without prompts")+'</button>'
474
- +(k?'<button type="button" class="cl-skip" data-a="skip2">Skip, don’t share any prompts</button>':'')}
482
+ +'<p class="cl-fine">We email you a link to your profile. That’s how you get back in. No password.</p></div></div>'
483
+ +'<div class="cl-err">'+esc(CS.err)+'</div><div class="cl-sp"></div>'
484
+ +'<div class="cl-foot"><p class="cl-fine">Continue checks the name with usehour.com. Nothing else is sent.</p><button type="button" class="cl-go" data-a="next1"'+(CS.busy?" disabled":"")+'>'+(CS.busy?"Checking…":"Continue")+'</button></div>'}
485
+ else if(n===2){var k=CS.picked.length;H=prog(2)+'<h2 class="cl-h">Show how you brief an agent.</h2><p class="cl-p">Your strongest prompts, found in your logs. Pick up to 3. You can black out words, but not add or change any.</p>'
486
+ +'<div class="cl-lw"><div class="cl-list" id="cll">'+D.examples.map(function(e,i){var at=CS.picked.indexOf(i),on=at>=0,c=on?PCOL[at%3]:null,ed=CS.editing===i,w=CS.texts[i].split(/\s+/).filter(Boolean).length,hn=hidCount(i);
487
+ return'<div class="cl-pc'+(on?" on":"")+(ed?" ed":"")+'" data-a="pick" data-i="'+i+'"'+(c?' style="background:'+c[0]+';color:'+c[1]+'"':'')+'><div class="cl-pch"><span>'+esc(e.date?new Date(e.date+"T12:00:00Z").toLocaleString("en",{month:"short",day:"numeric",timeZone:"UTC"})+" · ":"")+w+' words'+(hn?" · "+hn+" hidden":"")+'</span><span class="cl-pick">'+(on?"✓ Picked":"Pick")+'</span></div>'
488
+ +(ed?'<div class="cl-tip">Tap words to black them out. Tap again to bring them back.</div><div class="cl-toks">'+editHTML(i)+'</div>':'<p>“'+viewHTML(i)+'”</p>')
489
+ +'<button type="button" class="cl-edit" data-a="edit" data-i="'+i+'">'+(ed?"Done":"Black out words")+'</button></div>'}).join("")+'</div><div class="cl-more" id="clm" hidden><span>'+(D.examples.length>4?"More prompts below":"More below")+'</span></div></div>'
490
+ +'<div class="cl-err">'+esc(CS.err)+'</div><div class="cl-foot" style="margin-top:14px">'+(k?'<button type="button" class="cl-skip" data-a="skip2">Skip, don’t share any prompts</button>':'<span></span>')+'<button type="button" class="cl-go" data-a="next2">'+(k?"Continue with "+k+" prompt"+(k>1?"s":""):"Continue without prompts")+'</button></div>'}
475
491
  else if(n===3){var p=payload(),hh=esc(CS.handle);H=prog(3)+'<h2 class="cl-h">Make it work for you.</h2><p class="cl-p">All optional. Change any of it later from your profile.</p>'
476
- +'<label class="cl-opt dk"><span class="cl-oh"><span>Make <span class="grad">@'+hh+'</span> public</span><input type="checkbox" data-o="pub"'+(CS.pub?" checked":"")+'></span><span class="cl-ot">A link for your LinkedIn, GitHub and email signature. It shows how you work, which a CV can’t.</span><span class="cl-ot s">Off: your handle stays reserved, and only people with your private link can see the profile.</span></label>'
492
+ +'<div class="cl-g2"><div>'
493
+ +'<label class="cl-opt dk" style="margin-top:0"><span class="cl-oh"><span>Make <span class="grad">@'+hh+'</span> public</span><input type="checkbox" data-o="pub"'+(CS.pub?" checked":"")+'></span><span class="cl-ot">A link for your LinkedIn, GitHub and email signature. It shows how you work, which a CV can’t.</span><span class="cl-ot s">Off: your handle stays reserved, and only people with your private link can see the profile.</span></label>'
477
494
  +'<label class="cl-opt"><span class="cl-oh"><span>Let companies come to you</span><input type="checkbox" data-o="roles"'+(CS.roles?" checked":"")+'></span><ul class="cl-ul" style="--dot:#2B5FE3"><li>Teams hiring AI builders see your profile and ask for an intro. No applying, no cover letters.</li><li>You say yes or no to each one. Your email stays hidden until you say yes.</li></ul></label>'
478
495
  +'<label class="cl-opt"><span class="cl-oh"><span>Get featured by Hour</span><input type="checkbox" data-o="feature"'+(CS.feature?" checked":"")+'></span><ul class="cl-ul" style="--dot:#F2601F"><li>We post standout cards in our “builders to watch” series, tagged to you.</li><li>Your work in front of founders and hiring teams who follow Hour.</li></ul></label>'
479
- +'<h3 class="cl-h3">What goes to Hour</h3><div class="cl-two"><div><b><i style="background:#2B5FE3"></i>Sent</b><span>Your card numbers</span><span>'+(p.prompts.length?p.prompts.length+" prompt"+(p.prompts.length>1?"s":"")+" you picked":"No prompts")+'</span><span>Handle, name, email</span></div><div><b><i style="background:#F2601F"></i>Stays with you</b><span>Your logs and code</span><span>Every other prompt</span><span>File and project names</span></div></div>'
480
- +'<button type="button" class="cl-link" data-a="data">'+(CS.data?"Hide the exact data":"See the exact data")+'</button>'+(CS.data?'<pre>'+esc(JSON.stringify(p,null,2))+'</pre>':'')
481
- +'<div class="cl-err">'+esc(CS.err)+'</div><div class="cl-sp"></div><button type="button" class="cl-go" data-a="create"'+(CS.busy?" disabled":"")+'>'+(CS.busy?"Creating…":"Create @"+hh)+'</button><p class="cl-fine c">Delete it any time, with one link.</p>'}
482
- else{var d=CS.done,u=d.publicUrl||d.url;H='<div class="cl-top"><span></span><button type="button" class="cl-back" data-a="close">Close</button></div><div class="cl-done"><h2>@'+esc(d.handle)+'<br>is yours.</h2><a class="u" href="'+esc(u)+'" target="_blank" rel="noopener">'+esc(u.replace("https://",""))+'</a><p>'+(d.publicUrl?"Your profile is public. Put the link anywhere you like.":"Your profile is private. Only people you send this link to can see it.")+'</p></div>'
483
- +'<div class="cl-row"><a href="'+esc(u)+'" target="_blank" rel="noopener">Open my profile</a><button type="button" data-a="copy">Copy link</button></div><p class="cl-fine c">We emailed you this link, plus a private link to change your settings or delete your profile.</p>'}
496
+ +'</div><div><h3 class="cl-h3" style="margin-top:0">What goes to Hour</h3><div class="cl-two"><div><b><i style="background:#2B5FE3"></i>Sent</b><span>Your card numbers</span><span>'+(p.prompts.length?p.prompts.length+" prompt"+(p.prompts.length>1?"s":"")+" you picked":"No prompts")+'</span><span>Handle, name, email</span></div><div><b><i style="background:#F2601F"></i>Stays with you</b><span>Your logs and code</span><span>Every other prompt</span><span>File and project names</span></div></div>'
497
+ +'<button type="button" class="cl-link" data-a="data">'+(CS.data?"Hide the exact data":"See the exact data")+'</button>'+(CS.data?'<pre>'+esc(JSON.stringify(p,null,2))+'</pre>':'')+'</div></div>'
498
+ +'<div class="cl-err">'+esc(CS.err)+'</div><div class="cl-sp"></div><div class="cl-foot"><p class="cl-fine">Delete it any time, with one link.</p><button type="button" class="cl-go" data-a="create"'+(CS.busy?" disabled":"")+'>'+(CS.busy?"Creating…":"Create @"+hh)+'</button></div>'}
499
+ else{var d=CS.done,u=d.publicUrl||d.url;H='<div class="cl-top"><span></span><button type="button" class="cl-back" data-a="close">Close</button></div><div class="cl-narrow"><div class="cl-done"><h2>@'+esc(d.handle)+'<br>is yours.</h2><a class="u" href="'+esc(u)+'" target="_blank" rel="noopener">'+esc(u.replace("https://",""))+'</a><p>'+(d.publicUrl?"Your profile is public. Put the link anywhere you like.":"Your profile is private. Only people you send this link to can see it.")+'</p></div>'
500
+ +'<div class="cl-row"><a href="'+esc(u)+'" target="_blank" rel="noopener">Open my profile</a><button type="button" data-a="copy">Copy link</button></div><p class="cl-fine c">We emailed you this link, plus a private link to change your settings or delete your profile.</p></div>'}
484
501
  s.innerHTML=H;s.scrollTop=0;
485
- if(n===2){var L=document.getElementById("cll"),M=document.getElementById("clm"),f=function(){M.hidden=L.scrollHeight-L.clientHeight-L.scrollTop<24};L.addEventListener("scroll",f);f();var ta=s.querySelector("textarea");if(ta)ta.focus()}}
502
+ if(n===2){var L=document.getElementById("cll"),M=document.getElementById("clm"),f=function(){M.hidden=L.scrollHeight-L.clientHeight-L.scrollTop<24};L.addEventListener("scroll",f);f()}}
486
503
  document.getElementById("cls").addEventListener("input",function(e){var t=e.target;
487
504
  if(t.id==="clh"){var v=t.value.toLowerCase().replace(/[^a-z0-9-]/g,"");if(v!==t.value)t.value=v;CS.handle=v;CS.err="";document.getElementById("clhst").textContent=handleNote()}
488
505
  else if(t.id==="cln")CS.name=t.value;else if(t.id==="cle")CS.email=t.value;
@@ -493,18 +510,20 @@ if(a==="back"){CS.err="";if(CS.step===1)closeClaim();else{CS.step=CS.step===3&&!
493
510
  else if(a==="close"){closeClaim()}
494
511
  else if(a==="next1"){if(CS.busy)return;if(!HANDLE_RE.test(CS.handle)){CS.err="Handles are 3 to 24 letters, numbers or single dashes.";return renderClaim()}if(!CS.name.trim()){CS.err="Add your name.";return renderClaim()}if(!EMAIL_RE.test(CS.email.trim())){CS.err="Add an email address, so the handle stays yours.";return renderClaim()}
495
512
  CS.busy=true;CS.err="";renderClaim();fetch(D.api+"/handles/"+encodeURIComponent(CS.handle)).then(function(r){return r.json()}).then(function(j){CS.busy=false;if(j.available){CS.step=D.examples.length?2:3}else CS.err="@"+CS.handle+(j.reason==="already taken"?" is taken. Try another.":": "+j.reason+".");renderClaim()}).catch(function(){CS.busy=false;CS.err="Couldn’t reach usehour.com. Check your connection and try again.";renderClaim()})}
496
- else if(a==="edit"){e.stopPropagation();CS.editing=CS.editing===i?-1:i;renderClaim()}
497
- else if(a==="pick"){var at=CS.picked.indexOf(i);CS.err="";if(at>=0)CS.picked.splice(at,1);else if(CS.picked.length>=3)CS.err="Pick up to 3. Untick one first.";else CS.picked.push(i);var y=document.getElementById("cll").scrollTop;renderClaim();document.getElementById("cll").scrollTop=y}
513
+ else if(a==="edit"){e.stopPropagation();CS.editing=CS.editing===i?-1:i;var y0=document.getElementById("cll").scrollTop;renderClaim();document.getElementById("cll").scrollTop=y0}
514
+ else if(a==="tok"){e.stopPropagation();var kk=+b.getAttribute("data-k"),hs=CS.hide[i];if(hs.has(kk))hs.delete(kk);else hs.add(kk);b.classList.toggle("on");var mt=b.closest(".cl-pc").querySelector(".cl-pch span");mt.textContent=mt.textContent.replace(/ \u00B7 \d+ hidden$/,"")+(hidCount(i)?" \u00B7 "+hidCount(i)+" hidden":"")}
515
+ else if(a==="pick"){if(CS.editing===i)return;var at=CS.picked.indexOf(i);CS.err="";if(at>=0)CS.picked.splice(at,1);else if(CS.picked.length>=3)CS.err="Pick up to 3. Untick one first.";else CS.picked.push(i);var y=document.getElementById("cll").scrollTop;renderClaim();document.getElementById("cll").scrollTop=y}
498
516
  else if(a==="next2"){CS.err="";CS.editing=-1;CS.step=3;renderClaim()}
499
517
  else if(a==="skip2"){CS.picked=[];CS.err="";CS.editing=-1;CS.step=3;renderClaim()}
500
518
  else if(a==="data"){CS.data=!CS.data;renderClaim()}
501
519
  else if(a==="copy"){var u=CS.done.publicUrl||CS.done.url;(navigator.clipboard?navigator.clipboard.writeText(u):Promise.reject()).then(function(){b.textContent="Copied"},function(){window.prompt("Copy this link",u)})}
502
520
  else if(a==="create"){if(CS.busy)return;CS.busy=true;CS.err="";renderClaim();
503
521
  fetch(D.api+"/profiles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload())}).then(function(r){return r.json().catch(function(){return{}}).then(function(j){return{s:r.status,j:j}})}).then(function(x){CS.busy=false;
504
- if(x.s===201&&x.j.url){CS.done=x.j;CS.step=4;document.querySelector(".top .note").textContent="Your profile is on usehour.com. Your logs and other prompts stayed on this computer.";document.getElementById("claimbar").hidden=true}
522
+ if(x.s===201&&x.j.url){CS.done=x.j;CS.step=4;document.getElementById("topnote").textContent="Your profile is on usehour.com. Your logs and other prompts stayed on this computer.";document.getElementById("claimbar").hidden=true}
505
523
  else if(x.s===409){CS.step=1;CS.err="Someone just took @"+CS.handle+". Pick another."}
506
524
  else CS.err="That didn’t go through ("+(x.j.error||"error "+x.s)+"). Nothing was saved. Try again in a minute.";renderClaim()}).catch(function(){CS.busy=false;CS.err="Couldn’t reach usehour.com. Nothing was saved. Try again.";renderClaim()})}});
507
525
  document.getElementById("claimb").onclick=openClaim;
526
+ (function(){var h=document.getElementById("clhandle"),b=document.getElementById("claimb");if(CS.handle){h.textContent=CS.handle;b.textContent="Claim @"+CS.handle}})();
508
527
  `;
509
528
 
510
529
  /* --------------------------------------------------------------- the page */
@@ -514,6 +533,10 @@ function pageHTML(m, C, o = {}) {
514
533
  /* The page may talk to exactly one place: Hour's profile endpoint, and only
515
534
  when you press the button. With --no-share it may talk to nothing. */
516
535
  const connect = api ? new URL(api).origin : "'none'";
536
+ const escH = (t) => String(t).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
537
+ const meta = { period: `${monthYear(m.window.first)} – ${monthYear(m.window.last)}`, sessions: m.sessions.total.toLocaleString("en"), prompts: m.prompts.total.toLocaleString("en") };
538
+ const LOCK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>';
539
+ const DL = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v11M7 10l5 5 5-5M5 20h14"/></svg>';
517
540
  const csp = `default-src 'none'; img-src data: blob:; font-src data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src ${connect}; form-action 'none'; base-uri 'none'`;
518
541
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
519
542
  <meta http-equiv="Content-Security-Policy" content="${csp}">
@@ -522,17 +545,50 @@ function pageHTML(m, C, o = {}) {
522
545
  @font-face{font-family:"Geist";src:url(data:font/woff2;base64,${FONT_GEIST}) format("woff2");font-weight:100 900}
523
546
  :root{--paper:#F7F3EC;--ink:#0B0B0A;--ink-3:#6C6A64;--hair:rgba(11,11,10,.1);--surface:#fff;--sans:"Geist",ui-sans-serif,system-ui,-apple-system,"Helvetica Neue",Arial,sans-serif;--mono:"Geist Mono",ui-monospace,"SF Mono",Menlo,monospace}
524
547
  *{box-sizing:border-box}body{background:var(--paper);color:var(--ink);font-family:var(--sans);margin:0;-webkit-font-smoothing:antialiased}
525
- .wrap{max-width:1180px;margin:0 auto;padding:24px clamp(16px,4vw,32px) 64px}
526
- .top{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:22px}
527
- .brand{display:flex;align-items:center;gap:9px;font-weight:600}.brand svg{width:22px;height:22px}
528
- .note{font-size:14px;color:var(--ink-3);max-width:62ch;line-height:1.5}
529
- .stage{max-width:440px;margin:0 auto 14px;position:relative}
530
- .bars{position:absolute;left:4%;right:4%;top:2.6%;display:flex;gap:4px;z-index:3}.bars i{flex:1;height:3px;border-radius:2px;background:rgba(255,255,255,.35)}.bars i.on{background:rgba(255,255,255,.95)}
531
- .hit{position:absolute;top:0;bottom:0;z-index:4;background:transparent;border:0;cursor:pointer}.hit.l{left:0;width:40%}.hit.r{right:0;width:60%}
532
- .nav{display:flex;justify-content:center;gap:10px;margin-bottom:36px}.nav button{font:500 14px var(--sans);border:1px solid var(--hair);background:var(--surface);border-radius:999px;padding:8px 16px;cursor:pointer}
533
- h2{font-size:24px;letter-spacing:-.03em;margin:0 0 14px}
534
- .grid{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:14px}@media(max-width:1000px){.grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(max-width:560px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
535
- .grid .card{cursor:pointer}.save{display:block;margin-top:6px;font-size:12px;color:var(--ink-3);text-align:center}
548
+ .wrap{max-width:1180px;margin:0 auto;padding:24px clamp(16px,4vw,48px) 64px}
549
+ .top{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
550
+ .brand{display:flex;align-items:center;gap:9px;font-weight:600;font-size:17px}.brand svg{width:22px;height:22px}
551
+ .note{display:flex;align-items:center;gap:8px;font-size:14px;color:var(--ink-3);line-height:1.5}.note svg{flex:none;width:15px;height:15px}
552
+ .btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;height:52px;padding:0 22px;border-radius:999px;border:0;background:var(--ink);color:var(--paper);font:600 16px var(--sans);cursor:pointer;text-decoration:none}
553
+ .btn.ghost{background:#fff;color:var(--ink);font-weight:500;box-shadow:inset 0 0 0 1px rgba(11,11,10,.14)}.btn svg{width:17px;height:17px;flex:none}.btn[disabled]{opacity:.6;cursor:wait}
554
+ .hero{margin-top:40px;display:grid;grid-template-columns:minmax(0,420px) minmax(0,1fr);gap:clamp(28px,5vw,56px);align-items:stretch}
555
+ .hero-r{display:flex;flex-direction:column}
556
+ .hero h1{margin:4px 0 0;font-size:clamp(48px,6.4vw,76px);line-height:.9;letter-spacing:-.06em;font-weight:650}
557
+ .hero p.sub{margin:16px 0 0;font-size:19px;line-height:1.4;color:#3A3935}
558
+ .acts{margin-top:22px;display:flex;gap:10px;flex-wrap:wrap}
559
+ .claimbox{margin-top:auto;border-radius:26px;background:#8B5FD9;color:#fff;padding:28px 28px 26px;position:relative;overflow:hidden}
560
+ .claimbox:before{content:"";position:absolute;right:-20px;top:-20px;width:300px;height:190px;background-image:radial-gradient(circle,rgba(255,255,255,.9) 0 3px,transparent 3.4px);background-size:14px 14px;-webkit-mask-image:radial-gradient(circle at 80% 20%,#000 10%,transparent 68%);mask-image:radial-gradient(circle at 80% 20%,#000 10%,transparent 68%)}
561
+ .claimbox>*{position:relative}.claimbox .k{font-size:15px;font-weight:500;opacity:.9}.claimbox .h{margin-top:8px;font-size:clamp(32px,3.6vw,44px);line-height:.95;letter-spacing:-.05em;font-weight:650}
562
+ .claimbox .pill{display:inline-flex;margin-top:14px;padding:9px 15px;border-radius:999px;background:rgba(11,11,10,.22);font:16px var(--mono)}.claimbox .pill span{opacity:.7}
563
+ .claimbox p{margin:12px 0 0;max-width:460px;font-size:15.5px;line-height:1.42}
564
+ .claimbox button{margin-top:18px;height:52px;padding:0 24px;border-radius:999px;border:0;background:#fff;color:var(--ink);font:600 16px var(--sans);cursor:pointer}
565
+ h2.sec{margin:64px 0 18px;font-size:30px;letter-spacing:-.04em;font-weight:650}
566
+ .grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}@media(max-width:1000px){.grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(max-width:640px){.grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}}
567
+ .grid .card{cursor:pointer;transition:transform .15s}.grid .card:hover{transform:translateY(-3px)}
568
+ .save{display:flex;align-items:center;justify-content:center;gap:7px;width:100%;height:40px;margin-top:6px;border:0;background:none;border-radius:999px;font:500 13px var(--sans);color:#3A3935;cursor:pointer}.save:hover{background:rgba(11,11,10,.05)}.save svg{width:14px;height:14px}
569
+ .tips{margin-top:36px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}.tips div{border-radius:18px;background:#fff;box-shadow:inset 0 0 0 1px rgba(11,11,10,.08);padding:20px 22px}.tips b{display:block;font-size:16px}.tips span{display:block;margin-top:4px;font-size:14px;line-height:1.45;color:#3A3935}
570
+ @media(max-width:760px){.hero{grid-template-columns:1fr}.hero-r{order:-1}.claimbox{margin-top:24px}.tips{grid-template-columns:1fr}h2.sec{margin-top:48px;font-size:24px}}
571
+ .intro{position:fixed;inset:0;z-index:40;background:var(--ink);color:#F4F1EA;display:flex;flex-direction:column;padding:26px clamp(20px,4vw,48px);overflow:auto}.intro[hidden],.story[hidden]{display:none}
572
+ .intro:before{content:"";position:absolute;inset:0;background-image:radial-gradient(circle,rgba(244,241,234,.85) 0 3px,transparent 3.4px);background-size:18px 18px;-webkit-mask-image:radial-gradient(ellipse at 78% 40%,#000 0%,transparent 55%);mask-image:radial-gradient(ellipse at 78% 40%,#000 0%,transparent 55%);pointer-events:none}
573
+ .intro>*{position:relative}.intro .top .note{color:#F4F1EA;opacity:.75}
574
+ .intro .mid{margin:auto 0;max-width:720px;padding:40px 0}.intro .meta{font-size:clamp(15px,1.6vw,18px);font-weight:500;opacity:.8}
575
+ .intro h1{margin:18px 0 0;font-size:clamp(56px,9vw,112px);line-height:.88;letter-spacing:-.065em;font-weight:650}
576
+ .intro p{margin:24px 0 0;font-size:clamp(18px,2vw,22px);line-height:1.35;opacity:.9;max-width:560px}
577
+ .intro .go{margin-top:34px;display:flex;gap:12px;flex-wrap:wrap;align-items:center}
578
+ .intro .play{display:inline-flex;align-items:center;gap:12px;height:62px;padding:0 30px 0 24px;border-radius:999px;border:0;background:#F4F1EA;color:var(--ink);font:600 19px var(--sans);cursor:pointer}.intro .play svg{width:20px;height:20px}
579
+ .intro .skip{height:62px;padding:0 20px;border:0;background:none;color:#F4F1EA;opacity:.8;font:500 16px var(--sans);cursor:pointer}
580
+ .intro .tip{font-size:13px;opacity:.55}
581
+ .story{position:fixed;inset:0;z-index:40;background:#161614;color:#F4F1EA;display:flex;flex-direction:column;align-items:center;padding:18px clamp(12px,3vw,32px) 20px}
582
+ .story .top{align-self:stretch}.story .cnt{display:flex;align-items:center;gap:14px;font-size:14px}.story .cnt span{opacity:.6}
583
+ .rb{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:50%;border:0;background:rgba(244,241,234,.1);color:#F4F1EA;cursor:pointer;flex:none}.rb svg{width:18px;height:18px}
584
+ .st-row{flex:1;min-height:0;width:100%;display:flex;align-items:center;justify-content:center;gap:clamp(8px,3vw,36px)}
585
+ .st-col{--w:min(480px,88vw,calc((100svh - 190px) * .8));width:var(--w);display:flex;flex-direction:column;gap:10px}
586
+ .st-bars{display:flex;gap:4px}.st-bars i{flex:1;height:3px;border-radius:2px;background:rgba(244,241,234,.25);overflow:hidden}.st-bars i b{display:block;height:100%;width:0;background:#F4F1EA}
587
+ .st-slot{position:relative}.st-slot .card{box-shadow:0 40px 100px rgba(0,0,0,.5);border-radius:28px/22px}
588
+ .st-hit{position:absolute;top:0;bottom:0;z-index:4;background:transparent;border:0;cursor:pointer}.st-hit.l{left:0;width:32%}.st-hit.r{right:0;width:68%}
589
+ .nb{width:56px;height:56px}.nb.fw{background:#F4F1EA;color:var(--ink)}.nb svg{width:22px;height:22px}
590
+ @media(max-width:640px){.nb{display:none}}
591
+ .st-acts{margin-top:14px;display:flex;gap:10px}.st-acts button{display:inline-flex;align-items:center;gap:8px;height:44px;padding:0 18px;border-radius:999px;border:0;background:rgba(244,241,234,.1);color:#F4F1EA;font:500 14px var(--sans);cursor:pointer}.st-acts svg{width:16px;height:16px}
536
592
  .card{container-type:inline-size;aspect-ratio:4/5;width:100%;border-radius:5.5%/4.4%;overflow:hidden;position:relative;background:var(--bg);color:var(--fg);isolation:isolate}
537
593
  .card canvas{position:absolute;inset:0;width:100%;height:100%}
538
594
  .card .in{position:absolute;inset:0;padding:9cqw 8cqw 7cqw;display:flex;flex-direction:column;z-index:1}
@@ -547,12 +603,30 @@ ${api ? CLAIM_CSS : ""}
547
603
  .grad{background:linear-gradient(100deg,#4F88FF 0%,#B0ACFE 38%,#FB65C2 72%,#FD2980 100%);-webkit-background-clip:text;background-clip:text;color:transparent}
548
604
  </style></head><body>
549
605
  <svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs><linearGradient id="ob" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#4F88FF"/><stop offset="38%" stop-color="#B0ACFE"/><stop offset="72%" stop-color="#FB65C2"/><stop offset="100%" stop-color="#FD2980"/></linearGradient><symbol id="mark" viewBox="0 0 64 64"><path fill="url(#ob)" d="${MARK_PATH}"/></symbol></defs></svg>
606
+ <div class="intro" id="intro">
607
+ <div class="top"><div class="brand"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Hour</div><div class="note">${LOCK}Made on this computer. Nothing has been sent.</div></div>
608
+ <div class="mid"><div class="meta">${escH(meta.period)} · ${meta.sessions} sessions · ${meta.prompts} prompts</div>
609
+ <h1>Your year with <span class="grad">AI</span> is ready.</h1>
610
+ <p>${C.length} cards about how you build: your type, your hours, how you brief the agent. About a minute.</p>
611
+ <div class="go"><button type="button" class="play" id="play"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 4.5v15l13-7.5z"/></svg>Play my year</button><button type="button" class="skip" id="skip">Skip to all cards</button></div></div>
612
+ <div class="tip">Tip: use the arrow keys, or tap the left and right of a card. Space pauses.</div>
613
+ </div>
614
+ <div class="story" id="story" hidden role="dialog" aria-modal="true" aria-label="Your year with AI">
615
+ <div class="top"><div class="brand"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Your year with AI</div><div class="cnt"><span id="stn"></span><button type="button" class="rb" id="stx" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div></div>
616
+ <div class="st-row"><button type="button" class="rb nb" id="stp" aria-label="Previous card"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 5l-7 7 7 7"/></svg></button>
617
+ <div class="st-col"><div class="st-bars" id="bars"></div><div class="st-slot"><div id="slot"></div><button type="button" class="st-hit l" id="hl" aria-label="Previous"></button><button type="button" class="st-hit r" id="hr" aria-label="Next"></button></div></div>
618
+ <button type="button" class="rb nb fw" id="stf" aria-label="Next card"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5l7 7-7 7"/></svg></button></div>
619
+ <div class="st-acts" id="sta"><button type="button" id="sts">${DL}Save this card</button><button type="button" id="stz"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg><span>Pause</span></button></div>
620
+ </div>
550
621
  <div class="wrap">
551
- <div class="top"><div class="brand"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Hour</div><div class="note">Made on your machine from ${m.sessions.total} sessions. Nothing has been sent anywhere.</div></div>
552
- <div class="stage" id="stage"><div class="bars" id="bars"></div><div id="slot"></div><button class="hit l" id="prev" aria-label="Previous"></button><button class="hit r" id="next" aria-label="Next"></button></div>
553
- <div class="nav"><button id="b1">Back</button><button id="b2">Next</button></div>
554
- <h2>All cards</h2><div class="grid" id="grid"></div>
555
- <p class="note" style="margin-top:24px">To post a card, click "Save image" under it. The numbers are in wrapped.json next to this file.</p>
622
+ <div class="top"><div class="brand"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Hour</div><div class="note" id="topnote">${LOCK}Made on this computer. Nothing has been sent.</div></div>
623
+ <section class="hero"><div id="sumcard"></div><div class="hero-r">
624
+ <h1>Your year with AI</h1><p class="sub">${C.length} cards from ${meta.sessions} sessions. Save the ones you like and post them.</p>
625
+ <div class="acts"><button type="button" class="btn" id="saveall">${DL}Save all cards</button><button type="button" class="btn ghost" id="replay"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 4.5v15l13-7.5z"/></svg>Play again</button></div>
626
+ ${api ? `<div class="claimbox" id="claimbar"><div class="k">Next: make it a profile</div><div class="h">Claim your handle.</div><div class="pill"><span>usehour.com/</span>@<b id="clhandle">you</b></div><p>A profile companies hiring AI builders actually want to read. First come, first served. Private until you say otherwise.</p><button type="button" id="claimb">Claim your handle</button></div>` : ""}
627
+ </div></section>
628
+ <h2 class="sec">All your cards</h2><div class="grid" id="grid"></div>
629
+ <div class="tips"><div><b>What to post</b><span>Your summary card or your builder type, with one line on what surprised you.</span></div><div><b>Where they go</b><span>Saved cards land in your Downloads, sized for LinkedIn and X.</span></div><div><b>Come back any time</b><span>This page lives in the hour-wrapped folder where you ran the command. The numbers are in wrapped.json.</span></div></div>
556
630
  ${api ? CLAIM_HTML : ""}
557
631
  </div>
558
632
  <script>
@@ -590,11 +664,28 @@ let bs=parseFloat(c.bs||"19")/100*W;g.font="650 "+bs+"px "+F;let Bl=wrapText(g,c
590
664
  g.fillStyle=c.fg;g.globalAlpha=.85;g.font="500 "+W*.044+"px "+F;wrapText(g,c.ask,(W-2*P)*.62).forEach((l,i)=>g.fillText(l,P,W*.16+i*W*.055));g.globalAlpha=1;
591
665
  if(c.rows){const d=W*.027,gap=W*.011,pw=W*.1,dx=W-P-pw-10*d-9*gap;c.rows.forEach((w,k)=>{const yy=W*.3+k*W*.1;g.fillStyle=c.fg;g.font="500 "+W*.043+"px "+F;g.fillText(w[0],P,yy);g.font="600 "+W*.043+"px "+F;g.textAlign="right";g.fillText(w[2],W-P,yy);g.textAlign="left";for(let q=0;q<10;q++){const on=q<Math.round(w[1]*10);g.globalAlpha=on?1:.14;g.fillStyle=on?"#8B5FD9":c.fg;g.beginPath();g.arc(dx+q*(d+gap)+d/2,yy-W*.015,d/2,0,7);g.fill()}g.globalAlpha=1})}}
592
666
  async function save(c){try{await document.fonts.ready}catch(e){}const cv=document.createElement("canvas");cv.width=1080;cv.height=1350;paint(cv.getContext("2d"),1080,1350,c);const a=document.createElement("a");a.download="hour-"+c.id+".png";a.href=cv.toDataURL("image/png");document.body.appendChild(a);a.click();a.remove()}
593
- let cur=0;const slot=document.getElementById("slot"),bars=document.getElementById("bars"),grid=document.getElementById("grid");
594
- const N=D.cards.length+(D.api?1:0);for(let k=0;k<N;k++)bars.appendChild(document.createElement("i"));
595
- function show(n){cur=(n+N)%N;slot.innerHTML="";const cl=cur>=D.cards.length;prev.style.display=next.style.display=cl?"none":"";slot.appendChild(cl?claimEl():el(D.cards[cur]));[...bars.children].forEach((b,k)=>b.classList.toggle("on",k<=cur))}
596
- D.cards.forEach((c,k)=>{const w=document.createElement("div");const e=el(c);e.onclick=()=>show(k);const s=document.createElement("a");s.href="#";s.className="save";s.textContent="Save image";s.onclick=ev=>{ev.preventDefault();save(c)};w.append(e,s);grid.appendChild(w)});
597
- prev.onclick=b1.onclick=()=>show(cur-1);next.onclick=b2.onclick=()=>show(cur+1);addEventListener("keydown",e=>{const o=document.getElementById("cl");if(o&&!o.hidden)return;if(e.key==="ArrowRight")show(cur+1);if(e.key==="ArrowLeft")show(cur-1)});show(0);
667
+ let cur=0,timer=null,t0=0,paused=false;const DUR=6000;
668
+ const $=id=>document.getElementById(id),slot=$("slot"),bars=$("bars"),grid=$("grid");
669
+ const N=D.cards.length+(D.api?1:0);for(let k=0;k<N;k++){const i=document.createElement("i");i.appendChild(document.createElement("b"));bars.appendChild(i)}
670
+ function fill(){[...bars.children].forEach((b,k)=>{b.firstChild.style.width=k<cur?"100%":k>cur?"0":b.firstChild.style.width})}
671
+ function tick(){if(paused||$("story").hidden)return;const p=Math.min(1,(performance.now()-t0)/DUR);bars.children[cur].firstChild.style.width=(p*100)+"%";if(p>=1){go(cur+1);return}timer=requestAnimationFrame(tick)}
672
+ function show(n){cancelAnimationFrame(timer);cur=Math.max(0,n);const cl=cur>=D.cards.length;slot.innerHTML="";slot.appendChild(cl?claimEl():el(D.cards[cur]));
673
+ $("hl").style.display=$("hr").style.display=cl?"none":"";$("sts").style.visibility=cl?"hidden":"";$("stz").style.visibility=cl?"hidden":"";
674
+ $("stn").textContent=(cur+1)+" of "+N;[...bars.children].forEach((b,k)=>{b.firstChild.style.width=k<cur?"100%":"0"});
675
+ if(cl){bars.children[cur].firstChild.style.width="100%";return}t0=performance.now();if(!paused)timer=requestAnimationFrame(tick)}
676
+ function go(n){if(n>=N){closeStory();return}show(n)}
677
+ function openStory(n){$("intro").hidden=true;$("story").hidden=false;document.body.style.overflow="hidden";paused=false;setPause();show(n||0)}
678
+ function closeStory(){cancelAnimationFrame(timer);$("story").hidden=true;$("intro").hidden=true;document.body.style.overflow=""}
679
+ function setPause(){$("stz").querySelector("span").textContent=paused?"Play":"Pause"}
680
+ function togglePause(){if(cur>=D.cards.length)return;paused=!paused;setPause();if(!paused){const w=parseFloat(bars.children[cur].firstChild.style.width)||0;t0=performance.now()-w/100*DUR;timer=requestAnimationFrame(tick)}else cancelAnimationFrame(timer)}
681
+ $("play").onclick=()=>openStory(0);$("skip").onclick=closeStory;$("replay").onclick=()=>openStory(0);$("stx").onclick=closeStory;
682
+ $("stp").onclick=$("hl").onclick=()=>show(cur-1);$("stf").onclick=$("hr").onclick=()=>go(cur+1);$("stz").onclick=togglePause;$("sts").onclick=()=>save(D.cards[cur]);
683
+ addEventListener("keydown",e=>{const o=$("cl");if(o&&!o.hidden)return;if($("story").hidden){if(!$("intro").hidden&&(e.key==="Enter"||e.key===" ")){e.preventDefault();openStory(0)}return}
684
+ if(e.key==="ArrowRight")go(cur+1);else if(e.key==="ArrowLeft")show(cur-1);else if(e.key===" "){e.preventDefault();togglePause()}else if(e.key==="Escape")closeStory()});
685
+ const SUM=D.cards.find(c=>c.summary);if(SUM){const e=el(SUM);e.style.cursor="pointer";e.onclick=()=>openStory(D.cards.indexOf(SUM));$("sumcard").appendChild(e)}
686
+ const DLI='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v11M7 10l5 5 5-5M5 20h14"/></svg>';
687
+ D.cards.forEach((c,k)=>{const w=document.createElement("div");const e=el(c);e.onclick=()=>openStory(k);const s=document.createElement("button");s.type="button";s.className="save";s.innerHTML=DLI+"Save";s.onclick=()=>save(c);w.append(e,s);grid.appendChild(w)});
688
+ $("saveall").onclick=async()=>{const b=$("saveall"),t=b.innerHTML;b.disabled=true;for(let i=0;i<D.cards.length;i++){b.textContent="Saving "+(i+1)+" of "+D.cards.length+"…";await save(D.cards[i]);await new Promise(r=>setTimeout(r,350))}b.innerHTML=t;b.disabled=false};
598
689
  ${api ? CLAIM_JS : ""}
599
690
  </script></body></html>`;
600
691
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usehour",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Your year of building with AI, as Wrapped-style cards, and an optional private profile. Reads your Claude Code and Codex logs locally.",
5
5
  "type": "module",
6
6
  "bin": { "usehour": "hour.mjs" },