diff --git a/css/style.css b/css/style.css index 8716672..12e7d37 100644 --- a/css/style.css +++ b/css/style.css @@ -1891,6 +1891,10 @@ input[type="range"]::-webkit-slider-thumb { line-height: 1.55; white-space: pre-wrap; } +/* A textarea has no type="password"; -webkit-text-security is the Chromium + equivalent (WebView2 is Chromium, so it's always available here). */ +.so-note-body.is-masked { -webkit-text-security: disc; } +.so-note-label { display: flex; align-items: center; justify-content: space-between; } /* ---- + New dropdown ---------------------------------- */ .new-entry-wrap { diff --git a/js/app.js b/js/app.js index 2c6b95e..3e7c970 100644 --- a/js/app.js +++ b/js/app.js @@ -632,6 +632,9 @@ const state = { autoLock: parseInt(localStorage.getItem('autoLockMin') || '5'), askBeforeDelete: localStorage.getItem('askBeforeDelete') !== '0', // default true maskUsernames: localStorage.getItem('maskUsernames') === '1', // default false + // Mask the note body in the editor (shoulder-surfing). Toggled by the eye + // on the Note field itself — that button IS the setting, no Settings row. + maskNotes: localStorage.getItem('maskNotes') === '1', // default false // Show the raw site/URL under the display name on cards. Default OFF // because the display name is meant to be the user-friendly label; // most users don't want the hostname cluttering the card layout. @@ -4176,7 +4179,12 @@ function buildCustomFieldRow(field, idx, rerender) { // encrypted_password+iv columns (same crypto pipeline as login passwords). function soNoteBodyField(value) { const wrap = el('div', { class: 'slideover-field' }); - wrap.appendChild(el('div', { class: 'slideover-field-label' }, 'Note')); + const lbl = el('div', { class: 'slideover-field-label so-note-label' }, 'Note'); + const eye = el('button', { class: 'icon-btn icon-btn-sm', type: 'button', + title: 'Mask / reveal the note body' }); + eye.appendChild(icon('i-eye')); + lbl.appendChild(eye); + wrap.appendChild(lbl); const ta = el('textarea', { id: 'soNoteBody', class: 'so-input so-note-body', @@ -4185,6 +4193,14 @@ function soNoteBodyField(value) { autocomplete: 'off', autocorrect: 'off', spellcheck: 'false', }); ta.value = value || ''; + ta.classList.toggle('is-masked', state.maskNotes); + eye.addEventListener('click', ev => { + ev.stopPropagation(); // see "Click-outside-slideover bug" in CLAUDE.md + state.maskNotes = !state.maskNotes; + localStorage.setItem('maskNotes', state.maskNotes ? '1' : '0'); + ta.classList.toggle('is-masked', state.maskNotes); + saveServerSettings(); + }); wrap.appendChild(ta); return wrap; } @@ -6703,7 +6719,7 @@ async function autoPurgeTrashIfNeeded() { // Synced keys (user preferences, portable across devices). Device-specific // toggles (quickUnlockEnabled, autofillEnabled) stay in localStorage. const SYNCED_SETTING_KEYS = [ - 'theme', 'autoLock', 'askBeforeDelete', 'maskUsernames', + 'theme', 'autoLock', 'askBeforeDelete', 'maskUsernames', 'maskNotes', 'compactActions', 'viewMode', 'hibpEnabled', 'showSiteOnCards', 'sortBy', 'sortDir', 'pageSize', // Hotkey combos are user preferences — values are portable. The @@ -6773,6 +6789,7 @@ async function loadServerSettings() { case 'autoLock': localStorage.setItem('autoLockMin', String(v)); break; case 'askBeforeDelete': localStorage.setItem('askBeforeDelete', v ? '1' : '0'); break; case 'maskUsernames': localStorage.setItem('maskUsernames', v ? '1' : '0'); break; + case 'maskNotes': localStorage.setItem('maskNotes', v ? '1' : '0'); break; case 'compactActions': localStorage.setItem('compactActions', v ? '1' : '0'); break; case 'viewMode': localStorage.setItem('viewMode', v); break; case 'hibpEnabled': localStorage.setItem('hibpEnabled', v ? '1' : '0'); break;