diff --git a/frontend/css/mobile.css b/frontend/css/mobile.css index c28a9f5..828f819 100644 --- a/frontend/css/mobile.css +++ b/frontend/css/mobile.css @@ -1,22 +1,30 @@ /* Responsive overrides — small viewports up to 700px, narrow phones up to 400px. */ @media(max-width:700px){ - /* Sidebar becomes a slide-in drawer */ + /* Sidebar becomes a slide-in drawer. Use a comfortable fixed width rather than + --sw: on a phone-width viewport the desktop resize logic clamps --sw down to + its 200px minimum, which collapses the session-name column to a few pixels. */ #sb{ position:fixed;top:0;left:0; height:100vh;height:100dvh; z-index:100; - width:var(--sw)!important;min-width:var(--sw)!important; + width:min(86vw,330px)!important;min-width:0!important; transform:translateX(-100%); transition:transform .22s ease; overflow:hidden; } #sb.mobile-open{transform:translateX(0)!important} - #sb.collapsed{transform:translateX(-100%)!important;width:var(--sw)!important;min-width:var(--sw)!important} + #sb.collapsed{transform:translateX(-100%)!important;width:min(86vw,330px)!important;min-width:0!important} .sb-expand{display:none!important} /* No edge-resizing in the mobile drawer. */ .sb-resizer{display:none!important} .mob-menu-btn{display:flex} + /* Touch has no hover, so the per-row actions (favorite / tracker / rename / + delete) would otherwise stay invisible yet still reserve width, squeezing + the session title. Reveal them and let the title truncate cleanly. */ + .si-act{opacity:1} + .si-meta{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + /* Main fills the full screen — dvh keeps it within the actual visible area on iOS */ #main{ width:100vw;min-width:0; diff --git a/frontend/css/sidebar.css b/frontend/css/sidebar.css index 320250b..e8fb97e 100644 --- a/frontend/css/sidebar.css +++ b/frontend/css/sidebar.css @@ -10,8 +10,10 @@ .sb-resizer:hover::after,body.sb-resizing .sb-resizer::after{background:var(--ab)} #sb.collapsed .sb-resizer{display:none} body.sb-resizing{cursor:col-resize;user-select:none} -.sb-expand{position:absolute;left:8px;top:62px;z-index:30;width:34px;height:34px;background:var(--bg3);border:1px solid var(--border2);border-radius:9px;color:var(--text2);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s} -.sb-expand:hover{background:var(--bg4);color:var(--text)} +/* Lives in the toolbar; hidden until the sidebar is collapsed (toggleSidebar + flips display to flex). Sits where the mobile menu button does. */ +.sb-expand{display:none;width:34px;height:34px;background:none;border:1px solid var(--border);border-radius:8px;color:var(--text2);cursor:pointer;align-items:center;justify-content:center;flex-shrink:0;transition:all .15s} +.sb-expand:hover{background:var(--bg3);color:var(--text)} .sb-collapse{margin-left:auto;background:none;border:none;color:var(--text3);cursor:pointer;padding:4px 6px;border-radius:6px;display:flex;align-items:center;justify-content:center} .sb-collapse:hover{color:var(--text);background:var(--bg3)} .sb-head{padding:18px 14px 12px;border-bottom:1px solid var(--border)} diff --git a/frontend/index.html b/frontend/index.html index b7ec74a..f7837fb 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -121,9 +121,6 @@ -
@@ -152,6 +149,11 @@ + +
StackResume
Generating…
openai / gpt-4o
diff --git a/frontend/js/diff.js b/frontend/js/diff.js index 2eac5a1..11c75b7 100644 --- a/frontend/js/diff.js +++ b/frontend/js/diff.js @@ -113,29 +113,112 @@ function _diffWords(a,b){ return parts.map(p=>p.t==='='?esc(p.v):p.t==='+'?`${esc(p.v)}`:`${esc(p.v)}`).join(''); } +// ── Section serializers ─────────────────────────────────────────────────────── +// Turn each resume section into diff-friendly plain text: one logical entry per +// block, with a blank line between blocks so the line-diff aligns whole entries. +// A section/entry present only in the new version reads as all-added (green); +// one dropped from the new version reads as all-removed (red). Field ordering +// mirrors the preview so the diff reads naturally. ss()/toArr() come from +// preview.js and coerce the LLM's loose field shapes. +const _j=(arr,sep)=>arr.filter(x=>x!=null&&x!=='').join(sep); + +function _seEdu(list){return toArr(list).map(e=>{ + const deg=ss(e.degree)+(ss(e.field_of_study)?` in ${ss(e.field_of_study)}`:''); + const L=[_j([deg,ss(e.institution),ss(e.location),_j([ss(e.start_date),ss(e.end_date)],' – ')],' · ')]; + if(ss(e.gpa))L.push('GPA: '+ss(e.gpa)); + if(ss(e.honors))L.push(ss(e.honors)); + const cw=toArr(e.relevant_coursework).map(ss).filter(Boolean);if(cw.length)L.push('Coursework: '+cw.join(', ')); + const ac=toArr(e.activities).map(ss).filter(Boolean);if(ac.length)L.push('Activities: '+ac.join(', ')); + return L.join('\n'); +}).join('\n\n');} + +function _seProjects(list){return toArr(list).map(p=>{ + const L=[_j([ss(p.name),ss(p.role),ss(p.type),_j([ss(p.start_date),ss(p.end_date)],' – ')],' · ')]; + if(ss(p.description))L.push(ss(p.description)); + const tech=toArr(p.technologies).map(ss).filter(Boolean);if(tech.length)L.push('Tech: '+tech.join(', ')); + toArr(p.highlights).map(ss).filter(Boolean).forEach(h=>L.push('• '+h)); + const url=ss(p.url)||ss(p.github);if(url)L.push(url); + return L.join('\n'); +}).join('\n\n');} + +function _seOSS(list){return toArr(list).map(o=>{ + const L=[_j([ss(o.project),ss(o.role),ss(o.language),ss(o.stars)?'★ '+ss(o.stars):''],' · ')]; + if(ss(o.description))L.push(ss(o.description)); + toArr(o.contributions).map(ss).filter(Boolean).forEach(c=>L.push('• '+c)); + if(ss(o.url))L.push(ss(o.url)); + return L.join('\n'); +}).join('\n\n');} + +function _seCerts(list){return toArr(list).map(c=>{ + const d=[];if(ss(c.date))d.push('issued '+ss(c.date));if(ss(c.expiry))d.push('expires '+ss(c.expiry)); + return _j([ss(c.name),ss(c.issuer),d.join(', '),ss(c.credential_id)?'ID '+ss(c.credential_id):'',ss(c.url)],' · '); +}).join('\n');} + +function _sePubs(list){return toArr(list).map(p=> + _j([ss(p.title),ss(p.type),ss(p.venue),ss(p.date),toArr(p.authors).map(ss).filter(Boolean).join(', '),ss(p.url)],' · ') +).join('\n');} + +function _sePatents(list){return toArr(list).map(p=>{ + const L=[_j([ss(p.title),ss(p.patent_number),ss(p.date)],' · ')]; + if(ss(p.description))L.push(ss(p.description)); + return L.join('\n'); +}).join('\n\n');} + +function _seVol(list){return toArr(list).map(v=>{ + const L=[_j([ss(v.role),ss(v.organization),_j([ss(v.start_date),ss(v.end_date)],' – ')],' · ')]; + if(ss(v.description))L.push(ss(v.description)); + return L.join('\n'); +}).join('\n\n');} + +function _seLangs(list){return toArr(list).map(l=> + typeof l==='string'?l:_j([ss(l.language),ss(l.proficiency)?`(${ss(l.proficiency)})`:''],' ') +).join('\n');} + +function _seContact(r){const pi=r.personal_info||{}; + return _j([ss(pi.full_name),ss(pi.professional_title),ss(pi.email),ss(pi.phone),ss(pi.location),ss(pi.linkedin),ss(pi.github),ss(pi.website)||ss(pi.portfolio)],'\n'); +} + function _buildDiffHtml(a,b){ + a=a||{};b=b||{}; const secs=[]; - const check=(title,va,vb)=>{if(va!==vb)secs.push({title,html:_diffText(va||'',vb||'')});}; + const check=(title,va,vb)=>{if((va||'')!==(vb||''))secs.push({title,html:_diffText(va||'',vb||'')});}; + check('Contact',_seContact(a),_seContact(b)); check('Professional Summary',a.professional_summary,b.professional_summary); - const ccA=(a.core_competencies||[]).join(', '); - const ccB=(b.core_competencies||[]).join(', '); + const ccA=toArr(a.core_competencies).map(ss).filter(Boolean).join(', '); + const ccB=toArr(b.core_competencies).map(ss).filter(Boolean).join(', '); check('Core Competencies',ccA,ccB); - const expB=b.experience||[]; - const expA=a.experience||[]; - expB.forEach((job,idx)=>{ - const pj=expA[idx]; - const txtA=pj?[...(pj.responsibilities||[]),...(pj.achievements||[])].join('\n'):''; - const txtB=[...(job.responsibilities||[]),...(job.achievements||[])].join('\n'); - if(txtA!==txtB)secs.push({title:`Experience — ${job.title} @ ${job.company}`,html:_diffText(txtA,txtB)}); - }); - - const skA=Object.values(a.technical_skills||{}).flat().join(', '); - const skB=Object.values(b.technical_skills||{}).flat().join(', '); + // Experience — per job, so a small wording tweak in one bullet doesn't light up + // the whole section. Walking the longer of the two lists also surfaces jobs + // added to (or removed from) the new version, not just edited ones. + const expA=a.experience||[],expB=b.experience||[]; + for(let i=0;imetaA[k]!==metaB[k])){