feat(ext/popup/item-detail): colorize revealed password field

Add data-field-kind attribute to renderConcealedRow so wireFieldHandlers
can distinguish password fields from other concealed rows (TOTP secrets,
CVV, PIN, private keys). Apply colorizePassword() on reveal when kind is
"password"; plain textContent otherwise. Pass kind through renderSections
for custom-section password fields.
This commit is contained in:
adlee-was-taken
2026-05-02 18:49:56 -04:00
parent f45c275566
commit 6bca0b3526
2 changed files with 16 additions and 4 deletions

View File

@@ -6,6 +6,7 @@
/// copy click handlers on any rendered rows. /// copy click handlers on any rendered rows.
import { escapeHtml } from '../../shared/state'; import { escapeHtml } from '../../shared/state';
import { colorizePassword } from '../../shared/password-coloring';
import type { Item, Section, Field, FieldValue } from '../../shared/types'; import type { Item, Section, Field, FieldValue } from '../../shared/types';
export interface RowOpts { export interface RowOpts {
@@ -46,6 +47,7 @@ export interface ConcealedRowOpts {
id: string; id: string;
label: string; label: string;
value: string; value: string;
kind?: 'password' | 'concealed';
monospace?: boolean; monospace?: boolean;
multiline?: boolean; multiline?: boolean;
} }
@@ -53,12 +55,15 @@ export interface ConcealedRowOpts {
/// Concealed row — value rendered hidden until the user clicks "show". /// Concealed row — value rendered hidden until the user clicks "show".
/// Plaintext is stored in `data-field-value` on the row element and copied /// Plaintext is stored in `data-field-value` on the row element and copied
/// to the visible value span on reveal. Copy button always copies plaintext. /// to the visible value span on reveal. Copy button always copies plaintext.
/// When `kind` is "password", wireFieldHandlers applies colorizePassword on
/// reveal so digits/symbols/letters are rendered in distinct colours.
export function renderConcealedRow(opts: ConcealedRowOpts): string { export function renderConcealedRow(opts: ConcealedRowOpts): string {
const { id, label, value, monospace, multiline } = opts; const { id, label, value, kind, monospace, multiline } = opts;
const placeholder = multiline ? `•••• (${value.length} chars)` : '••••'; const placeholder = multiline ? `•••• (${value.length} chars)` : '••••';
const valueClass = `field-row__value${monospace ? ' monospace' : ''}`; const valueClass = `field-row__value${monospace ? ' monospace' : ''}`;
const kindAttr = kind ? ` data-field-kind="${escapeHtml(kind)}"` : '';
return ` return `
<div class="field-row" data-field-id="${escapeHtml(id)}" data-revealed="false" data-field-value="${escapeHtml(value)}" data-field-multiline="${multiline ? 'true' : 'false'}"> <div class="field-row" data-field-id="${escapeHtml(id)}" data-revealed="false" data-field-value="${escapeHtml(value)}" data-field-multiline="${multiline ? 'true' : 'false'}"${kindAttr}>
<span class="field-row__label">${escapeHtml(label)}</span> <span class="field-row__label">${escapeHtml(label)}</span>
<span class="${valueClass}" data-field-role="value">${escapeHtml(placeholder)}</span> <span class="${valueClass}" data-field-role="value">${escapeHtml(placeholder)}</span>
<span class="field-row__actions"> <span class="field-row__actions">
@@ -101,7 +106,13 @@ export function wireFieldHandlers(scope: HTMLElement): void {
row.setAttribute('data-revealed', 'false'); row.setAttribute('data-revealed', 'false');
btn.textContent = 'show'; btn.textContent = 'show';
} else { } else {
valueEl.textContent = plaintext; const isPassword = row.getAttribute('data-field-kind') === 'password';
valueEl.textContent = '';
if (isPassword) {
valueEl.appendChild(colorizePassword(plaintext));
} else {
valueEl.textContent = plaintext;
}
row.setAttribute('data-revealed', 'true'); row.setAttribute('data-revealed', 'true');
btn.textContent = 'hide'; btn.textContent = 'hide';
} }
@@ -150,6 +161,7 @@ export function renderSections(item: Item, idPrefix: string): string {
id: `${idPrefix}-s${sIdx}-f${fIdx}`, id: `${idPrefix}-s${sIdx}-f${fIdx}`,
label: field.label, label: field.label,
value: field.value.value, value: field.value.value,
kind: field.value.kind,
}); });
} }
}); });

View File

@@ -84,7 +84,7 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise<void>
${renderSignatureBlock({ accent: 'gold', children: sigInner })} ${renderSignatureBlock({ accent: 'gold', children: sigInner })}
</div> </div>
${username ? renderRow({ label: 'username', value: username, copyable: true }) : ''} ${username ? renderRow({ label: 'username', value: username, copyable: true }) : ''}
${renderConcealedRow({ id: 'login-password', label: 'password', value: password })} ${renderConcealedRow({ id: 'login-password', label: 'password', value: password, kind: 'password' })}
${url ? renderRow({ label: 'url', value: url, href: url }) : ''} ${url ? renderRow({ label: 'url', value: url, href: url }) : ''}
${hasTotp ? ` ${hasTotp ? `
<div class="field-row"> <div class="field-row">