From c12de9c6456c9a7f93eb7fdb40b7748e10c25ade Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 22 Jun 2026 15:48:37 -0700 Subject: [PATCH] =?UTF-8?q?web=20UI:=20command-palette=20search=20overlay?= =?UTF-8?q?=20(f=20/=20=F0=9F=94=8D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the old search modal with a keyboard-driven command palette: blurred backdrop, centered input, results grouped by channel with highlighted FTS snippets (char(2)/char(3) β†’ ), ↑↓ navigation, Enter to open, Esc to close, recent searches + quick actions (Downloads/Stats/Health/Shortcuts) in localStorage. Debounced queries to /api/search with a sequence guard so stale responses don't clobber newer ones. Upload date is looked up client-side from the loaded library (SearchHit has no date field). Opened by the πŸ” header button and the `f` hotkey. Verified: node --check passes, release builds clean, and a headless harness screenshot confirms grouped results, highlighted matches, and selection. Co-Authored-By: Claude Opus 4.8 --- src/web_ui/index.html | 126 ++++++++++++++++++++++++++++-------------- 1 file changed, 84 insertions(+), 42 deletions(-) diff --git a/src/web_ui/index.html b/src/web_ui/index.html index 4a2c151..bbbaa9b 100644 --- a/src/web_ui/index.html +++ b/src/web_ui/index.html @@ -493,6 +493,35 @@ .pl-menu button.on{color:var(--accent);font-weight:700} #v-cc.on{color:var(--accent);background:color-mix(in srgb,var(--accent) 22%,transparent)} + /* Command palette: overlay search ────────────────────────────────── */ + .cp-bg{position:fixed;inset:0;z-index:1000;background:color-mix(in srgb,var(--bg) 78%,transparent);backdrop-filter:blur(14px) brightness(.88);animation:cpFadeIn .2s ease;opacity:1;pointer-events:all;overflow:auto} + .cp-bg.cp-closing{animation:cpFadeOut .2s ease;opacity:0;pointer-events:none} + @keyframes cpFadeIn{from{opacity:0;backdrop-filter:blur(0px) brightness(1)}}@keyframes cpFadeOut{to{opacity:0;backdrop-filter:blur(0px) brightness(1)}} + .cp-container{position:relative;max-width:640px;margin:60px auto 0;z-index:1001;display:flex;flex-direction:column;gap:0;border-radius:var(--radius);border:1px solid var(--hair);overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.4),0 0 0 1px var(--ring);animation:cpDrop .35s cubic-bezier(.22,1,.36,1)} + @keyframes cpDrop{from{opacity:0;transform:translateY(-20px);filter:blur(8px)}} + .cp-input-wrap{background:linear-gradient(180deg,color-mix(in srgb,var(--panel) 95%,transparent),color-mix(in srgb,var(--card) 60%,transparent));border-bottom:1px solid var(--hair);padding:14px 18px;display:flex;align-items:center;gap:11px} + .cp-icon{font-size:18px;color:var(--accent)} + .cp-input{flex:1;background:none;border:none;color:var(--text);font-size:14px;outline:none;padding:0;font-family:inherit;letter-spacing:.01em} + .cp-input::placeholder{color:var(--muted)} + .cp-clear{background:transparent;border:none;color:var(--muted);cursor:pointer;font-size:14px;padding:0;opacity:.6;transition:opacity .15s,color .15s} + .cp-clear:hover{opacity:1;color:var(--text)} + .cp-results{flex:0 1 64vh;background:var(--bg);overflow-y:auto;max-height:64vh;display:flex;flex-direction:column} + .cp-empty{padding:40px 20px;text-align:center;color:var(--muted)} + .cp-empty .cp-empty-title{font-family:var(--font-display);font-size:1.3rem;font-weight:400;letter-spacing:.01em;margin-bottom:6px} + .cp-group{border-bottom:1px solid color-mix(in srgb,var(--border) 50%,transparent)} + .cp-group:last-child{border-bottom:none} + .cp-group-label{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--muted);font-weight:700;padding:9px 18px 5px;background:var(--bg)} + .cp-item{padding:10px 18px;cursor:pointer;border-bottom:1px solid color-mix(in srgb,var(--border) 30%,transparent);transition:background .12s,border-color .12s;display:flex;flex-direction:column;gap:4px} + .cp-item:hover,.cp-item.cp-selected{background:color-mix(in srgb,var(--accent) 12%,transparent);border-color:var(--accent)} + .cp-item.cp-selected{border-left:3px solid var(--accent);padding-left:15px} + .cp-item-title{font-size:13px;font-weight:600;color:var(--text);letter-spacing:.005em} + .cp-item-meta{font-size:11px;color:var(--muted);display:flex;gap:8px;flex-wrap:wrap} + .cp-item-snip{font-size:11.5px;color:var(--text);line-height:1.4;margin-top:2px} + .cp-item-snip mark{background:color-mix(in srgb,var(--accent) 28%,transparent);color:var(--accent);font-weight:600;border-radius:2px;padding:0 2px} + .cp-actions{padding:10px 18px;background:var(--bg);border-top:1px solid color-mix(in srgb,var(--border) 50%,transparent);display:flex;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--muted)} + .cp-action-btn{background:color-mix(in srgb,var(--card) 55%,transparent);border:1px solid var(--hair);color:var(--text);padding:5px 10px;border-radius:var(--radius-sm);cursor:pointer;font-size:11px;transition:background .12s,border-color .12s} + .cp-action-btn:hover{background:color-mix(in srgb,var(--accent) 15%,transparent);border-color:var(--accent);color:var(--accent)} + @media(prefers-reduced-motion:reduce){*{animation:none!important;transition-duration:.001ms!important}} @@ -2059,53 +2088,64 @@ async function copyChannelFeed(platform,handle,btn){ }catch(e){setStatus('Error: '+e.message)} } -/* ── Full-text library search ───────────────────────────────────── */ -let ftsSeq=0; +/* ── Full-text library search (command palette) ───────────────────────────────────────── */ +let cpSeq=0,cpSelected=-1,cpRecents=[]; function openSearch(){ - if(document.getElementById('fts-modal'))return; - const bg=document.createElement('div');bg.className='modal-bg';bg.id='fts-modal'; - bg.onclick=e=>{if(e.target===bg)bg.remove()}; - bg.innerHTML=``; + if(document.getElementById('cp-bg'))return; + const bg=document.createElement('div');bg.className='cp-bg';bg.id='cp-bg'; + bg.onclick=e=>{if(e.target===bg)closeCommandPalette()}; + const cont=document.createElement('div');cont.className='cp-container';bg.appendChild(cont); + cont.innerHTML=`
πŸ”
Use ↑↓ to navigate Β· Enter to select Β· f to search
`; document.body.appendChild(bg); - const input=document.getElementById('fts-input'); - input.focus(); - let timer=null; - input.oninput=()=>{clearTimeout(timer);timer=setTimeout(()=>runFtsSearch(input.value.trim()),180)}; + cpSelected=-1;const input=document.getElementById('cp-input');cpRecents=JSON.parse(localStorage.getItem('cp-recents')||'[]');input.focus();renderCommandPalette('');let timer=null; + input.oninput=()=>{clearTimeout(timer);document.getElementById('cp-clear').style.display=input.value?'block':'none';timer=setTimeout(()=>runCommandSearch(input.value.trim()),150)}; + input.onkeydown=(e)=>cpKeydown(e); } -async function runFtsSearch(q){ - const my=++ftsSeq; - const status=document.getElementById('fts-status'); - const box=document.getElementById('fts-results'); - if(!box)return; - if(!q){box.innerHTML='';if(status)status.textContent='';return} - if(status)status.textContent='Searching…'; +function cpKeydown(e){ + const results=document.querySelectorAll('.cp-item');switch(e.key){ + case'ArrowUp':e.preventDefault();cpSelected=Math.max(-1,cpSelected-1);scrollIntoSelected(results);updateSelected(results);break; + case'ArrowDown':e.preventDefault();cpSelected=Math.min(results.length-1,cpSelected+1);scrollIntoSelected(results);updateSelected(results);break; + case'Enter':e.preventDefault();if(cpSelected>=0&&cpSelectedr.classList.toggle('cp-selected',i===cpSelected));} +function scrollIntoSelected(results){ + if(cpSelected>=0&&cpSelectedbox.scrollTop+box.offsetHeight)box.scrollTop=itemBot-box.offsetHeight; + } +} +function renderCommandPalette(q){ + const box=document.getElementById('cp-results');if(!box)return;if(!q){ + if(cpRecents.length){ + box.innerHTML=`
Recent searches
${cpRecents.slice(0,5).map(r=>`
${esc(r)}
`).join('')}
Quick actions
⬇ Downloads
Open downloads panel
πŸ“Š Statistics
View library insights
🩺 Library health
Diagnostics & maintenance
? Keyboard shortcuts
View all shortcuts
`; + }else{box.innerHTML=`
Start typing to search or browse quick actions below
Quick actions
⬇ Downloads
Open downloads panel
πŸ“Š Statistics
View library insights
🩺 Library health
Diagnostics & maintenance
? Keyboard shortcuts
View all shortcuts
`;}cpSelected=-1;return; + }box.innerHTML='
Searching…
'; +} +async function runCommandSearch(q){ + const my=++cpSeq;const box=document.getElementById('cp-results');if(!box)return;if(!q){renderCommandPalette('');return} try{ - const r=await(await api('/api/search?limit=100&q='+encodeURIComponent(q))).json(); - if(my!==ftsSeq)return; // a newer keystroke superseded this response - const res=r.results||[]; - if(!res.length){box.innerHTML='
No matches.
';if(status)status.textContent='0 results';return} - box.innerHTML=res.map(h=>`
-
${esc(h.title)}
-
${esc(h.channel)} Β· ${esc(h.platform)}
- ${h.snippet?`
${ftsSnippet(h.snippet)}
`:''} -
`).join(''); - if(status)status.textContent=res.length+' result'+(res.length===1?'':'s'); - }catch(e){box.innerHTML=`
Search failed: ${esc(e.message)}
`} + const r=await(await api('/api/search?limit=60&q='+encodeURIComponent(q))).json(); + if(my!==cpSeq)return;const res=r.results||[]; + if(!res.length){box.innerHTML=`
No matches
Try different keywords
`;cpSelected=-1;return} + const byChannel={};res.forEach(h=>{if(!byChannel[h.channel])byChannel[h.channel]=[];byChannel[h.channel].push(h)}); + const groups=Object.entries(byChannel).map(([ch,items])=>`
${esc(ch)}
${items.map(h=>{const v=findVideo(h.video_id);const dt=(v&&v.upload_date)?fmtDate(v.upload_date):'';return `
${esc(h.title)}
${esc(h.platform)}${dt?' β€’ '+dt:''}
${h.snippet?`
${ftsSnippet(h.snippet)}
`:''}
`}).join('')}
`).join(''); + box.innerHTML=groups;cpSelected=0;const items=document.querySelectorAll('.cp-item');if(items.length>0){items[0].classList.add('cp-selected');scrollIntoSelected(items)} + }catch(e){box.innerHTML=`
Search failed: ${esc(e.message)}
`;cpSelected=-1} +} +function cpSelect(id){ + const q=document.getElementById('cp-input')?.value.trim()||'';if(q){cpRecents=cpRecents.filter(r=>r!==q);cpRecents.unshift(q);localStorage.setItem('cp-recents',JSON.stringify(cpRecents.slice(0,20)))} + closeCommandPalette();if(!findVideo(id)){setStatus('Video not in the loaded library β€” try Rescan');return} + selectVideo(id);document.getElementById('details')?.scrollIntoView({behavior:'smooth',block:'nearest'}); +} +function cpOpenRecent(q){document.getElementById('cp-input').value=q;runCommandSearch(q);} +function cpClearRecents(){cpRecents=[];localStorage.removeItem('cp-recents');renderCommandPalette('');} +function closeCommandPalette(){ + const bg=document.getElementById('cp-bg');if(!bg)return;bg.classList.add('cp-closing');setTimeout(()=>bg.remove(),200); } // FTS5 wraps matched terms in STX/ETX control chars; escape first, then mark. function ftsSnippet(s){return esc(s).replace(/\x02/g,'').replace(/\x03/g,'')} -function ftsOpen(id){ - document.getElementById('fts-modal')?.remove(); - if(!findVideo(id)){setStatus('Video not in the loaded library β€” try Rescan');return} - selectVideo(id); - document.getElementById('details')?.scrollIntoView({behavior:'smooth',block:'nearest'}); -} /* ── Select / Details ───────────────────────────────────────────── */ function selectVideo(id){selectedId=selectedId===id?null:id;renderGrid();renderDetails()} @@ -3330,7 +3370,8 @@ document.addEventListener('keydown',(e)=>{ e.preventDefault(); break; case 'f': - openSearch(); + if(document.getElementById('cp-bg'))closeCommandPalette(); + else openSearch(); e.preventDefault(); break; case 'r': @@ -3356,7 +3397,8 @@ function showShortcutsHelp(){ ${row('/','Focus filter box')} - ${row('f','Search titles, descriptions & transcripts')} + ${row('f','Command palette search (keyboard-driven)')} + ${row('↑↓ in search','Navigate results')} ${row('r','Rescan library')} ${row('d','Open downloads')} ${row('Esc','Close modal / cancel')}