fix: project picker clipping, create-from-picker, button visibility, listener leak

- Picker dropdown: append to document.body with fixed positioning instead
  of inside the session-item (which has overflow:hidden). Flips above
  when near bottom of viewport.
- Add "+ New project" item at bottom of picker so users can create a
  project and assign in one flow.
- Folder button stays visible (blue, 60% opacity) when session belongs
  to a project, instead of only appearing on hover.
- Clean up document click listener in all picker item onclick handlers
  to prevent stale listener accumulation.

Tests: 214 passed, 23 pre-existing failures, 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nathan Esquenazi
2026-04-02 01:10:18 -07:00
parent 537337a158
commit 2f281cbbd7
2 changed files with 47 additions and 7 deletions

View File

@@ -287,7 +287,7 @@ function renderSessionListFromCache(){
trash.onclick=async(e)=>{e.stopPropagation();e.preventDefault();await deleteSession(s.session_id);}; trash.onclick=async(e)=>{e.stopPropagation();e.preventDefault();await deleteSession(s.session_id);};
// Project move button (folder icon) // Project move button (folder icon)
const move=document.createElement('button'); const move=document.createElement('button');
move.className='session-action-btn';move.innerHTML='&#128194;';move.title='Move to project'; move.className='session-action-btn'+(s.project_id?' has-project':'');move.innerHTML='&#128194;';move.title='Move to project';
move.onclick=async(e)=>{e.stopPropagation();e.preventDefault();_showProjectPicker(s,move);}; move.onclick=async(e)=>{e.stopPropagation();e.preventDefault();_showProjectPicker(s,move);};
// Project dot indicator // Project dot indicator
if(s.project_id){ if(s.project_id){
@@ -360,11 +360,14 @@ function _showProjectPicker(session, anchorEl){
document.querySelectorAll('.project-picker').forEach(p=>p.remove()); document.querySelectorAll('.project-picker').forEach(p=>p.remove());
const picker=document.createElement('div'); const picker=document.createElement('div');
picker.className='project-picker'; picker.className='project-picker';
// Close on outside click
const close=(e)=>{if(!picker.contains(e.target)&&e.target!==anchorEl){picker.remove();document.removeEventListener('click',close);}};
// "No project" option // "No project" option
const none=document.createElement('div'); const none=document.createElement('div');
none.className='project-picker-item'+(!session.project_id?' active':''); none.className='project-picker-item'+(!session.project_id?' active':'');
none.textContent='No project'; none.textContent='No project';
none.onclick=async()=>{ none.onclick=async()=>{
document.removeEventListener('click',close);
picker.remove(); picker.remove();
await api('/api/session/move',{method:'POST',body:JSON.stringify({session_id:session.session_id,project_id:null})}); await api('/api/session/move',{method:'POST',body:JSON.stringify({session_id:session.session_id,project_id:null})});
session.project_id=null; session.project_id=null;
@@ -386,6 +389,7 @@ function _showProjectPicker(session, anchorEl){
name.textContent=p.name; name.textContent=p.name;
item.appendChild(name); item.appendChild(name);
item.onclick=async()=>{ item.onclick=async()=>{
document.removeEventListener('click',close);
picker.remove(); picker.remove();
await api('/api/session/move',{method:'POST',body:JSON.stringify({session_id:session.session_id,project_id:p.project_id})}); await api('/api/session/move',{method:'POST',body:JSON.stringify({session_id:session.session_id,project_id:p.project_id})});
session.project_id=p.project_id; session.project_id=p.project_id;
@@ -394,11 +398,43 @@ function _showProjectPicker(session, anchorEl){
}; };
picker.appendChild(item); picker.appendChild(item);
} }
// Position relative to anchor // "+ New project" item
anchorEl.style.position='relative'; const createItem=document.createElement('div');
anchorEl.appendChild(picker); createItem.className='project-picker-item project-picker-create';
// Close on outside click createItem.textContent='+ New project';
const close=(e)=>{if(!picker.contains(e.target)&&e.target!==anchorEl){picker.remove();document.removeEventListener('click',close);}}; createItem.onclick=async()=>{
picker.remove();
document.removeEventListener('click',close);
const name=prompt('Project name:');
if(!name||!name.trim()) return;
const color=PROJECT_COLORS[_allProjects.length%PROJECT_COLORS.length];
const res=await api('/api/projects/create',{method:'POST',body:JSON.stringify({name:name.trim(),color})});
if(res.project){
_allProjects.push(res.project);
await api('/api/session/move',{method:'POST',body:JSON.stringify({session_id:session.session_id,project_id:res.project.project_id})});
session.project_id=res.project.project_id;
await renderSessionList();
showToast('Created "'+res.project.name+'" and moved session');
}
};
picker.appendChild(createItem);
// Position picker on document.body to avoid overflow:hidden clipping
document.body.appendChild(picker);
const rect=anchorEl.getBoundingClientRect();
picker.style.position='fixed';
picker.style.zIndex='999';
const spaceBelow=window.innerHeight-rect.bottom;
if(spaceBelow<160&&rect.top>160){
picker.style.bottom=(window.innerHeight-rect.top+4)+'px';
picker.style.top='auto';
}else{
picker.style.top=(rect.bottom+4)+'px';
picker.style.bottom='auto';
}
const pickerW=160;
let left=rect.right-pickerW;
if(left<8) left=8;
picker.style.left=left+'px';
setTimeout(()=>document.addEventListener('click',close),0); setTimeout(()=>document.addEventListener('click',close),0);
} }

View File

@@ -504,6 +504,8 @@ body.resizing{user-select:none;cursor:col-resize;}
.session-dup,.session-action-btn{background:none;border:none;color:var(--muted);font-size:11px;cursor:pointer;opacity:0;transition:opacity .15s;padding:2px 4px;flex-shrink:0;} .session-dup,.session-action-btn{background:none;border:none;color:var(--muted);font-size:11px;cursor:pointer;opacity:0;transition:opacity .15s;padding:2px 4px;flex-shrink:0;}
.session-item:hover .session-dup,.session-item:hover .session-action-btn{opacity:1;} .session-item:hover .session-dup,.session-item:hover .session-action-btn{opacity:1;}
.session-dup:hover,.session-action-btn:hover{color:var(--text);} .session-dup:hover,.session-action-btn:hover{color:var(--text);}
.session-action-btn.has-project{opacity:.6;color:var(--blue);}
.session-item:hover .session-action-btn.has-project{opacity:1;}
/* ── Cron alert badge ── */ /* ── Cron alert badge ── */
.cron-badge{position:absolute;top:2px;right:2px;background:#e53e3e;color:#fff;font-size:9px;font-weight:700;min-width:14px;height:14px;line-height:14px;text-align:center;border-radius:7px;padding:0 3px;} .cron-badge{position:absolute;top:2px;right:2px;background:#e53e3e;color:#fff;font-size:9px;font-weight:700;min-width:14px;height:14px;line-height:14px;text-align:center;border-radius:7px;padding:0 3px;}
@@ -538,10 +540,12 @@ body.resizing{user-select:none;cursor:col-resize;}
.project-create-btn{font-size:10px;padding:3px 6px;border-radius:12px;cursor:pointer;border:1px dashed var(--border2);background:none;color:var(--muted);opacity:.6;transition:all .15s;} .project-create-btn{font-size:10px;padding:3px 6px;border-radius:12px;cursor:pointer;border:1px dashed var(--border2);background:none;color:var(--muted);opacity:.6;transition:all .15s;}
.project-create-btn:hover{opacity:1;border-color:var(--blue);color:var(--blue);} .project-create-btn:hover{opacity:1;border-color:var(--blue);color:var(--blue);}
.project-create-input{font-size:10px;padding:3px 8px;border-radius:12px;border:1px solid rgba(124,185,255,.6);background:rgba(20,32,60,.9);color:var(--text);outline:none;width:100px;font-family:inherit;box-shadow:0 0 0 2px rgba(124,185,255,.15);} .project-create-input{font-size:10px;padding:3px 8px;border-radius:12px;border:1px solid rgba(124,185,255,.6);background:rgba(20,32,60,.9);color:var(--text);outline:none;width:100px;font-family:inherit;box-shadow:0 0 0 2px rgba(124,185,255,.15);}
.project-picker{position:absolute;right:0;top:100%;background:var(--sidebar);border:1px solid var(--border2);border-radius:8px;padding:4px;z-index:30;min-width:140px;box-shadow:0 4px 16px rgba(0,0,0,.3);} .project-picker{position:absolute;right:0;top:100%;background:var(--sidebar);border:1px solid var(--border2);border-radius:8px;padding:4px;z-index:30;min-width:140px;max-width:220px;width:max-content;box-shadow:0 4px 16px rgba(0,0,0,.3);}
.project-picker-item{padding:5px 10px;font-size:11px;border-radius:6px;cursor:pointer;color:var(--muted);transition:all .1s;display:flex;align-items:center;gap:6px;} .project-picker-item{padding:5px 10px;font-size:11px;border-radius:6px;cursor:pointer;color:var(--muted);transition:all .1s;display:flex;align-items:center;gap:6px;}
.project-picker-item:hover{background:rgba(255,255,255,.08);color:var(--text);} .project-picker-item:hover{background:rgba(255,255,255,.08);color:var(--text);}
.project-picker-item.active{color:var(--blue);} .project-picker-item.active{color:var(--blue);}
.project-picker-create{color:var(--blue);opacity:.7;border-top:1px solid var(--border2);margin-top:2px;padding-top:6px;}
.project-picker-create:hover{opacity:1;background:rgba(124,185,255,.08);}
.session-project-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;display:inline-block;margin-left:4px;vertical-align:middle;} .session-project-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;display:inline-block;margin-left:4px;vertical-align:middle;}
/* ── Code copy button ── */ /* ── Code copy button ── */