diff --git a/containers/libreportal/frontend/components/apps/overview/css/overview.css b/containers/libreportal/frontend/components/apps/overview/css/overview.css index 5e7894e..21bdf97 100644 --- a/containers/libreportal/frontend/components/apps/overview/css/overview.css +++ b/containers/libreportal/frontend/components/apps/overview/css/overview.css @@ -117,6 +117,22 @@ background: rgba(var(--text-rgb), .03); border-color: rgba(var(--text-rgb), .08); } +/* Rows whose action is just "go to that tab" are clickable end to end — the + button inside remains the affordance (and the focusable control), the row + only widens the target. Hover lifts the whole row AND the button together so + it reads as one control, not a row that happens to sit under the cursor. */ +.ov-board-row.ov-clickable { cursor: pointer; transition: background .12s ease, border-color .12s ease; } +.ov-board-row.ov-clickable:hover { + background: rgba(var(--page-rgb, var(--accent-rgb)), .11); + border-color: rgba(var(--page-rgb, var(--accent-rgb)), .28); +} +.ov-board-row.ov-clickable.ok:hover, +.ov-board-row.ov-clickable.none:hover { + background: rgba(var(--text-rgb), .06); + border-color: rgba(var(--text-rgb), .14); +} +/* Keep the button's own hover styling from reading as "only this bit is live". */ +.ov-board-row.ov-clickable:hover .ov-board-actions .updater-btn { filter: brightness(1.08); } .ov-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; } .ov-dot.ok { background: #36d399; } .ov-dot.warn { background: #fbbd23; } diff --git a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js index 22b5044..77a4531 100644 --- a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js +++ b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js @@ -316,6 +316,13 @@ class OverviewManager { if (oa) { switch (oa.dataset.overviewAction) { case 'goto': + // Whole-row navigation is a convenience over the row's own button — + // don't fire it when the click was really the end of a text drag + // inside the row. (Clicks on the button itself always count.) + if (oa.classList.contains('ov-board-row')) { + const sel = window.getSelection && window.getSelection(); + if (sel && !sel.isCollapsed && String(sel).trim()) break; + } // Board rows can land on Updates pre-filtered (e.g. Security → the // affected apps only), so honor an optional data-filter on the way. if (oa.dataset.filter) this.filter = oa.dataset.filter; @@ -425,6 +432,7 @@ class OverviewManager { text: `${waiting.length} update${waiting.length === 1 ? '' : 's'} waiting for you`, sub: `${esc(listOf(waiting))} — a recovery snapshot is taken before each update` + (selfing ? ` · ${selfing} more will install automatically` : ''), + nav: { tab: 'updates', filter: 'updates' }, actions: goto('updates', 'Review', 'updates') + ``, }); @@ -438,6 +446,7 @@ class OverviewManager { hue: 'updates', icon: '⬆️', kind: 'ok', text: `${pending.length} update${pending.length === 1 ? '' : 's'} installing automatically${winBit}`, sub: `${esc(listOf(pending))} — each is snapshotted first, and rolled back if it fails`, + nav: { tab: 'updates', filter: 'updates' }, actions: goto('updates', 'Review', 'updates'), }); } @@ -469,6 +478,7 @@ class OverviewManager { text: `${u.totalCves} known CVE${u.totalCves === 1 ? '' : 's'} across ${hit} app${hit === 1 ? '' : 's'}`, badge: worst ? ` ${worst}` : '', sub: order.filter((s) => sev[s]).map((s) => `${sev[s]} ${s}`).join(' · '), + nav: { tab: 'updates', filter: 'security' }, actions: goto('updates', 'Review', 'security'), }); } else { @@ -482,6 +492,7 @@ class OverviewManager { hue: 'updater', icon: '✨', kind: 'warn', text: `${u.improvements} signed improvement${u.improvements === 1 ? '' : 's'} ready to apply`, sub: 'individually reversible — a snapshot is taken first', + nav: { tab: 'improvements' }, actions: goto('improvements', 'View'), }); } else { @@ -497,6 +508,7 @@ class OverviewManager { hue: 'backups', icon: '💾', kind: 'warn', text: 'No backup location configured', sub: 'apps can’t be protected until there’s somewhere to back them up to', + nav: { tab: 'backups' }, actions: goto('backups', 'Open Backups'), }); } else if (b.never + b.old) { @@ -505,6 +517,7 @@ class OverviewManager { hue: 'backups', icon: '💾', kind: 'warn', text: `${n} app${n === 1 ? '' : 's'} need${n === 1 ? 's' : ''} a fresh backup`, sub: [b.never && `${b.never} never backed up`, b.old && `${b.old} older than a week`].filter(Boolean).join(' · '), + nav: { tab: 'backups' }, actions: goto('backups', 'Open Backups'), }); } else { @@ -516,8 +529,17 @@ class OverviewManager { return rows; } + // A row whose action is a plain "go to that tab" (Review / View / Open + // Backups) makes the WHOLE row a hit target for it — the button stays put as + // the visible affordance and the only focusable control, so the row is a + // mouse convenience layered on top rather than a nested role="button" (which + // would swallow the buttons it contains). Rows that only carry a real + // operation (Update all) or no action at all stay inert. boardRow(r) { - return `