From 3c0f8d2c5c18d4729ac1477e80a6f9bcbf5316c8 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 24 Apr 2026 23:13:43 -0400 Subject: [PATCH] =?UTF-8?q?docs(plan):=20generator=20UX=20redesign=20?= =?UTF-8?q?=E2=80=94=20inline=20panel=20+=20=E2=9C=A8=20trigger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4 tasks, ~3 commits. Task 1 polishes labels (lowercase + gold *). Task 2 git-mvs the popover module to generator-panel. Task 3 rewrites the panel with new API (parent + trigger + context), updates both callers (login.ts, settings-vault.ts) for ✨ + inline mount, swaps CSS, adapts existing tests + adds 3 new ones (aria-expanded, auto-gen, Escape). Task 4 verifies build + tests + manual smoke. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-04-24-relicario-gen-ux-redesign.md | 908 ++++++++++++++++++ 1 file changed, 908 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-24-relicario-gen-ux-redesign.md diff --git a/docs/superpowers/plans/2026-04-24-relicario-gen-ux-redesign.md b/docs/superpowers/plans/2026-04-24-relicario-gen-ux-redesign.md new file mode 100644 index 0000000..da697cc --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-relicario-gen-ux-redesign.md @@ -0,0 +1,908 @@ +# Generator UX Redesign Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the right-anchored popover (which clips off the popup edge) with an inline panel injected into the form below the password row. Trigger becomes ✨; lowercase form labels with a gold required-marker. + +**Architecture:** The popover module gets renamed (`generator-popover.ts` → `generator-panel.ts`) and rewritten: same knob → message logic, but DOM mounts inside a passed parent element instead of `document.body`, and the action row varies by context (`fill-field` for the login form's password input, `configure-defaults` for the vault settings screen). Label polish is a single CSS rule update plus an `` wrap around the `*` markers in 6 type forms. + +**Tech Stack:** TypeScript, vitest, webpack, plain CSS (no preprocessor). + +**Spec:** `docs/superpowers/specs/2026-04-24-relicario-gen-ux-redesign-design.md` (commit `9add305`). + +--- + +## Task 1: Label polish — lowercase + gold required marker + +**Files:** +- Modify: `extension/src/popup/styles.css` (the `.label` rule + add `.req` rule) +- Modify: `extension/src/popup/components/types/login.ts` (1 markup change at line ~234) +- Modify: `extension/src/popup/components/types/identity.ts` (1 markup change at line ~129) +- Modify: `extension/src/popup/components/types/card.ts` (1 markup change at line ~169) +- Modify: `extension/src/popup/components/types/key.ts` (2 markup changes at lines ~118, ~120) +- Modify: `extension/src/popup/components/types/totp.ts` (2 markup changes at lines ~208, ~217) +- Modify: `extension/src/popup/components/types/secure-note.ts` (1 markup change at line ~107) + +Working dir: `/home/alee/Sources/relicario`. Branch: main. Do NOT push. + +- [ ] **Step 1: Update the `.label` rule** + +In `extension/src/popup/styles.css`, find the `.label {` block (around line 36-45) and change `text-transform`, `letter-spacing`, and `font-weight`: + +Old: +```css +.label { + font-size: 11px; + font-weight: 600; + color: #8b949e; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 4px; +} +``` + +New: +```css +.label { + font-size: 11px; + font-weight: 500; + color: #8b949e; + text-transform: lowercase; + letter-spacing: 0.02em; + margin-bottom: 4px; +} +``` + +- [ ] **Step 2: Add the `.req` rule for gold required-marker** + +Append this rule directly after the `.label` rule (so it's adjacent and easy to find): + +```css +.label .req { + color: #aa812a; + margin-left: 2px; + font-weight: 600; +} +``` + +- [ ] **Step 3: Update markup in all 6 type forms** + +For each of the 7 occurrences of `title *`, `key material *`, `secret (base32) *`, etc., replace the literal `*` with `*`. + +Run a sed sweep across the 6 type files (preserves all other content, swaps just the trailing `*` pattern): + +```bash +sed -i 's| \*| *|g' \ + extension/src/popup/components/types/login.ts \ + extension/src/popup/components/types/identity.ts \ + extension/src/popup/components/types/card.ts \ + extension/src/popup/components/types/key.ts \ + extension/src/popup/components/types/totp.ts \ + extension/src/popup/components/types/secure-note.ts +``` + +- [ ] **Step 4: Verify the swap landed in every expected file** + +```bash +grep -rn '\*' extension/src/popup/components/types/ +``` + +Expected: 8 hits across 6 files (login×1, identity×1, card×1, key×2, totp×2, secure-note×1). + +```bash +grep -rn ' \*' extension/src/popup/components/types/ +``` + +Expected: no output (every literal `*` should now be wrapped). + +- [ ] **Step 5: Run vitest** + +```bash +cd extension && bun run test 2>&1 | tail -3 +``` + +Expected: 124 passed (some test fixtures may render label HTML — verify they don't have hard-coded assertions on the literal `*` text or the `text-transform: uppercase` style. If any test fails on a label assertion, update the test to match the new markup). + +- [ ] **Step 6: Type-check** + +```bash +cd extension && bunx tsc --noEmit +``` + +Expected: zero errors. + +- [ ] **Step 7: Commit** + +```bash +cd /home/alee/Sources/relicario +git add extension/src/popup/styles.css \ + extension/src/popup/components/types/login.ts \ + extension/src/popup/components/types/identity.ts \ + extension/src/popup/components/types/card.ts \ + extension/src/popup/components/types/key.ts \ + extension/src/popup/components/types/totp.ts \ + extension/src/popup/components/types/secure-note.ts +git commit -m "$(cat <<'EOF' +feat(ext/popup): lowercase form labels + gold required marker + +.label drops text-transform: uppercase and tightens letter-spacing. +The `*` required marker gets wrapped in so it +picks up the gold accent color (matches palette refresh). + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 2: Rename module — `generator-popover` → `generator-panel` + +**Files (rename via git-mv):** +- Rename: `extension/src/popup/components/generator-popover.ts` → `generator-panel.ts` +- Rename: `extension/src/popup/components/__tests__/generator-popover.test.ts` → `generator-panel.test.ts` + +**Files modified (import path update only — function names stay the same in this task):** +- `extension/src/popup/components/types/login.ts` (line 17 import) +- `extension/src/popup/components/settings-vault.ts` (line 9 import) + +Working dir: `/home/alee/Sources/relicario`. Branch: main. Do NOT push. + +- [ ] **Step 1: git-mv source + test** + +```bash +cd /home/alee/Sources/relicario +git mv extension/src/popup/components/generator-popover.ts \ + extension/src/popup/components/generator-panel.ts +git mv extension/src/popup/components/__tests__/generator-popover.test.ts \ + extension/src/popup/components/__tests__/generator-panel.test.ts +``` + +- [ ] **Step 2: Update the test file's import path** + +Edit `extension/src/popup/components/__tests__/generator-panel.test.ts` line 8: + +Old: +```ts +import { openGeneratorPopover, closeGeneratorPopover } from '../generator-popover'; +``` + +New: +```ts +import { openGeneratorPopover, closeGeneratorPopover } from '../generator-panel'; +``` + +(Only the path string changes; function names stay untouched in this task.) + +- [ ] **Step 3: Update login.ts import path** + +Edit `extension/src/popup/components/types/login.ts` line 17: + +Old: +```ts +import { openGeneratorPopover, closeGeneratorPopover } from '../generator-popover'; +``` + +New: +```ts +import { openGeneratorPopover, closeGeneratorPopover } from '../generator-panel'; +``` + +- [ ] **Step 4: Update settings-vault.ts import path** + +Edit `extension/src/popup/components/settings-vault.ts` line 9: + +Old: +```ts +import { openGeneratorPopover } from './generator-popover'; +``` + +New: +```ts +import { openGeneratorPopover } from './generator-panel'; +``` + +- [ ] **Step 5: Verify no stale references to `generator-popover` exist** + +```bash +grep -rn "generator-popover" extension/src/ +``` + +Expected: no output (all imports updated). + +- [ ] **Step 6: Run vitest** + +```bash +cd extension && bun run test 2>&1 | tail -3 +``` + +Expected: 124 passed (no behavioral change — just file rename). + +- [ ] **Step 7: Type-check** + +```bash +cd extension && bunx tsc --noEmit +``` + +Expected: zero errors. + +- [ ] **Step 8: Commit** + +```bash +cd /home/alee/Sources/relicario +git add extension/src/popup/components/generator-panel.ts \ + extension/src/popup/components/generator-popover.ts \ + extension/src/popup/components/__tests__/generator-panel.test.ts \ + extension/src/popup/components/__tests__/generator-popover.test.ts \ + extension/src/popup/components/types/login.ts \ + extension/src/popup/components/settings-vault.ts +git commit -m "$(cat <<'EOF' +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) +EOF +)" +``` + +--- + +## Task 3: Rewrite panel module + new CSS + caller updates + new tests + +**Files:** +- Modify: `extension/src/popup/components/generator-panel.ts` (major rewrite — new API, inline mount, escape handler) +- Modify: `extension/src/popup/components/__tests__/generator-panel.test.ts` (function rename + parent mount + 3 new tests) +- Modify: `extension/src/popup/styles.css` (delete `.generator-popover` rules; add `.gen-trigger` + `.gen-panel` rules) +- Modify: `extension/src/popup/components/types/login.ts` (✨ trigger button + new openGeneratorPanel call with `context: 'fill-field'`) +- Modify: `extension/src/popup/components/settings-vault.ts` (✨ trigger button + new openGeneratorPanel call with `context: 'configure-defaults'`) + +Working dir: `/home/alee/Sources/relicario`. Branch: main. Do NOT push. + +This is the largest task. Steps walk through each file. + +### Step 1: Rewrite `generator-panel.ts` + +Read the current file first (Read tool) to understand the existing helper functions (`knobsFromRequest`, `requestFromKnobs`, `buildInnerHtml`, `wireInner`, `updateValidation`). KEEP those helpers AS-IS — they encode the knob→GeneratorRequest mapping which is correct. The rewrite only changes: + +1. Function rename: `openGeneratorPopover` → `openGeneratorPanel`. Same for `closeGeneratorPopover` → `closeGeneratorPanel`. +2. New options interface (replaces `OpenPopoverOpts`): + +```ts +export type GeneratorPanelContext = 'fill-field' | 'configure-defaults'; + +export interface OpenPanelOpts { + parent: HTMLElement; // mount target (form root or settings section) + trigger: HTMLElement; // ✨ button (aria-expanded gets toggled here) + initial: GeneratorRequest; + context: GeneratorPanelContext; + onPicked?: (value: string) => void; // required when context === 'fill-field' +} +``` + +3. The `host` div is appended to `opts.parent` instead of `document.body`. Drop the `position: absolute / top / left` styling — just `parent.appendChild(host)`. +4. The trigger gets `aria-expanded="true"` on open, `"false"` on close. +5. Escape key closes the panel. Add a `document.addEventListener('keydown', escHandler)` on open; remove on close. Handler: + ```ts + const escHandler = (e: KeyboardEvent): void => { + if (e.key === 'Escape') closeGeneratorPanel(); + }; + ``` +6. Auto-generate on open: call `render()` then immediately `refreshPreview()` (the existing render does this already in the current popover — confirm it still does in the rewrite). +7. Action row varies by context. Two HTML branches: + - `context === 'fill-field'`: ` ` + - `context === 'configure-defaults'`: `` (no cancel/use) +8. Clicking ✨ while panel open should close it. The trigger's click handler in the caller (login.ts / settings-vault.ts) checks `if (isGeneratorPanelOpen()) closeGeneratorPanel(); else openGeneratorPanel(...)`. Add `export function isGeneratorPanelOpen(): boolean { return activePanel !== null; }`. +9. The "more ▾" disclosure: render only for `random` mode (BIP39 has no advanced knobs after the redesign). For `random`, advanced contains the `symbolCharset` toggle. Use `
` element for natural disclosure semantics: + ```html +
+ more ▾ +
+ +
+
+ ``` +10. Element IDs that the existing tests assert on MUST be preserved verbatim: `#gen-kind-random`, `#gen-kind-bip39`, `#gen-length`, `#gen-lower`, `#gen-upper`, `#gen-digits`, `#gen-symbols`, `#gen-use`, `#gen-save-default`. The HTML structure can change, but these IDs stay. +11. The `closeGeneratorPanel` function must clear: + - `activePanel = null` + - The `host` element from its parent (host.remove()) + - `aria-expanded="false"` on the trigger + - `document.removeEventListener('keydown', escHandler)` + - Any pending debounce timer + +The full new `openGeneratorPanel` skeleton (use this as the structure; fill in the helper-function calls from the existing module which you keep unchanged): + +```ts +let activePanel: { + host: HTMLElement; + trigger: HTMLElement; + cleanup: () => void; +} | null = null; + +let debounceTimer: ReturnType | null = null; + +export function openGeneratorPanel(opts: OpenPanelOpts): void { + closeGeneratorPanel(); + + const knobs = knobsFromRequest(opts.initial); + let currentPreview = ''; + + const host = document.createElement('div'); + host.className = 'gen-panel'; + opts.parent.appendChild(host); + + opts.trigger.setAttribute('aria-expanded', 'true'); + + const escHandler = (e: KeyboardEvent): void => { + if (e.key === 'Escape') closeGeneratorPanel(); + }; + document.addEventListener('keydown', escHandler); + + const cleanup = (): void => { + document.removeEventListener('keydown', escHandler); + if (debounceTimer !== null) { + clearTimeout(debounceTimer); + debounceTimer = null; + } + opts.trigger.setAttribute('aria-expanded', 'false'); + host.remove(); + }; + + activePanel = { host, trigger: opts.trigger, cleanup }; + + const render = (): void => { + host.innerHTML = buildInnerHtml(knobs, opts.context); + wireInner(opts); + refreshPreview(); + }; + + const refreshPreview = (): void => { + /* existing debounced refresh logic — copy from current module */ + }; + + /* wireInner needs `opts` for context (action row composition) and onPicked callback */ + + render(); +} + +export function closeGeneratorPanel(): void { + if (activePanel === null) return; + activePanel.cleanup(); + activePanel = null; +} + +export function isGeneratorPanelOpen(): boolean { + return activePanel !== null; +} +``` + +Update `buildInnerHtml(knobs, context)` to: +- Use `
` for the disclosure +- Render the action row based on `context` +- Use the new `.gen-panel` child class names (no more `.gen-row`, `.gen-row__label`, etc. — see new CSS in Step 2) + +Keep `wireInner` as a closure-scoped helper inside `openGeneratorPanel` (NOT a parameter-taking function — it gets direct access to `opts`, `knobs`, `host`, `currentPreview` via the parent scope, just like the current popover does). Update its body to wire: +- `#gen-use` click → `opts.onPicked?.(currentPreview); closeGeneratorPanel();` +- `#gen-cancel` click → `closeGeneratorPanel();` +- `#gen-save-default` click → existing logic (fetch settings, update with new defaults, send `update_vault_settings`); on success append a `✓ saved` to the save-link button and remove it after 1500 ms via `setTimeout`. Skeleton: + +```ts +document.getElementById('gen-save-default')?.addEventListener('click', async () => { + const link = host.querySelector('#gen-save-default') as HTMLElement; + /* fetch settings, write generator_defaults, send update_vault_settings */ + const settingsResp = await sendMessage({ type: 'get_vault_settings' }); + if (!settingsResp.ok) return; + const settings = (settingsResp.data as { settings: VaultSettings }).settings; + settings.generator_defaults = requestFromKnobs(knobs); + const updateResp = await sendMessage({ type: 'update_vault_settings', settings }); + if (!updateResp.ok) return; + /* append + auto-remove toast */ + link.querySelector('.save-link__toast')?.remove(); + const toast = document.createElement('span'); + toast.className = 'save-link__toast'; + toast.textContent = '✓ saved'; + link.appendChild(toast); + setTimeout(() => toast.remove(), 1500); +}); +``` + +Apply this rewrite. The full file should still be ~250-350 lines; structure stays similar to the current popover. + +### Step 2: Replace popover CSS with panel CSS in `styles.css` + +Find the current `/* --- generator popover (β₂ slice 4) --- */` section (around line 592) and the `.gen-preview-line` rule below it. DELETE the entire block of `.generator-popover` rules (~80 lines). + +Add this new block in the same location: + +```css +/* --- generator panel (gen-UX redesign) --- */ + +.gen-trigger { + background: #7c5719; + color: #fff3cf; + border: none; + border-radius: 4px; + padding: 0 12px; + font-size: 16px; + cursor: pointer; + line-height: 1; + min-width: 38px; + display: inline-flex; + align-items: center; + justify-content: center; +} +.gen-trigger:hover { background: #aa812a; } +.gen-trigger[aria-expanded="true"] { background: #aa812a; } + +.gen-panel { + background: #161b22; + border: 1px solid #aa812a; + border-radius: 6px; + padding: 11px; + margin: 6px 0; + font-size: 11px; + color: #c9d1d9; +} +.gen-panel .panel-toggle { + display: flex; + gap: 4px; + background: #21262d; + border-radius: 4px; + padding: 2px; + margin-bottom: 8px; +} +.gen-panel .panel-toggle button { + flex: 1; + background: transparent; + border: 0; + color: #8b949e; + padding: 5px; + font-size: 11px; + cursor: pointer; + border-radius: 3px; + font-weight: 600; +} +.gen-panel .panel-toggle button.active { + background: #aa812a; + color: #fff3cf; +} +.gen-panel .knob { + display: flex; + align-items: center; + gap: 8px; + margin: 6px 0; +} +.gen-panel .knob__label { + color: #8b949e; + width: 56px; + flex-shrink: 0; + font-size: 10px; +} +.gen-panel .knob__slider { flex: 1; } +.gen-panel .knob__value { + font-family: ui-monospace, monospace; + min-width: 24px; + text-align: right; + color: #c9d1d9; +} +.gen-panel .classes { + display: flex; + gap: 8px; + font-size: 10px; + margin: 6px 0; + flex-wrap: wrap; + color: #8b949e; +} +.gen-panel .classes label { + display: flex; + align-items: center; + gap: 3px; + user-select: none; + cursor: pointer; +} +.gen-panel .preview { + background: #0d1117; + border: 1px solid #30363d; + border-radius: 4px; + padding: 8px 10px; + margin-top: 8px; + display: flex; + align-items: center; + gap: 8px; +} +.gen-panel .preview__value { + flex: 1; + color: #f1cf6e; + font-family: ui-monospace, monospace; + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.gen-panel .preview__regen { + background: transparent; + border: 0; + color: #8b949e; + cursor: pointer; + padding: 0 4px; + font-size: 14px; +} +.gen-panel .more { + color: #8b949e; + font-size: 10px; + margin-top: 6px; + cursor: pointer; + user-select: none; + padding: 2px 0; +} +.gen-panel .more summary { + list-style: none; + outline: none; +} +.gen-panel .more summary::-webkit-details-marker { display: none; } +.gen-panel .more:hover { color: #d2ab43; } +.gen-panel .more__advanced { margin-top: 6px; } +.gen-panel .actions { + display: flex; + gap: 6px; + margin-top: 10px; + align-items: center; +} +.gen-panel .actions .save-link { + flex: 1; + background: transparent; + border: 0; + color: #8b949e; + cursor: pointer; + font-size: 10px; + text-align: left; + padding: 4px 0; + text-decoration: underline; + text-decoration-color: #30363d; + text-underline-offset: 2px; +} +.gen-panel .actions .save-link:hover { + color: #d2ab43; + text-decoration-color: #d2ab43; +} +.gen-panel .actions .save-link__toast { + color: #3fb950; + margin-left: 6px; + font-size: 10px; +} + +/* keep .gen-preview-line — it's the summary-text in vault settings, separate from panel */ +``` + +The pre-existing `.gen-preview-line` rule (around line 674) must stay — it's used by the vault-settings summary text, not the panel itself. + +### Step 3: Update `login.ts` + +Find the `gen-btn` markup (around line 243): + +Old: +```ts + +``` + +New: +```ts + +``` + +Find the click handler (around line 268): + +Old: +```ts +document.getElementById('gen-btn')?.addEventListener('click', (e) => { + const anchor = e.currentTarget as HTMLElement; + const initial = getState().generatorDefaults ?? DEFAULT_PASSWORD_REQUEST; + openGeneratorPopover({ + anchor, + initial, + onPicked: (value) => { + const pw = document.getElementById('f-password') as HTMLInputElement | null; + if (pw) { pw.value = value; pw.type = 'text'; } + }, + }); +}); +``` + +New: +```ts +document.getElementById('gen-btn')?.addEventListener('click', (e) => { + const trigger = e.currentTarget as HTMLElement; + if (isGeneratorPanelOpen()) { + closeGeneratorPanel(); + return; + } + const passwordRow = trigger.closest('.form-group') as HTMLElement | null; + if (!passwordRow) return; + const initial = getState().generatorDefaults ?? DEFAULT_PASSWORD_REQUEST; + openGeneratorPanel({ + parent: passwordRow, // panel mounts inside the password form-group + trigger, + initial, + context: 'fill-field', + onPicked: (value) => { + const pw = document.getElementById('f-password') as HTMLInputElement | null; + if (pw) { pw.value = value; pw.type = 'text'; } + }, + }); +}); +``` + +Update the import on line 17: + +Old: +```ts +import { openGeneratorPopover, closeGeneratorPopover } from '../generator-panel'; +``` + +New: +```ts +import { openGeneratorPanel, closeGeneratorPanel, isGeneratorPanelOpen } from '../generator-panel'; +``` + +### Step 4: Update `settings-vault.ts` + +Find the `configure-gen` button (around line 131): + +Old: +```ts + +``` + +New: +```ts + +``` + +Find the click handler (around line 196): + +Old: +```ts +document.getElementById('configure-gen')?.addEventListener('click', (e) => { + /* current popover open with onPicked that writes to vault settings */ + ... + openGeneratorPopover({ + anchor: e.currentTarget as HTMLElement, + initial: pendingSettings.generator_defaults, + /* ... onPicked writes to settings ... */ + }); +}); +``` + +New: +```ts +document.getElementById('configure-gen')?.addEventListener('click', (e) => { + const trigger = e.currentTarget as HTMLElement; + if (isGeneratorPanelOpen()) { + closeGeneratorPanel(); + return; + } + const generatorSection = trigger.closest('.settings-section') as HTMLElement | null; + if (!generatorSection || pendingSettings === null) return; + openGeneratorPanel({ + parent: generatorSection, + trigger, + initial: pendingSettings.generator_defaults, + context: 'configure-defaults', + }); +}); +``` + +Update the import on line 9: + +Old: +```ts +import { openGeneratorPopover } from './generator-panel'; +``` + +New: +```ts +import { openGeneratorPanel, closeGeneratorPanel, isGeneratorPanelOpen } from './generator-panel'; +``` + +### Step 5: Update tests + +In `extension/src/popup/components/__tests__/generator-panel.test.ts`, multiple changes: + +1. Update import at line 8: +```ts +import { openGeneratorPanel, closeGeneratorPanel, isGeneratorPanelOpen } from '../generator-panel'; +``` + +2. Update `setupAnchor()` to set up a parent + trigger in a way that matches the new API: +```ts +function setupMount(): { parent: HTMLElement; trigger: HTMLElement } { + document.body.innerHTML = ` +
+ +
+ `; + return { + parent: document.getElementById('parent')!, + trigger: document.getElementById('trigger')!, + }; +} +``` + +3. Update each test's `openGeneratorPopover({ anchor, ... })` to `openGeneratorPanel({ parent, trigger, context: 'fill-field', onPicked, ...})`. For the `save-as-default` test, use `context: 'fill-field'` (the save-link is shown in both contexts). For tests that don't care about onPicked, pass `vi.fn()`. + +4. Update the selector `.generator-popover` → `.gen-panel` in tests that query for the panel host element (e.g., the "opens a popover" test asserts `document.querySelector('.generator-popover')` — change to `.gen-panel`). + +5. Add 3 new tests at the end of the `describe` block: + +```ts +it('sets aria-expanded on the trigger when opened', async () => { + const { parent, trigger } = setupMount(); + expect(trigger.getAttribute('aria-expanded')).toBe('false'); + openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'fill-field', onPicked: vi.fn() }); + expect(trigger.getAttribute('aria-expanded')).toBe('true'); + closeGeneratorPanel(); + expect(trigger.getAttribute('aria-expanded')).toBe('false'); +}); + +it('auto-generates a preview on open', async () => { + const { parent, trigger } = setupMount(); + openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'fill-field', onPicked: vi.fn() }); + await new Promise((r) => setTimeout(r, 200)); + const calls = vi.mocked(sendMessage).mock.calls.filter( + ([msg]) => (msg as { type: string }).type === 'generate_password', + ); + expect(calls.length).toBeGreaterThan(0); +}); + +it('Escape key closes the panel', async () => { + const { parent, trigger } = setupMount(); + openGeneratorPanel({ parent, trigger, initial: DEFAULT_REQ, context: 'fill-field', onPicked: vi.fn() }); + await new Promise((r) => setTimeout(r, 50)); + expect(isGeneratorPanelOpen()).toBe(true); + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); + expect(isGeneratorPanelOpen()).toBe(false); + expect(document.querySelector('.gen-panel')).toBeNull(); +}); +``` + +### Step 6: Run the tests + +```bash +cd extension && bun run test 2>&1 | tail -10 +``` + +Expected: 127 passed (was 124, added 3 new tests). If a test fails: +- Selector mismatch: confirm `.gen-panel` is the new host class and tests query that, not `.generator-popover`. +- Mount target mismatch: confirm tests pass `parent`+`trigger` not `anchor`. +- Save-link selector: still `#gen-save-default` (preserved per Step 1, item 10). + +### Step 7: Type-check + +```bash +cd extension && bunx tsc --noEmit +``` + +Expected: zero errors. If errors: +- `OpenPopoverOpts` is gone; tests/callers reference must use `OpenPanelOpts`. Should be caught by the import update. +- `onPicked` is now optional in `OpenPanelOpts` — TS may complain at the call site if not passed. The `fill-field` context needs `onPicked`; configure-defaults doesn't. + +### Step 8: Build both bundles + +```bash +cd extension && bun run build:all 2>&1 | tail -10 +``` + +Expected: "compiled with 2 warnings" (WASM size only) for each of Chrome and Firefox. + +### Step 9: Commit + +```bash +cd /home/alee/Sources/relicario +git add extension/src/popup/components/generator-panel.ts \ + extension/src/popup/components/__tests__/generator-panel.test.ts \ + extension/src/popup/styles.css \ + extension/src/popup/components/types/login.ts \ + extension/src/popup/components/settings-vault.ts +git commit -m "$(cat <<'EOF' +feat(ext/popup): rewrite generator as inline panel with ✨ trigger + +The popover (which clipped off the popup edge) becomes an inline panel +that mounts inside the form (login.ts) or settings section +(settings-vault.ts). Trigger button is ✨ with aria-expanded toggling. +Action row varies by context: fill-field has cancel+use; configure- +defaults has only the save-default link. Escape key closes the panel. +Tests adapted to new API; 3 new tests for aria-expanded, auto-generate, +and Escape behavior. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 4: Build, full verification, manual smoke + +Working dir: `/home/alee/Sources/relicario`. Branch: main. + +- [ ] **Step 1: Run all test suites end to end** + +```bash +cd /home/alee/Sources/relicario && cargo test --workspace 2>&1 | grep -E "test result" | tail -20 +cd /home/alee/Sources/relicario/extension && bun run test 2>&1 | tail -5 +cd /home/alee/Sources/relicario/extension && bunx tsc --noEmit 2>&1 | tail -5 +``` + +Expected: +- Cargo: every "test result" line shows `0 failed`. Total ~155. +- Vitest: `Tests 127 passed (127)` (was 124; added 3 new generator-panel tests). +- tsc: zero output (no errors). + +- [ ] **Step 2: Build both bundles** + +```bash +cd /home/alee/Sources/relicario/extension && bun run build:all 2>&1 | tail -10 +``` + +Expected: "compiled with 2 warnings" (WASM size only) for both Chrome and Firefox bundles. + +- [ ] **Step 3: Final lint sweep — confirm no stale references to popover** + +```bash +cd /home/alee/Sources/relicario && git grep -nE 'generator-popover|generatorPopover|openGeneratorPopover|closeGeneratorPopover|\.generator-popover' -- 'extension/src/' 'extension/setup.html' +``` + +Expected: zero output. The only remaining occurrences allowed are inside markdown specs/plans (`docs/`) — these document the historical name and should NOT be modified. + +- [ ] **Step 4: Manual smoke test (relay these instructions to the user)** + +Have the user reload the extension and walk through: + + - **Login form:** Open popup → New → Login. Click ✨ button next to password input. Verify: + - Inline panel appears below the password row (not a clipped popover) + - Panel auto-fills with a generated preview immediately + - ✨ button shows gold-active state (`aria-expanded="true"`) + - Clicking length slider regenerates the preview after a brief debounce + - Toggling kind to "passphrase" switches knobs and regenerates + - "more ▾" disclosure expands to reveal symbol charset (random mode only) + - "use" button fills the password input and closes the panel + - "cancel" button closes the panel without committing + - Escape key closes the panel + - Clicking ✨ again while open closes the panel + - "↑ save these as default" link writes to vault settings (verify by reopening) + - **Vault settings:** Open ⚙ → vault settings → ✨ button next to generator preview. Verify: + - Inline panel appears inside the generator section + - No use/cancel buttons (configure-defaults context) + - "↑ save these as default" link works + - ✨ closes the panel + - **Polish:** All form labels are lowercase across all type forms. Required-field `*` markers are gold (`#aa812a`). Run through Login, SecureNote, Identity, Card, Key, TOTP forms briefly. + +- [ ] **Step 5: No close-out commit needed if all green** + +If steps 1-3 passed, the slice is complete via the prior 3 commits (label polish, rename, panel rewrite). If any fix was needed, commit as `fix(ext/popup): `. + +--- + +## Verification summary + +```bash +cd /home/alee/Sources/relicario/extension && bun run build:all +cd /home/alee/Sources/relicario && cargo test --workspace +cd /home/alee/Sources/relicario/extension && bun run test +cd /home/alee/Sources/relicario/extension && bunx tsc --noEmit +git grep -nE 'generator-popover|generatorPopover|openGeneratorPopover|closeGeneratorPopover|\.generator-popover' -- 'extension/src/' 'extension/setup.html' +``` + +All five must succeed (grep returns nothing) for the slice to be complete. + +--- + +## Notes for the implementer + +- **No worktree** — direct commits to main per project's single-maintainer flow. +- **Order matters:** Task 1 (label polish) is independent and ships first because it's harmless and doesn't depend on the panel rewrite. Task 2 (rename) MUST come before Task 3 because Task 3's commit message references `generator-panel.ts`. Task 3 must come before Task 4. +- **The `
` element** is the cleanest way to implement the "more ▾" disclosure — it's natively accessible and the CSS hides the default disclosure marker. Make sure the disclosure is conditionally rendered (only for random mode). +- **Test ID preservation:** the existing test asserts on specific element IDs (`#gen-kind-random`, `#gen-length`, `#gen-use`, `#gen-save-default`, `#gen-lower` etc.). The rewrite must keep those IDs intact, even if surrounding markup changes. Check the test file before completing the rewrite. +- **Don't add animation/transitions** — the spec explicitly defers those. Panel appears/disappears instantly. +- **Don't add click-outside-to-close** — the spec explicitly excludes it.