feat: persistent inline header search with overlay results

Replaces the hidden toggle-based mobile search bar with a permanently
visible pill-shaped search input in the header row, next to the logo.

- Search is always visible — no button click required
- Typing shows a floating overlay with library results (artists, albums,
  songs) without navigating away from the current page
- Clearing the input or navigating to any page closes the overlay silently
- The ✕ clear button appears only while text is present
- navigate() now calls closeHeaderSearch() so results disappear cleanly
  when tapping any result or bottom nav tab
- Old toggleMobileSearch / closeMobileSearch kept as no-op stubs for safety

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-27 12:55:26 +02:00
1 parent b7afa19f70
commit 36f968881b
5 files changed
+48 -38

No files matched your search

+11 -13
View File
@@ -103,30 +103,28 @@
<!-- Mobile header -->
<div id="mobile-header" style="position:sticky;top:0;background:var(--bg);z-index:10;border-bottom:1px solid var(--border)">
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 14px">
<div class="sidebar-logo">
<div style="display:flex;align-items:center;gap:8px;padding:10px 12px">
<div class="sidebar-logo" style="flex-shrink:0;gap:6px">
<div class="icon" style="width:26px;height:26px;font-size:12px;border-radius:7px">♫</div>
<span style="font-size:15px;font-weight:700">Nova<em style="color:var(--accent);font-style:normal">Sonic</em></span>
<span style="font-size:14px;font-weight:700">Nova<em style="color:var(--accent);font-style:normal">Sonic</em></span>
</div>
<div style="display:flex;align-items:center;gap:4px">
<div class="header-search-wrap">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input id="header-search-input" type="text" placeholder="Suchen…" oninput="onHeaderSearch(this.value)" autocomplete="off">
<button id="header-search-clear" onclick="closeHeaderSearch()" style="display:none;background:none;border:none;color:var(--text3);cursor:pointer;font-size:15px;padding:0;line-height:1;flex-shrink:0">✕</button>
</div>
<div style="display:flex;align-items:center;gap:2px;flex-shrink:0">
<button class="hbtn notif-bell-btn" onclick="toggleNotifPanel()" title="Benachrichtigungen" style="padding:8px;color:var(--text2)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<span class="notif-badge" style="display:none">0</span>
</button>
<button class="hbtn" id="mobile-search-btn" onclick="toggleMobileSearch()" title="Suche" style="padding:8px;color:var(--text2)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
</button>
<button class="hbtn" onclick="openSettings()" title="Einstellungen" style="padding:8px;color:var(--text2)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</button>
</div>
</div>
<div id="mobile-search-bar" style="display:none;padding:0 14px 10px">
<div class="search-box" style="background:var(--surface);border-color:var(--border2)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input id="mobile-search-input" type="text" placeholder="Künstler, Album, Titel…" oninput="onMobileSearch(this.value)" style="font-size:15px">
<button onclick="closeMobileSearch()" style="background:none;border:none;color:var(--text3);cursor:pointer;font-size:16px;padding:0 2px;line-height:1">✕</button>
</div>
<div id="header-search-overlay" style="display:none;position:absolute;left:0;right:0;top:100%;max-height:calc(100vh - 100px);overflow-y:auto;background:var(--bg);border-bottom:1px solid var(--border);box-shadow:0 12px 32px rgba(0,0,0,.5);padding:6px 12px 20px;z-index:9">
<div id="header-search-results"></div>
</div>
</div>
+2 -2
View File
@@ -12,7 +12,7 @@ import {
npToggleStar, updateMediaSession,
} from './player.js';
import {
navigate, _navGoBack, _navLoad, onLocalSearch, onMobileSearch,
navigate, _navGoBack, _navLoad, onHeaderSearch, closeHeaderSearch,
onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum,
searchSimilarArtist, searchAndPreview, loadItunesAlbum, expandArtistBio,
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist,
@@ -293,7 +293,7 @@ try { history.replaceState({ nsIdx: 0 }, '', ''); } catch {}
// ─── EXPOSE GLOBALS (for HTML onclick attributes) ───
Object.assign(window, {
// Navigation
navigate, onLocalSearch, onMobileSearch, onGlobalSearch, s_goArtist, s_goAlbum,
navigate, onHeaderSearch, closeHeaderSearch, onGlobalSearch, s_goArtist, s_goAlbum,
playerBarGoArtist, playerBarGoAlbum, searchSimilarArtist, searchAndPreview,
loadItunesAlbum, expandArtistBio,
// Player
+3 -15
View File
@@ -25,24 +25,12 @@ export function switchNavMode(mode) {
document.getElementById('bnav-tab-discover')?.classList.toggle('active', mode==='discover');
}
// ─── MOBILE SEARCH ───
// ─── MOBILE SEARCH (legacy stubs — header search is now always visible) ───
export function toggleMobileSearch() {
const bar = document.getElementById('mobile-search-bar');
const btn = document.getElementById('mobile-search-btn');
const open = bar.style.display==='none' || bar.style.display==='';
bar.style.display = open ? 'block' : 'none';
btn.style.color = open ? 'var(--accent)' : 'var(--text2)';
if(open) { setTimeout(() => document.getElementById('mobile-search-input').focus(), 50); }
else { closeMobileSearch(); }
document.getElementById('header-search-input')?.focus();
}
export function closeMobileSearch() {
const bar = document.getElementById('mobile-search-bar');
const inp = document.getElementById('mobile-search-input');
const btn = document.getElementById('mobile-search-btn');
bar.style.display = 'none';
btn.style.color = 'var(--text2)';
inp.value = '';
navigate('home');
import('./views.js').then(m => m.closeHeaderSearch());
}
// ─── SETUP ───
+26 -8
View File
@@ -27,7 +27,18 @@ export function _navLoad() {
return false;
}
export function closeHeaderSearch() {
const overlay = document.getElementById('header-search-overlay');
const input = document.getElementById('header-search-input');
const clearBtn = document.getElementById('header-search-clear');
if (overlay) overlay.style.display = 'none';
if (input) input.value = '';
if (clearBtn) clearBtn.style.display = 'none';
clearTimeout(S._headerSearchTimer);
}
export function navigate(page, data=null) {
closeHeaderSearch();
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item,.bnav-item').forEach(n => n.classList.remove('active','active-blue'));
const cls = ['discover','new-releases','recommendations','spotify-importer'].includes(page) ? 'active-blue' : 'active';
@@ -940,8 +951,8 @@ export function onLocalSearch(val) {
S.searchTimer = setTimeout(() => doLocalSearch(val), 350);
}
export async function doLocalSearch(q) {
const el = document.getElementById('search-results');
export async function doLocalSearch(q, el) {
if (!el) el = document.getElementById('search-results');
el.innerHTML = '<div class="spinner"></div>';
try {
const hiragana = await _romajiToHiragana(q);
@@ -999,14 +1010,21 @@ export async function doLocalSearch(q) {
} catch(e) { el.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
}
// ─── MOBILE SEARCH ───
export function onMobileSearch(val) {
clearTimeout(S._mobileSearchTimer);
if(!val.trim()) { navigate('home'); return; }
navigate('search');
S._mobileSearchTimer = setTimeout(() => doLocalSearch(val), 300);
// ─── HEADER SEARCH (persistent inline bar) ───
export function onHeaderSearch(val) {
const overlay = document.getElementById('header-search-overlay');
const clearBtn = document.getElementById('header-search-clear');
if (clearBtn) clearBtn.style.display = val ? 'block' : 'none';
clearTimeout(S._headerSearchTimer);
if (!val.trim()) { if (overlay) overlay.style.display = 'none'; return; }
if (overlay) overlay.style.display = 'block';
const el = document.getElementById('header-search-results');
S._headerSearchTimer = setTimeout(() => doLocalSearch(val, el), 300);
}
// ─── MOBILE SEARCH (legacy no-op — replaced by inline header search) ───
export function onMobileSearch(val) { onHeaderSearch(val); }
// ─── GLOBAL (ITUNES) SEARCH ───
export function onGlobalSearch(val) {
clearTimeout(S.globalSearchTimer);
+6
View File
@@ -63,6 +63,12 @@ html.ns-authed #setup-screen{display:none}
.search-box input::placeholder{color:var(--text3)}
.search-box svg{flex-shrink:0;color:var(--text3)}
.header-search-wrap{flex:1;min-width:0;display:flex;align-items:center;gap:7px;background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:7px 12px;cursor:text;transition:border-color .2s}
.header-search-wrap:focus-within{border-color:var(--border2)}
.header-search-wrap svg{flex-shrink:0;color:var(--text3)}
.header-search-wrap input{flex:1;min-width:0;background:none;border:none;outline:none;color:var(--text);font-family:var(--font);font-size:14px}
.header-search-wrap input::placeholder{color:var(--text3)}
nav{flex:1;overflow-y:auto;padding:4px 10px}
.nav-section{margin-bottom:18px}
.nav-section-title{font-size:10px;font-weight:600;letter-spacing:1px;text-transform:uppercase;color:var(--text3);padding:0 8px;margin-bottom:3px}