diff --git a/TEST_SCENARIOS.md b/TEST_SCENARIOS.md index 45d229f..de02a10 100644 --- a/TEST_SCENARIOS.md +++ b/TEST_SCENARIOS.md @@ -310,7 +310,7 @@ l'éditeur slideover : créer + éditer une entrée, ajouter/retirer des tags, ### H7 · combo déjà pris par une autre application ✅ - **ACTION** : assigner à un hotkey un combo qu'une autre app tient déjà en global (ex. `Ctrl+Shift+Q` si Quick Assist / un lanceur l'occupe ; sinon, tester en assignant le même combo dans deux apps). -- **ATTENDU** : toast **nommant le combo refusé** — « Already used by another app: 2FA code — pick another combo » — **et** un ⚠ ambre persistant sur cette ligne de Settings (visible longtemps après le toast). Les **autres** hotkeys restent fonctionnels (enregistrement best-effort, pas de tout-ou-rien). +- **ATTENDU** : toast **nommant le combo refusé** — « Already used by another app: 2FA code — pick another combo » — **et** un ⚠ ambre persistant sur cette ligne de Settings, **et** une pastille ambre sur l'entrée *Settings* de la sidebar (seul indice hors du panneau). Le toast d'avertissement reste ~10 s et se ferme au clic. Les **autres** hotkeys restent fonctionnels (enregistrement best-effort, pas de tout-ou-rien). - **ACTION 2** : fermer l'app concurrente → *Reset to defaults* (ou re-capturer le combo) → le hotkey redevient actif **et le ⚠ disparaît**. - **Piège de test** : une app qui capture via un *hook* bas niveau (et non `RegisterHotKey`) avale la frappe sans faire échouer notre enregistrement → hotkey inerte **sans** ⚠. Utiliser AutoHotkey (`^+t::MsgBox`) ou Lightshot pour un vrai conflit. - **Historique** : le message disait « one or both hotkeys » (faux depuis qu'il y en a 3) sans dire lequel ; et un échec sur **Quick search** ou **New entry** n'était écrit que dans le panel debug — invisible en prod, le hotkey semblait juste cassé. diff --git a/css/style.css b/css/style.css index 973ac0d..3ffe5e8 100644 --- a/css/style.css +++ b/css/style.css @@ -2551,6 +2551,19 @@ body[data-editor-position="center"]:has(#settingsPanel.is-open)::before { min-width: 240px; animation: toastIn var(--t-base); } +/* Errors / warnings linger — let the user clear them instead of waiting. */ +.toast.is-dismissable { cursor: pointer; } +/* Settings entry point when a hotkey is dead: a dot the user meets in the + normal UI, not only inside the panel they may never open. */ +#settingsBtn.has-alert { position: relative; } +#settingsBtn.has-alert::after { + content: ""; + position: absolute; + top: 6px; right: 8px; + width: 7px; height: 7px; + border-radius: 50%; + background: var(--warning, #e0a800); +} .toast.is-success { border-left-color: var(--success); } .toast.is-error { border-left-color: var(--danger); } .toast.is-warning { border-left-color: var(--warning); } diff --git a/js/app.autofill.js b/js/app.autofill.js index 691bbfd..c559068 100644 --- a/js/app.autofill.js +++ b/js/app.autofill.js @@ -36,6 +36,17 @@ function renderHotkeyConflicts() { 'the hotkey does nothing. Pick another one.'; else btn.removeAttribute('title'); } + // Dot on the Settings button: the ⚠ inside the panel only helps someone who + // opens it, and the toast may well have been missed. This is the only cue + // left in the main UI that a hotkey is silently dead. + const gear = document.getElementById('settingsBtn'); + if (gear) { + gear.classList.toggle('has-alert', bad.size > 0); + if (bad.size > 0) + gear.title = 'A hotkey combo is already used by another app — see Autofill settings'; + else if (gear.title.startsWith('A hotkey combo')) + gear.title = 'Settings'; + } } // Format a combo for human display: "Ctrl+Shift+L". diff --git a/js/app.js b/js/app.js index d79f51a..aed6e39 100644 --- a/js/app.js +++ b/js/app.js @@ -1016,7 +1016,16 @@ function toast(msg, type) { t.className = 'toast is-' + type; t.textContent = msg; container.appendChild(t); - setTimeout(() => t.remove(), 2800); + // A success toast is a receipt — 2.8 s is plenty. An error or warning is + // something the user has to READ and usually act on, and 2.8 s isn't + // enough for either, so those linger and can be clicked away early. + const actionable = (type === 'error' || type === 'warning'); + if (actionable) { + t.classList.add('is-dismissable'); + t.title = 'Click to dismiss'; + t.addEventListener('click', () => t.remove()); + } + setTimeout(() => t.remove(), actionable ? 10000 : 2800); } // ============================================================