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=`
-
🔍 Search library
-
-
-
-
`;
+ 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=>`
`).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(){
Keyboard shortcuts
${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')}