diff --git a/frontend/app.js b/frontend/app.js index 515a196..cfaa6b3 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -8705,10 +8705,34 @@ async function maybeShowUpdateBanner() { // swap is instant. Purely an optimisation — if it never finishes, applyUpdate // falls back to downloading. prefetchUpdate(); + // Ask only once the new build is fully on disk, so "Refresh UI" is an + // instant swap. Until then keep downloading quietly and re-check; if the + // download never completes (8 min) ask anyway — Refresh UI then downloads. + _updateSeenAt = _updateSeenAt || Date.now(); + if (!(await updateDownloaded()) && Date.now() - _updateSeenAt < 8 * 60 * 1000) { + setTimeout(maybeShowUpdateBanner, 15000); + return; + } showUpdateBanner(server); } } +let _updateSeenAt = 0; +// Is the new worker installed and holding the complete new shell? No service +// worker support (or no registration) means there is nothing to wait for. +async function updateDownloaded() { + try { + if (!('serviceWorker' in navigator)) return true; + const reg = _swReg || await navigator.serviceWorker.getRegistration(); + if (!reg) return true; + if (!reg.waiting) return false; + const SU = window.SwUpdate; + if (!SU) return true; + const st = await SU.askCacheStatus(reg.waiting, { timeoutMs: 4000, setTimeout: (fn, ms) => setTimeout(fn, ms) }); + return !!(st && st.ready); + } catch { return false; } +} + // Kicks the service worker into installing the new build in the background. // Safe to call repeatedly: update() is a no-op when a worker is already // installing or waiting. diff --git a/frontend/styles.css b/frontend/styles.css index dbe047a..f0ce080 100755 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -4365,3 +4365,18 @@ html[data-layout="bento-hub"] .media-shape-classic { display:none; } html[data-layout="f7-swipe"] .media-shape-f7-swipe, html[data-layout="glass-stage"] .media-shape-glass-stage, html[data-layout="bento-hub"] .media-shape-bento-hub { display:inline; } + +/* ===== Search button — one icon + label, restyled per layout and colour theme ===== */ +.search-btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; } +.search-btn::before { + content: ""; width: 17px; height: 17px; flex: none; background: currentColor; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='m16 16 5 5'/%3E%3C/svg%3E") center / contain no-repeat; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='m16 16 5 5'/%3E%3C/svg%3E") center / contain no-repeat; +} +.search-btn { box-shadow: 0 8px 22px -8px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, .28); } +.search-btn:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 3px; } +:where(:root[data-theme="light"]) .search-btn { background: var(--accent-deep); box-shadow: 0 2px 8px -2px rgba(0, 0, 0, .35), inset 0 1px 0 rgba(255, 255, 255, .25); } +:where(:root[data-theme="light"]) .search-btn:hover { background: var(--accent); box-shadow: 0 4px 12px -3px rgba(0, 0, 0, .4); } +:where(:root[data-theme="contrast"]) .search-btn { background: #fff; color: #000; border: 2px solid #fff; box-shadow: none; } +:where(:root[data-theme="contrast"]) .search-btn:hover { background: var(--accent-bright); border-color: var(--accent-bright); box-shadow: none; } +:where(:root[data-theme="contrast"]) .search-btn:focus-visible { outline: 3px solid #fff; outline-offset: 3px; } diff --git a/frontend/theme-bento-hub.css b/frontend/theme-bento-hub.css index 758a290..eae713e 100644 --- a/frontend/theme-bento-hub.css +++ b/frontend/theme-bento-hub.css @@ -361,3 +361,6 @@ html[data-layout="bento-hub"] .main { padding-bottom:var(--bento-bottom-clearanc html[data-layout="bento-hub"] :is(.body,.player-pane,.cards) { padding-bottom:16px; } html[data-layout="bento-hub"] .modal-backdrop { z-index:600; } html[data-layout="bento-hub"] .shortcut-overlay { z-index:600; } + +/* Bento draws its own icon span; skip the shared mask icon. */ +html[data-layout="bento-hub"] .search-btn::before { display: none; } diff --git a/frontend/theme-classic.css b/frontend/theme-classic.css index 9db1018..198ccc8 100644 --- a/frontend/theme-classic.css +++ b/frontend/theme-classic.css @@ -235,3 +235,11 @@ html[data-layout="classic"] .card:has(.card-note-btn) .card-note-btn { grid-colu html[data-layout="classic"] .card:has(.card-note-btn) .card-menu { grid-column: 4; grid-row: 1; } html[data-layout="classic"] .card:has(.card-note-btn) .card-del { grid-column: 4; grid-row: 2; } html[data-layout="classic"] .card:has(.card-note-btn) { column-gap: 8px; } + +/* Search button: outlined rectangle that fills with the accent on hover. */ +html[data-layout="classic"] .search-btn { background: transparent; color: var(--accent); border: 1.5px solid var(--accent); box-shadow: none; } +html[data-layout="classic"] .search-btn:hover { background: var(--accent); color: #fff; transform: none; box-shadow: none; } +html[data-layout="classic"][data-theme="contrast"] .search-btn { background: #fff; color: #000; border-color: #fff; } +html[data-layout="classic"][data-theme="contrast"] .search-btn:hover { background: var(--accent-bright); border-color: var(--accent-bright); color: #000; } +html[data-layout="classic"][data-theme="light"] .search-btn { color: var(--accent-deep); border-color: var(--accent-deep); } +html[data-layout="classic"][data-theme="light"] .search-btn:hover { background: var(--accent-deep); color: #fff; } diff --git a/frontend/theme-f7.css b/frontend/theme-f7.css index a8a6b96..e006b39 100644 --- a/frontend/theme-f7.css +++ b/frontend/theme-f7.css @@ -343,3 +343,6 @@ html[data-layout="f7-swipe"] #remoteView, html[data-layout="f7-swipe"] #presente html[data-layout="f7-swipe"] .list-title-row h2 { font-size: 29px; } html[data-layout="f7-swipe"] #cards .card { min-height: 76px; } } + +/* Search button: plain accent text action (navbar style) — no icon. */ +html[data-layout="f7-swipe"] .search-btn::before { display: none; } diff --git a/frontend/theme-glass.css b/frontend/theme-glass.css index 71439b6..033a834 100644 --- a/frontend/theme-glass.css +++ b/frontend/theme-glass.css @@ -375,3 +375,15 @@ html[data-layout="glass-stage"] .svc-font button { width: 28px; height: 30px; } html[data-layout="glass-stage"] #controls .ch-badge { cursor: pointer; font-family: var(--mono); transition: background .15s, border-color .15s; } html[data-layout="glass-stage"] #controls .ch-badge:hover { background: rgba(255, 255, 255, 0.12); border-color: var(--accent); } html[data-layout="glass-stage"] #controls .ch-badge:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } + +/* Search button: frosted pill, accent-tinted, with a soft glow. */ +html[data-layout="glass-stage"] .search-btn { + background: color-mix(in srgb, var(--accent) 30%, transparent); color: var(--text); + border: 1px solid color-mix(in srgb, var(--accent-bright) 55%, transparent); border-radius: 999px; + -webkit-backdrop-filter: blur(14px) saturate(1.4); backdrop-filter: blur(14px) saturate(1.4); + box-shadow: 0 6px 22px -8px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, .3); +} +html[data-layout="glass-stage"] .search-btn:hover { background: color-mix(in srgb, var(--accent) 48%, transparent); transform: translateY(-1px); } +html[data-layout="glass-stage"][data-theme="contrast"] .search-btn { background: #fff; color: #000; border-color: #fff; } +html[data-layout="glass-stage"][data-theme="light"] .search-btn { background: color-mix(in srgb, var(--accent-deep) 88%, transparent); color: #fff; border-color: color-mix(in srgb, var(--accent-deep) 70%, #fff); } +html[data-layout="glass-stage"][data-theme="light"] .search-btn:hover { background: var(--accent-deep); }