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:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user