From a0a069c7f6a8f6c3ec37d7525bf69d0281984110 Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:47:25 +0100 Subject: [PATCH] refactor(entries): fold the secret toggle into a per-field row menu The lock button sat next to the eye and both masked the value, so they read as the same control. The eye now carries the state - cyan ring when the field is stored as secret - and flipping the flag moved into a row menu alongside Remove field, one deliberate step away from the everyday reveal. The row drops from six columns to five. The menu reuses the card kebab classes, so closing it on an outside click is already handled. --- css/style.css | 25 +++++++--------- js/app.js | 83 +++++++++++++++++++++++++++++++-------------------- 2 files changed, 61 insertions(+), 47 deletions(-) diff --git a/css/style.css b/css/style.css index f70b8c5..8ebc40e 100644 --- a/css/style.css +++ b/css/style.css @@ -1592,11 +1592,16 @@ input[type="range"]::-webkit-slider-thumb { margin-top: 6px; } .so-custom-row { + /* label | value | eye | copy | row menu */ display: grid; - grid-template-columns: 1fr 1.4fr auto auto auto auto; + grid-template-columns: 1fr 1.4fr auto auto auto; gap: 4px; align-items: center; } +/* The row menu is absolutely positioned inside its wrap; without this the + slideover clips it or it lands under the next row. */ +.so-custom-row .entry-kebab-wrap { position: relative; } +.so-custom-row .entry-kebab-menu { right: 0; } .so-custom-row .so-input { padding: 4px 8px; font-size: 12px; @@ -1644,21 +1649,13 @@ input[type="range"]::-webkit-slider-thumb { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .so-combo-item:hover { background: var(--accent); color: #fff; } -.so-custom-secret-toggle { - background: var(--bg); - border: 1px solid var(--border); - border-radius: 4px; - width: 28px; height: 28px; - cursor: pointer; - color: var(--text-dim); - font-size: 13px; - line-height: 1; - padding: 0; -} -.so-custom-secret-toggle.is-on { +/* The eye carries the secret state itself — a second toggle beside it read as + a duplicate of the reveal. Flipping the flag lives in the row menu. */ +.so-custom-row .icon-btn.is-secret { background: var(--accent-soft); color: var(--accent); - border-color: var(--accent); + border: 1px solid var(--accent); + border-radius: 4px; } .so-custom-add { margin-top: 8px; diff --git a/js/app.js b/js/app.js index 3ea3c1b..21105bf 100644 --- a/js/app.js +++ b/js/app.js @@ -4275,38 +4275,21 @@ function buildCustomFieldRow(field, idx, rerender) { if (valueInput.tagName !== 'INPUT') return; valueInput.type = valueInput.type === 'password' ? 'text' : 'password'; }); - if (!field.is_secret || (valueInput.tagName !== 'INPUT' && !field.multiline)) - eye.style.visibility = 'hidden'; - // Secret-flag toggle (chip-style) — flips both the storage flag and - // the visible input type. - const secretBtn = el('button', { - class: 'so-custom-secret-toggle' + (field.is_secret ? ' is-on' : ''), - type: 'button', - title: field.is_secret - ? 'Stored as secret — masked every time it is opened. Click to make it public.' - : 'Public field — shown in clear. Click to store it as a secret.', - }, field.is_secret ? '🔒' : '👁'); - secretBtn.addEventListener('click', ev => { - ev.stopPropagation(); - field.is_secret = !field.is_secret; - // Update this row in place. rerender() rebuilds every row from scratch - // (list.innerHTML = ''), which the user sees as the panel flashing — - // and it would drop the focus and the auto-sized height too. - secretBtn.classList.toggle('is-on', field.is_secret); - secretBtn.textContent = field.is_secret ? '🔒' : '👁'; - secretBtn.title = field.is_secret - ? 'Stored as secret — masked every time it is opened. Click to make it public.' - : 'Public field — shown in clear. Click to store it as a secret.'; + // The secret flag no longer has a button of its own: two adjacent toggles + // that both masked the value read as duplicates. The eye alone carries the + // state (cyan ring = stored as secret) and flipping it lives in the row + // menu, one deliberate step away from the everyday reveal. + function applySecretState() { + eye.classList.toggle('is-secret', !!field.is_secret); + eye.style.visibility = + (!field.is_secret || (valueInput.tagName !== 'INPUT' && !field.multiline)) + ? 'hidden' : ''; if (field.multiline) valueInput.classList.toggle('is-masked', field.is_secret); else if (valueInput.tagName === 'INPUT') valueInput.type = field.is_secret ? 'password' : 'text'; - eye.style.visibility = - (!field.is_secret || (valueInput.tagName !== 'INPUT' && !field.multiline)) - ? 'hidden' : ''; - soDirtyCheck(); - }); + } const copyBtn = el('button', { class: 'icon-btn icon-btn-sm', type: 'button', title: 'Copy value' }); @@ -4320,22 +4303,56 @@ function buildCustomFieldRow(field, idx, rerender) { toast('Copied' + clipClearSuffix()); }); - const delBtn = el('button', { class: 'icon-btn icon-btn-sm', type: 'button', - title: 'Remove field' }); - delBtn.appendChild(icon('i-x')); - delBtn.addEventListener('click', ev => { + // Row menu — reuses the card kebab classes, so the document-level + // "click outside .entry-kebab-wrap closes it" handler covers it for free. + const kebabWrap = el('div', { class: 'entry-kebab-wrap' }); + const kebabBtn = el('button', { class: 'entry-kebab', type: 'button', + title: 'Field options' }); + kebabBtn.appendChild(icon('i-more')); + const kebabMenu = el('div', { class: 'entry-kebab-menu' }); + kebabBtn.addEventListener('click', ev => { + ev.stopPropagation(); + $$('.entry-kebab-menu.is-open').forEach(m => { + if (m !== kebabMenu) m.classList.remove('is-open'); + }); + kebabMenu.classList.toggle('is-open'); + }); + + const secretItem = el('button', { class: 'kebab-item', type: 'button' }); + const paintSecretItem = () => { + secretItem.textContent = field.is_secret ? 'Make public' : 'Store as secret'; + }; + paintSecretItem(); + secretItem.addEventListener('click', ev => { + ev.stopPropagation(); + field.is_secret = !field.is_secret; + // In place, never rerender(): rebuilding the list flashes the panel and + // drops both the focus and a multiline field's auto-sized height. + applySecretState(); + paintSecretItem(); + kebabMenu.classList.remove('is-open'); + soDirtyCheck(); + }); + kebabMenu.appendChild(secretItem); + + const delItem = el('button', { class: 'kebab-item is-danger', type: 'button' }, + 'Remove field'); + delItem.addEventListener('click', ev => { ev.stopPropagation(); soState.customFields.splice(idx, 1); rerender(); soDirtyCheck(); }); + kebabMenu.appendChild(delItem); + kebabWrap.appendChild(kebabBtn); + kebabWrap.appendChild(kebabMenu); row.appendChild(labelInput); row.appendChild(valueSlot); row.appendChild(eye); - row.appendChild(secretBtn); row.appendChild(copyBtn); - row.appendChild(delBtn); + row.appendChild(kebabWrap); + applySecretState(); return row; }