Files
relicario/docs/superpowers/plans/2026-04-24-relicario-gen-ux-redesign.md
adlee-was-taken 3c0f8d2c5c docs(plan): generator UX redesign — inline panel + trigger
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) <noreply@anthropic.com>
2026-04-24 23:13:43 -04:00

909 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `<span class="req">` 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 *</label>`, `key material *</label>`, `secret (base32) *</label>`, etc., replace the literal `*` with `<span class="req">*</span>`.
Run a sed sweep across the 6 type files (preserves all other content, swaps just the trailing `*</label>` pattern):
```bash
sed -i 's| \*</label>| <span class="req">*</span></label>|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 '<span class="req">\*</span></label>' 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 ' \*</label>' extension/src/popup/components/types/
```
Expected: no output (every literal `*</label>` 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 <span class="req"> so it
picks up the gold accent color (matches palette refresh).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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'`: `<button class="save-link" id="gen-save-default">↑ save these as default</button> <button class="btn" id="gen-cancel">cancel</button> <button class="btn btn-primary" id="gen-use">use</button>`
- `context === 'configure-defaults'`: `<button class="save-link" id="gen-save-default">↑ save these as default</button>` (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 `<details>` element for natural disclosure semantics:
```html
<details class="more">
<summary>more ▾</summary>
<div class="more__advanced">
<!-- knobs go here -->
</div>
</details>
```
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<typeof setTimeout> | 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 `<details class="more">` 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 `<span class="save-link__toast">✓ saved</span>` 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
<button class="btn" id="gen-btn" title="generate">gen</button>
```
New:
```ts
<button class="gen-trigger" id="gen-btn" type="button" title="generate password" aria-expanded="false">✨</button>
```
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
<button class="btn" id="configure-gen">configure ▾</button>
```
New:
```ts
<button class="gen-trigger" id="configure-gen" type="button" title="configure generator defaults" aria-expanded="false">✨</button>
```
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 = `
<div id="parent">
<button id="trigger" aria-expanded="false">✨</button>
</div>
`;
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) <noreply@anthropic.com>
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): <description>`.
---
## 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 `<details>` 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.