refactor(ext/popup): rename generator-popover module to generator-panel

Pure rename via git-mv (preserves history). Function names and behavior
unchanged. Sets up the API rewrite in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-24 23:20:50 -04:00
parent 083b01aa91
commit c9cd3696ae
2 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,350 @@
/// Inline generator popover — anchored to a "gen" button, renders a
/// live preview that updates as knobs change (150ms debounce). Single
/// underlying GeneratorRequest; kind toggle swaps between Random +
/// BIP39 knob sets. Actions: use / save-as-default / reset / cancel.
import { sendMessage } from '../popup';
import type { GeneratorRequest, VaultSettings } from '../../shared/types';
interface UiKnobs {
kind: 'random' | 'bip39';
// Random
length: number;
lower: boolean;
upper: boolean;
digits: boolean;
symbols: boolean;
symbolCharset: 'safe_only' | 'extended' | 'custom';
customSymbols: string;
// BIP39
wordCount: number;
separator: string;
capitalization: 'lower' | 'upper' | 'first_of_each' | 'title' | 'mixed';
}
function knobsFromRequest(req: GeneratorRequest): UiKnobs {
const defaults: UiKnobs = {
kind: 'random',
length: 20, lower: true, upper: true, digits: true, symbols: true,
symbolCharset: 'safe_only', customSymbols: '',
wordCount: 5, separator: ' ', capitalization: 'lower',
};
if (req.kind === 'random') {
return {
...defaults,
kind: 'random',
length: req.length,
lower: req.classes.lower,
upper: req.classes.upper,
digits: req.classes.digits,
symbols: req.classes.symbols,
symbolCharset: req.symbol_charset.kind,
customSymbols: req.symbol_charset.kind === 'custom' ? req.symbol_charset.value : '',
};
}
return {
...defaults,
kind: 'bip39',
wordCount: req.word_count,
separator: req.separator,
capitalization: req.capitalization,
};
}
function requestFromKnobs(knobs: UiKnobs): GeneratorRequest {
if (knobs.kind === 'random') {
return {
kind: 'random',
length: knobs.length,
classes: {
lower: knobs.lower, upper: knobs.upper,
digits: knobs.digits, symbols: knobs.symbols,
},
symbol_charset:
knobs.symbolCharset === 'safe_only' ? { kind: 'safe_only' } :
knobs.symbolCharset === 'extended' ? { kind: 'extended' } :
{ kind: 'custom', value: knobs.customSymbols },
};
}
return {
kind: 'bip39',
word_count: knobs.wordCount,
separator: knobs.separator,
capitalization: knobs.capitalization,
};
}
let activePopover: {
host: HTMLElement;
cleanup: () => void;
} | null = null;
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
export interface OpenPopoverOpts {
anchor: HTMLElement;
initial: GeneratorRequest;
onPicked: (value: string) => void;
}
export function openGeneratorPopover(opts: OpenPopoverOpts): void {
closeGeneratorPopover();
const knobs = knobsFromRequest(opts.initial);
let currentPreview = '';
const host = document.createElement('div');
host.className = 'generator-popover';
document.body.appendChild(host);
// Position below anchor
const rect = opts.anchor.getBoundingClientRect();
host.style.top = `${rect.bottom + 6}px`;
host.style.left = `${rect.left}px`;
const render = (): void => {
host.innerHTML = buildInnerHtml(knobs);
wireInner();
refreshPreview();
};
const refreshPreview = (): void => {
if (debounceTimer !== null) clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
debounceTimer = null;
const request = requestFromKnobs(knobs);
const msg = knobs.kind === 'random'
? { type: 'generate_password' as const, request }
: { type: 'generate_passphrase' as const, request };
const resp = await sendMessage(msg);
if (resp.ok) {
const d = resp.data as { password?: string; passphrase?: string };
currentPreview = d.password ?? d.passphrase ?? '';
const el = host.querySelector('.gen-preview__value');
if (el) el.textContent = currentPreview;
updateValidation();
}
}, 150);
};
const updateValidation = (): void => {
const useBtn = host.querySelector('#gen-use') as HTMLButtonElement | null;
if (!useBtn) return;
const noClass = knobs.kind === 'random'
&& !(knobs.lower || knobs.upper || knobs.digits || knobs.symbols);
useBtn.disabled = noClass;
const note = host.querySelector('.gen-validation');
if (note) (note as HTMLElement).style.display = noClass ? 'block' : 'none';
};
const wireInner = (): void => {
host.querySelector('#gen-kind-random')?.addEventListener('click', () => {
knobs.kind = 'random'; render();
});
host.querySelector('#gen-kind-bip39')?.addEventListener('click', () => {
knobs.kind = 'bip39'; render();
});
host.querySelector('#gen-length')?.addEventListener('input', (e) => {
knobs.length = Number((e.target as HTMLInputElement).value);
const out = host.querySelector('#gen-length-val');
if (out) out.textContent = String(knobs.length);
refreshPreview();
});
for (const { id, key } of [
{ id: 'gen-lower', key: 'lower' as const },
{ id: 'gen-upper', key: 'upper' as const },
{ id: 'gen-digits', key: 'digits' as const },
{ id: 'gen-symbols', key: 'symbols' as const },
]) {
host.querySelector(`#${id}`)?.addEventListener('change', (e) => {
knobs[key] = (e.target as HTMLInputElement).checked;
updateValidation();
refreshPreview();
});
}
host.querySelectorAll<HTMLButtonElement>('[data-symbol-charset]').forEach((btn) => {
btn.addEventListener('click', () => {
knobs.symbolCharset = btn.dataset.symbolCharset as UiKnobs['symbolCharset'];
render();
});
});
host.querySelector('#gen-word-count')?.addEventListener('input', (e) => {
knobs.wordCount = Number((e.target as HTMLInputElement).value);
const out = host.querySelector('#gen-word-count-val');
if (out) out.textContent = String(knobs.wordCount);
refreshPreview();
});
host.querySelectorAll<HTMLButtonElement>('[data-separator]').forEach((btn) => {
btn.addEventListener('click', () => {
knobs.separator = btn.dataset.separator ?? ' ';
render();
});
});
host.querySelectorAll<HTMLButtonElement>('[data-capitalization]').forEach((btn) => {
btn.addEventListener('click', () => {
knobs.capitalization = btn.dataset.capitalization as UiKnobs['capitalization'];
render();
});
});
host.querySelector('.gen-preview__regen')?.addEventListener('click', () => {
refreshPreview();
});
host.querySelector('#gen-use')?.addEventListener('click', () => {
opts.onPicked(currentPreview);
closeGeneratorPopover();
});
host.querySelector('#gen-save-default')?.addEventListener('click', async () => {
const getResp = await sendMessage({ type: 'get_vault_settings' });
if (!getResp.ok) return;
const vs = (getResp.data as { settings: VaultSettings }).settings;
const updated: VaultSettings = { ...vs, generator_defaults: requestFromKnobs(knobs) };
await sendMessage({ type: 'update_vault_settings', settings: updated });
const btn = host.querySelector('#gen-save-default') as HTMLButtonElement | null;
if (btn) {
const original = btn.textContent;
btn.textContent = 'saved';
setTimeout(() => { if (btn.textContent === 'saved') btn.textContent = original; }, 1500);
}
});
host.querySelector('#gen-reset')?.addEventListener('click', async () => {
const getResp = await sendMessage({ type: 'get_vault_settings' });
if (!getResp.ok) return;
const vs = (getResp.data as { settings: VaultSettings }).settings;
Object.assign(knobs, knobsFromRequest(vs.generator_defaults));
render();
});
host.querySelector('#gen-cancel')?.addEventListener('click', () => closeGeneratorPopover());
host.querySelector('#gen-close')?.addEventListener('click', () => closeGeneratorPopover());
};
const onOutsideClick = (e: MouseEvent) => {
if (!host.contains(e.target as Node) && e.target !== opts.anchor) {
closeGeneratorPopover();
}
};
const onEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') closeGeneratorPopover();
};
const cleanup = (): void => {
document.removeEventListener('click', onOutsideClick, true);
document.removeEventListener('keydown', onEsc);
host.remove();
};
activePopover = { host, cleanup };
setTimeout(() => {
document.addEventListener('click', onOutsideClick, true);
document.addEventListener('keydown', onEsc);
}, 0);
render();
}
export function closeGeneratorPopover(): void {
if (activePopover === null) return;
if (debounceTimer !== null) { clearTimeout(debounceTimer); debounceTimer = null; }
activePopover.cleanup();
activePopover = null;
}
// --- HTML builders ---
function buildInnerHtml(knobs: UiKnobs): string {
return `
<div class="gen-header">
<span class="gen-title">generate</span>
<button type="button" id="gen-close" class="gen-close">×</button>
</div>
<div class="gen-row">
<span class="gen-row__label">kind</span>
<div class="gen-toggle-group">
<button id="gen-kind-random" class="${knobs.kind === 'random' ? 'active' : ''}">Random</button>
<button id="gen-kind-bip39" class="${knobs.kind === 'bip39' ? 'active' : ''}">BIP39</button>
</div>
</div>
${knobs.kind === 'random' ? buildRandomKnobs(knobs) : buildBip39Knobs(knobs)}
<div class="gen-preview">
<span class="gen-preview__value"></span>
<button type="button" class="gen-preview__regen" title="regenerate">↻</button>
</div>
${knobs.kind === 'random'
? `<p class="gen-validation" style="display:none;color:#ab2b20;font-size:10px;margin:4px 0 0;">pick at least one character class</p>`
: ''}
<div class="gen-actions">
<button type="button" class="btn" id="gen-reset">reset to defaults</button>
<button type="button" class="btn" id="gen-save-default">save as default</button>
<button type="button" class="btn" id="gen-cancel">cancel</button>
<button type="button" class="btn btn-primary" id="gen-use">use this value</button>
</div>
`;
}
function buildRandomKnobs(k: UiKnobs): string {
return `
<div class="gen-row">
<span class="gen-row__label">length</span>
<input type="range" id="gen-length" min="8" max="64" value="${k.length}" class="gen-slider">
<span id="gen-length-val">${k.length}</span>
</div>
<div class="gen-check-grid">
<label><input type="checkbox" id="gen-lower" ${k.lower ? 'checked' : ''}> lowercase</label>
<label><input type="checkbox" id="gen-digits" ${k.digits ? 'checked' : ''}> digits</label>
<label><input type="checkbox" id="gen-upper" ${k.upper ? 'checked' : ''}> uppercase</label>
<label><input type="checkbox" id="gen-symbols" ${k.symbols ? 'checked' : ''}> symbols</label>
</div>
<div class="gen-row">
<span class="gen-row__label">symbols</span>
<div class="gen-toggle-group">
<button data-symbol-charset="safe_only" class="${k.symbolCharset === 'safe_only' ? 'active' : ''}">safe</button>
<button data-symbol-charset="extended" class="${k.symbolCharset === 'extended' ? 'active' : ''}">extended</button>
</div>
</div>
`;
}
function buildBip39Knobs(k: UiKnobs): string {
const sepChip = (label: string, sep: string) => `
<button data-separator="${sep}" class="${k.separator === sep ? 'active' : ''}">${label}</button>
`;
const capChip = (label: string, val: string) => `
<button data-capitalization="${val}" class="${k.capitalization === val ? 'active' : ''}">${label}</button>
`;
return `
<div class="gen-row">
<span class="gen-row__label">words</span>
<input type="range" id="gen-word-count" min="3" max="12" value="${k.wordCount}" class="gen-slider">
<span id="gen-word-count-val">${k.wordCount}</span>
</div>
<div class="gen-row">
<span class="gen-row__label">separator</span>
<div class="gen-toggle-group">
${sepChip('space', ' ')}
${sepChip('-', '-')}
${sepChip('_', '_')}
${sepChip('.', '.')}
${sepChip(':', ':')}
</div>
</div>
<div class="gen-row">
<span class="gen-row__label">case</span>
<div class="gen-toggle-group">
${capChip('lower', 'lower')}
${capChip('upper', 'upper')}
${capChip('first', 'first_of_each')}
${capChip('title', 'title')}
</div>
</div>
`;
}