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

31 KiB
Raw Blame History

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.tsgenerator-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:

.label {
  font-size: 11px;
  font-weight: 600;
  color: #8b949e;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 4px;
}

New:

.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):

.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):

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
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).

grep -rn ' \*</label>' extension/src/popup/components/types/

Expected: no output (every literal *</label> should now be wrapped).

  • Step 5: Run vitest
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
cd extension && bunx tsc --noEmit

Expected: zero errors.

  • Step 7: Commit
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-popovergenerator-panel

Files (rename via git-mv):

  • Rename: extension/src/popup/components/generator-popover.tsgenerator-panel.ts
  • Rename: extension/src/popup/components/__tests__/generator-popover.test.tsgenerator-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
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:

import { openGeneratorPopover, closeGeneratorPopover } from '../generator-popover';

New:

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:

import { openGeneratorPopover, closeGeneratorPopover } from '../generator-popover';

New:

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:

import { openGeneratorPopover } from './generator-popover';

New:

import { openGeneratorPopover } from './generator-panel';
  • Step 5: Verify no stale references to generator-popover exist
grep -rn "generator-popover" extension/src/

Expected: no output (all imports updated).

  • Step 6: Run vitest
cd extension && bun run test 2>&1 | tail -3

Expected: 124 passed (no behavioral change — just file rename).

  • Step 7: Type-check
cd extension && bunx tsc --noEmit

Expected: zero errors.

  • Step 8: Commit
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: openGeneratorPopoveropenGeneratorPanel. Same for closeGeneratorPopovercloseGeneratorPanel.
  2. New options interface (replaces OpenPopoverOpts):
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'
}
  1. The host div is appended to opts.parent instead of document.body. Drop the position: absolute / top / left styling — just parent.appendChild(host).
  2. The trigger gets aria-expanded="true" on open, "false" on close.
  3. Escape key closes the panel. Add a document.addEventListener('keydown', escHandler) on open; remove on close. Handler:
    const escHandler = (e: KeyboardEvent): void => {
      if (e.key === 'Escape') closeGeneratorPanel();
    };
    
  4. 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).
  5. 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)
  6. 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; }.
  7. 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:
    <details class="more">
      <summary>more ▾</summary>
      <div class="more__advanced">
        <!-- knobs go here -->
      </div>
    </details>
    
  8. 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.
  9. 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):

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:
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:

/* --- 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:

<button class="btn" id="gen-btn" title="generate">gen</button>

New:

<button class="gen-trigger" id="gen-btn" type="button" title="generate password" aria-expanded="false"></button>

Find the click handler (around line 268):

Old:

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:

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:

import { openGeneratorPopover, closeGeneratorPopover } from '../generator-panel';

New:

import { openGeneratorPanel, closeGeneratorPanel, isGeneratorPanelOpen } from '../generator-panel';

Step 4: Update settings-vault.ts

Find the configure-gen button (around line 131):

Old:

<button class="btn" id="configure-gen">configure </button>

New:

<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:

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:

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:

import { openGeneratorPopover } from './generator-panel';

New:

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:
import { openGeneratorPanel, closeGeneratorPanel, isGeneratorPanelOpen } from '../generator-panel';
  1. Update setupAnchor() to set up a parent + trigger in a way that matches the new API:
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')!,
  };
}
  1. 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().

  2. 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).

  3. Add 3 new tests at the end of the describe block:

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

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

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

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

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
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

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
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

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.