('[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 `
+
+
+
kind
+
+
+
+
+
+ ${knobs.kind === 'random' ? buildRandomKnobs(knobs) : buildBip39Knobs(knobs)}
+
+
+
+
+ ${knobs.kind === 'random'
+ ? `pick at least one character class
`
+ : ''}
+
+
+
+
+
+
+ `;
+}
+
+function buildRandomKnobs(k: UiKnobs): string {
+ return `
+
+ length
+
+ ${k.length}
+
+
+
+
+
+
+
+
+
symbols
+
+
+
+
+
+ `;
+}
+
+function buildBip39Knobs(k: UiKnobs): string {
+ const sepChip = (label: string, sep: string) => `
+
+ `;
+ const capChip = (label: string, val: string) => `
+
+ `;
+ return `
+
+ words
+
+ ${k.wordCount}
+
+
+
separator
+
+ ${sepChip('space', ' ')}
+ ${sepChip('-', '-')}
+ ${sepChip('_', '_')}
+ ${sepChip('.', '.')}
+ ${sepChip(':', ':')}
+
+
+
+
case
+
+ ${capChip('lower', 'lower')}
+ ${capChip('upper', 'upper')}
+ ${capChip('first', 'first_of_each')}
+ ${capChip('title', 'title')}
+
+
+ `;
+}
diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css
index 04e5a52..41a199e 100644
--- a/extension/src/popup/styles.css
+++ b/extension/src/popup/styles.css
@@ -588,3 +588,64 @@ textarea {
width: 100%; font-size: 11px; font-family: inherit;
}
.disclosure__body .add-section:hover { border-color: #484f58; color: #c9d1d9; }
+
+/* --- generator popover (β₂ slice 4) --- */
+.generator-popover {
+ position: absolute; z-index: 9999999;
+ background: #161b22; border: 1px solid #30363d; border-radius: 6px;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.5);
+ padding: 14px; min-width: 300px; max-width: 340px;
+ font-size: 11px; font-family: system-ui, sans-serif; color: #c9d1d9;
+}
+.generator-popover .gen-header {
+ display: flex; justify-content: space-between; align-items: center;
+ margin-bottom: 8px;
+}
+.generator-popover .gen-title { font-size: 11px; font-weight: 600; color: #8b949e; text-transform: lowercase; letter-spacing: 0.08em; }
+.generator-popover .gen-close {
+ background: transparent; border: 0; color: #8b949e; cursor: pointer;
+ font-size: 14px; padding: 2px 6px;
+}
+.generator-popover .gen-row {
+ display: flex; align-items: center; gap: 8px; margin: 6px 0;
+}
+.generator-popover .gen-row__label {
+ color: #8b949e; width: 70px; flex-shrink: 0;
+ font-size: 10px; text-transform: lowercase;
+}
+.generator-popover .gen-toggle-group {
+ display: flex; gap: 0; border: 1px solid #30363d; border-radius: 3px; overflow: hidden;
+}
+.generator-popover .gen-toggle-group button {
+ background: transparent; border: 0; color: #8b949e;
+ padding: 3px 10px; cursor: pointer; font: inherit; font-size: 10px;
+}
+.generator-popover .gen-toggle-group button.active { background: #1f6feb; color: #fff; }
+.generator-popover .gen-slider { flex: 1; }
+.generator-popover .gen-slider + span {
+ color: #c9d1d9; font-variant-numeric: tabular-nums;
+ font-family: monospace; min-width: 24px; text-align: right;
+}
+.generator-popover .gen-check-grid {
+ display: grid; grid-template-columns: 1fr 1fr;
+ gap: 4px 16px; margin: 6px 0; font-size: 11px;
+}
+.generator-popover .gen-check-grid label {
+ display: flex; align-items: center; gap: 6px;
+}
+.generator-popover .gen-preview {
+ margin: 10px 0 8px; padding: 8px 10px;
+ background: #0d1117; border: 1px solid #30363d; border-radius: 4px;
+ font-family: "SF Mono", "JetBrains Mono", monospace; color: #c9d1d9;
+ display: flex; justify-content: space-between; align-items: center; gap: 8px;
+ word-break: break-all;
+}
+.generator-popover .gen-preview__regen {
+ flex-shrink: 0; background: transparent; border: 0;
+ color: #58a6ff; cursor: pointer; font-size: 12px;
+}
+.generator-popover .gen-actions {
+ display: grid; grid-template-columns: 1fr 1fr;
+ gap: 6px; margin-top: 10px;
+}
+.generator-popover .gen-actions .btn { font-size: 11px; padding: 5px 10px; }