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.
This commit is contained in:
r-zakarya
2026-08-01 11:47:25 +01:00
parent 6705a8b478
commit a0a069c7f6
2 changed files with 61 additions and 47 deletions
+11 -14
View File
@@ -1592,11 +1592,16 @@ input[type="range"]::-webkit-slider-thumb {
margin-top: 6px; margin-top: 6px;
} }
.so-custom-row { .so-custom-row {
/* label | value | eye | copy | row menu */
display: grid; display: grid;
grid-template-columns: 1fr 1.4fr auto auto auto auto; grid-template-columns: 1fr 1.4fr auto auto auto;
gap: 4px; gap: 4px;
align-items: center; 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 { .so-custom-row .so-input {
padding: 4px 8px; padding: 4px 8px;
font-size: 12px; font-size: 12px;
@@ -1644,21 +1649,13 @@ input[type="range"]::-webkit-slider-thumb {
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
} }
.so-combo-item:hover { background: var(--accent); color: #fff; } .so-combo-item:hover { background: var(--accent); color: #fff; }
.so-custom-secret-toggle { /* The eye carries the secret state itself — a second toggle beside it read as
background: var(--bg); a duplicate of the reveal. Flipping the flag lives in the row menu. */
border: 1px solid var(--border); .so-custom-row .icon-btn.is-secret {
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 {
background: var(--accent-soft); background: var(--accent-soft);
color: var(--accent); color: var(--accent);
border-color: var(--accent); border: 1px solid var(--accent);
border-radius: 4px;
} }
.so-custom-add { .so-custom-add {
margin-top: 8px; margin-top: 8px;
+50 -33
View File
@@ -4275,38 +4275,21 @@ function buildCustomFieldRow(field, idx, rerender) {
if (valueInput.tagName !== 'INPUT') return; if (valueInput.tagName !== 'INPUT') return;
valueInput.type = valueInput.type === 'password' ? 'text' : 'password'; 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 secret flag no longer has a button of its own: two adjacent toggles
// the visible input type. // that both masked the value read as duplicates. The eye alone carries the
const secretBtn = el('button', { // state (cyan ring = stored as secret) and flipping it lives in the row
class: 'so-custom-secret-toggle' + (field.is_secret ? ' is-on' : ''), // menu, one deliberate step away from the everyday reveal.
type: 'button', function applySecretState() {
title: field.is_secret eye.classList.toggle('is-secret', !!field.is_secret);
? 'Stored as secret — masked every time it is opened. Click to make it public.' eye.style.visibility =
: 'Public field — shown in clear. Click to store it as a secret.', (!field.is_secret || (valueInput.tagName !== 'INPUT' && !field.multiline))
}, field.is_secret ? '🔒' : '👁'); ? 'hidden' : '';
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.';
if (field.multiline) if (field.multiline)
valueInput.classList.toggle('is-masked', field.is_secret); valueInput.classList.toggle('is-masked', field.is_secret);
else if (valueInput.tagName === 'INPUT') else if (valueInput.tagName === 'INPUT')
valueInput.type = field.is_secret ? 'password' : 'text'; 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', const copyBtn = el('button', { class: 'icon-btn icon-btn-sm', type: 'button',
title: 'Copy value' }); title: 'Copy value' });
@@ -4320,22 +4303,56 @@ function buildCustomFieldRow(field, idx, rerender) {
toast('Copied' + clipClearSuffix()); toast('Copied' + clipClearSuffix());
}); });
const delBtn = el('button', { class: 'icon-btn icon-btn-sm', type: 'button', // Row menu — reuses the card kebab classes, so the document-level
title: 'Remove field' }); // "click outside .entry-kebab-wrap closes it" handler covers it for free.
delBtn.appendChild(icon('i-x')); const kebabWrap = el('div', { class: 'entry-kebab-wrap' });
delBtn.addEventListener('click', ev => { 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(); ev.stopPropagation();
soState.customFields.splice(idx, 1); soState.customFields.splice(idx, 1);
rerender(); rerender();
soDirtyCheck(); soDirtyCheck();
}); });
kebabMenu.appendChild(delItem);
kebabWrap.appendChild(kebabBtn);
kebabWrap.appendChild(kebabMenu);
row.appendChild(labelInput); row.appendChild(labelInput);
row.appendChild(valueSlot); row.appendChild(valueSlot);
row.appendChild(eye); row.appendChild(eye);
row.appendChild(secretBtn);
row.appendChild(copyBtn); row.appendChild(copyBtn);
row.appendChild(delBtn); row.appendChild(kebabWrap);
applySecretState();
return row; return row;
} }