From d8643b96d15b689c22c8be45c3f672912a622b6e Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Tue, 28 Jul 2026 23:06:39 +0100 Subject: [PATCH] feat(entries): multiline Notes field for any entry, masked by default The note body of a note entry is the encrypted_password column, so logins had no way to carry notes. Rather than a dedicated encrypted column and the seven call sites that come with it, this rides on custom_fields, which rotation, duplicate, export/import and sync already carry. Both the open-time normalisation and the save-time rebuild drop any key they do not name, so multiline is carried explicitly in each - otherwise the field silently reopens single-line. --- css/style.css | 12 +++++++++- js/app.js | 63 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/css/style.css b/css/style.css index 3ffe5e8..260e9b5 100644 --- a/css/style.css +++ b/css/style.css @@ -1601,6 +1601,15 @@ input[type="range"]::-webkit-slider-thumb { padding: 4px 8px; font-size: 12px; } +/* Multiline custom field (Notes). The row stays a grid, so the textarea just + grows in its own column and the buttons keep their places. */ +.so-custom-multiline { + min-height: 72px; + resize: vertical; + align-self: stretch; + font-family: inherit; + line-height: 1.5; +} /* Editable combobox for option-backed custom fields (card brand, expiry year…). Input fills the cell; the arrow drops a menu of ALL options. */ .so-combo { position: relative; display: flex; align-items: center; } @@ -1875,7 +1884,8 @@ input[type="range"]::-webkit-slider-thumb { } /* 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-body.is-masked, +.so-custom-multiline.is-masked { -webkit-text-security: disc; } .so-note-label { display: flex; align-items: center; justify-content: space-between; } /* ---- + New dropdown ---------------------------------- */ diff --git a/js/app.js b/js/app.js index c8af3de..258ae35 100644 --- a/js/app.js +++ b/js/app.js @@ -3983,6 +3983,10 @@ async function openSlideOver(id, opts) { }; if (Array.isArray(f.options) && f.options.length > 0) out.options = f.options.slice(); + // Carried explicitly: this normalisation drops every key it does not + // name, so a multiline field would silently come back single-line on + // the next open. + if (f.multiline) out.multiline = true; return out; }); soState.originalCustomJson = JSON.stringify(soState.customFields); @@ -4128,6 +4132,31 @@ function soCustomFieldsField() { }, 0); }); addWrap.appendChild(addBtn); + + // Notes for a login: a multiline custom field rather than a column of its + // own. The note body of a note entry IS encrypted_password, so a dedicated + // notes column would mean a new encrypted field and the seven call sites + // CLAUDE.md lists — while custom_fields already rides along with rotation, + // duplicate, export/import and sync for free. Secret by default. + const noteBtn = el('button', { class: 'btn btn-ghost btn-sm', type: 'button' }); + noteBtn.appendChild(icon('i-edit')); + noteBtn.appendChild(document.createTextNode(' Add note')); + noteBtn.addEventListener('click', ev => { + ev.stopPropagation(); + soState.customFields = soState.customFields || []; + soState.customFields.push({ + label: 'Notes', value: '', is_secret: true, multiline: true, + }); + renderRows(); + soDirtyCheck(); + setTimeout(() => { + const areas = list.querySelectorAll('.so-custom-multiline'); + const last = areas[areas.length - 1]; + if (last) last.focus(); + }, 0); + }); + addWrap.appendChild(noteBtn); + wrap.appendChild(addWrap); return wrap; } @@ -4154,19 +4183,31 @@ function buildCustomFieldRow(field, idx, rerender) { let valueInput; let valueSlot; // what actually goes into the row (input or combo wrap) const hasOptions = Array.isArray(field.options) && field.options.length > 0; - valueInput = el('input', { - type: field.is_secret ? 'password' : 'text', - class: 'so-input so-custom-value', - placeholder: hasOptions ? 'Pick or type…' : 'Value', - autocomplete: field.is_secret ? 'new-password' : 'off', - spellcheck: 'false', - }); + // Multiline fields (Notes) are a