feat(notes): eye toggle to mask the note body in the editor

The note textarea showed its plaintext to anyone glancing at the screen,
unlike every other secret in the editor. An eye button on the NOTE label
masks it with -webkit-text-security (a textarea has no type="password").

The button IS the setting: it flips state.maskNotes, which is synced via
settings_json, so the choice sticks across entries and devices — no extra
Settings row for a one-bit preference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-22 20:16:06 +01:00
parent 72ef034459
commit 3f07c1217b
2 changed files with 23 additions and 2 deletions
+19 -2
View File
@@ -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;