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:
@@ -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">
|
||||||
@@ -100,8 +105,14 @@ export function wireFieldHandlers(scope: HTMLElement): void {
|
|||||||
valueEl.textContent = placeholder;
|
valueEl.textContent = placeholder;
|
||||||
row.setAttribute('data-revealed', 'false');
|
row.setAttribute('data-revealed', 'false');
|
||||||
btn.textContent = 'show';
|
btn.textContent = 'show';
|
||||||
|
} else {
|
||||||
|
const isPassword = row.getAttribute('data-field-kind') === 'password';
|
||||||
|
valueEl.textContent = '';
|
||||||
|
if (isPassword) {
|
||||||
|
valueEl.appendChild(colorizePassword(plaintext));
|
||||||
} else {
|
} else {
|
||||||
valueEl.textContent = plaintext;
|
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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user