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;
|
line-height: 1.55;
|
||||||
white-space: pre-wrap;
|
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 dropdown ---------------------------------- */
|
||||||
.new-entry-wrap {
|
.new-entry-wrap {
|
||||||
|
|||||||
@@ -632,6 +632,9 @@ const state = {
|
|||||||
autoLock: parseInt(localStorage.getItem('autoLockMin') || '5'),
|
autoLock: parseInt(localStorage.getItem('autoLockMin') || '5'),
|
||||||
askBeforeDelete: localStorage.getItem('askBeforeDelete') !== '0', // default true
|
askBeforeDelete: localStorage.getItem('askBeforeDelete') !== '0', // default true
|
||||||
maskUsernames: localStorage.getItem('maskUsernames') === '1', // default false
|
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
|
// 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;
|
// because the display name is meant to be the user-friendly label;
|
||||||
// most users don't want the hostname cluttering the card layout.
|
// 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).
|
// encrypted_password+iv columns (same crypto pipeline as login passwords).
|
||||||
function soNoteBodyField(value) {
|
function soNoteBodyField(value) {
|
||||||
const wrap = el('div', { class: 'slideover-field' });
|
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', {
|
const ta = el('textarea', {
|
||||||
id: 'soNoteBody',
|
id: 'soNoteBody',
|
||||||
class: 'so-input so-note-body',
|
class: 'so-input so-note-body',
|
||||||
@@ -4185,6 +4193,14 @@ function soNoteBodyField(value) {
|
|||||||
autocomplete: 'off', autocorrect: 'off', spellcheck: 'false',
|
autocomplete: 'off', autocorrect: 'off', spellcheck: 'false',
|
||||||
});
|
});
|
||||||
ta.value = value || '';
|
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);
|
wrap.appendChild(ta);
|
||||||
return wrap;
|
return wrap;
|
||||||
}
|
}
|
||||||
@@ -6703,7 +6719,7 @@ async function autoPurgeTrashIfNeeded() {
|
|||||||
// Synced keys (user preferences, portable across devices). Device-specific
|
// Synced keys (user preferences, portable across devices). Device-specific
|
||||||
// toggles (quickUnlockEnabled, autofillEnabled) stay in localStorage.
|
// toggles (quickUnlockEnabled, autofillEnabled) stay in localStorage.
|
||||||
const SYNCED_SETTING_KEYS = [
|
const SYNCED_SETTING_KEYS = [
|
||||||
'theme', 'autoLock', 'askBeforeDelete', 'maskUsernames',
|
'theme', 'autoLock', 'askBeforeDelete', 'maskUsernames', 'maskNotes',
|
||||||
'compactActions', 'viewMode', 'hibpEnabled', 'showSiteOnCards',
|
'compactActions', 'viewMode', 'hibpEnabled', 'showSiteOnCards',
|
||||||
'sortBy', 'sortDir', 'pageSize',
|
'sortBy', 'sortDir', 'pageSize',
|
||||||
// Hotkey combos are user preferences — values are portable. The
|
// 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 'autoLock': localStorage.setItem('autoLockMin', String(v)); break;
|
||||||
case 'askBeforeDelete': localStorage.setItem('askBeforeDelete', v ? '1' : '0'); break;
|
case 'askBeforeDelete': localStorage.setItem('askBeforeDelete', v ? '1' : '0'); break;
|
||||||
case 'maskUsernames': localStorage.setItem('maskUsernames', 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 'compactActions': localStorage.setItem('compactActions', v ? '1' : '0'); break;
|
||||||
case 'viewMode': localStorage.setItem('viewMode', v); break;
|
case 'viewMode': localStorage.setItem('viewMode', v); break;
|
||||||
case 'hibpEnabled': localStorage.setItem('hibpEnabled', v ? '1' : '0'); break;
|
case 'hibpEnabled': localStorage.setItem('hibpEnabled', v ? '1' : '0'); break;
|
||||||
|
|||||||
Reference in New Issue
Block a user