From ef6f02967f66e2663fb6d9f12dd46f94e0f9b0f7 Mon Sep 17 00:00:00 2001 From: Prince Chrix <134553523+princechrix@users.noreply.github.com> Date: Sat, 2 May 2026 00:07:55 +0200 Subject: [PATCH] feat: forced acknowledgment with typed confirmation - Two dismiss methods: type DONE or click button - 5-second mandatory delay before acknowledgment area appears - Keyboard escape prevention (Escape, Alt+F4, Alt+Tab blocked in renderer) - Periodic refocus interval ensures overlay stays on top - Shake animation on incorrect input --- src/main/windowManager.js | 14 +++++ src/renderer/overlay.html | 105 +++++++++++++++++++++++++++++++------ src/renderer/overlayApp.js | 35 ++++++++++++- 3 files changed, 137 insertions(+), 17 deletions(-) diff --git a/src/main/windowManager.js b/src/main/windowManager.js index f761036..44e11f9 100644 --- a/src/main/windowManager.js +++ b/src/main/windowManager.js @@ -2,6 +2,7 @@ const { BrowserWindow, screen } = require('electron'); const path = require('path'); let overlayWindows = []; +let refocusInterval = null; function createOverlayForDisplay(display, session) { const { x, y, width, height } = display.bounds; @@ -56,9 +57,22 @@ function showOverlay(session) { const overlay = createOverlayForDisplay(display, session); overlayWindows.push(overlay); }); + + refocusInterval = setInterval(() => { + overlayWindows.forEach((w) => { + if (!w.isDestroyed() && !w.isFocused()) { + w.setAlwaysOnTop(true, 'screen-saver'); + w.focus(); + } + }); + }, 500); } function closeOverlay() { + if (refocusInterval) { + clearInterval(refocusInterval); + refocusInterval = null; + } overlayWindows.forEach((w) => { if (!w.isDestroyed()) w.destroy(); }); diff --git a/src/renderer/overlay.html b/src/renderer/overlay.html index 40b00ab..b997d5a 100644 --- a/src/renderer/overlay.html +++ b/src/renderer/overlay.html @@ -12,8 +12,12 @@ --bg: #0a0a0a; --text: #e8e8e8; --text-dim: #666; + --text-muted: #444; --accent: #ff4d00; + --accent-glow: rgba(255, 77, 0, 0.08); + --danger: #e81123; --font: 'Segoe UI', -apple-system, system-ui, sans-serif; + --mono: 'Cascadia Code', 'Fira Code', monospace; } html, body { @@ -32,7 +36,7 @@ align-items: center; justify-content: center; height: 100vh; - gap: 32px; + gap: 24px; animation: fadeIn 0.3s ease; } @@ -59,8 +63,72 @@ color: var(--text-dim); } + .overlay__countdown { + font-size: 12px; + color: var(--text-dim); + font-variant-numeric: tabular-nums; + height: 18px; + } + + .overlay__ack { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + margin-top: 8px; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + } + + .overlay__ack.active { + opacity: 1; + pointer-events: auto; + } + + .overlay__ack-hint { + font-size: 11px; + color: var(--text-muted); + letter-spacing: 0.5px; + } + + .overlay__type-input { + width: 200px; + padding: 10px 14px; + background: transparent; + border: 2px solid var(--text-muted); + border-radius: 6px; + color: var(--text); + font-family: var(--mono); + font-size: 18px; + font-weight: 700; + text-align: center; + letter-spacing: 4px; + text-transform: uppercase; + outline: none; + transition: border-color 0.2s ease; + } + + .overlay__type-input::placeholder { + color: var(--text-muted); + letter-spacing: 4px; + font-size: 14px; + } + + .overlay__type-input:focus { + border-color: var(--accent); + } + + .overlay__type-input.error { + border-color: var(--danger); + animation: shake 0.3s ease; + } + + .overlay__type-input.success { + border-color: var(--accent); + } + .overlay__dismiss { - margin-top: 16px; padding: 14px 48px; border: 2px solid var(--accent); background: transparent; @@ -73,31 +141,30 @@ cursor: pointer; border-radius: 6px; transition: all 0.2s ease; - opacity: 0; - pointer-events: none; } - .overlay__dismiss.active { - opacity: 1; - pointer-events: auto; - } - - .overlay__dismiss.active:hover { + .overlay__dismiss:hover { background: var(--accent); color: #fff; } - .overlay__countdown { - font-size: 12px; - color: var(--text-dim); - font-variant-numeric: tabular-nums; - height: 18px; + .overlay__or { + font-size: 11px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 2px; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } + + @keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-6px); } + 75% { transform: translateX(6px); } + }
@@ -106,7 +173,13 @@ - + + diff --git a/src/renderer/overlayApp.js b/src/renderer/overlayApp.js index a992cac..f67d8d1 100644 --- a/src/renderer/overlayApp.js +++ b/src/renderer/overlayApp.js @@ -1,11 +1,15 @@ const DISMISS_DELAY = 5; +const CONFIRM_WORD = 'DONE'; const messageEl = document.getElementById('message'); const sessionNameEl = document.getElementById('session-name'); const countdownEl = document.getElementById('countdown'); +const ackArea = document.getElementById('ack-area'); +const typeInput = document.getElementById('type-input'); const btnDismiss = document.getElementById('btn-dismiss'); let remaining = DISMISS_DELAY; +let unlocked = false; window.overlay.onData((data) => { messageEl.textContent = data.message; @@ -18,13 +22,42 @@ window.overlay.onData((data) => { } else { clearInterval(tick); countdownEl.textContent = ''; - btnDismiss.classList.add('active'); + unlocked = true; + ackArea.classList.add('active'); + typeInput.focus(); } }, 1000); countdownEl.textContent = `Dismiss available in ${remaining}s`; }); +typeInput.addEventListener('input', () => { + if (!unlocked) return; + + typeInput.classList.remove('error'); + + if (typeInput.value.toUpperCase() === CONFIRM_WORD) { + typeInput.classList.add('success'); + window.overlay.dismiss(); + } +}); + +typeInput.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + e.preventDefault(); + } +}); + btnDismiss.addEventListener('click', () => { + if (!unlocked) return; window.overlay.dismiss(); }); + +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' || (e.altKey && e.key === 'F4')) { + e.preventDefault(); + } + if (e.altKey && e.key === 'Tab') { + e.preventDefault(); + } +});