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>
This commit is contained in:
908
docs/superpowers/plans/2026-04-24-relicario-gen-ux-redesign.md
Normal file
908
docs/superpowers/plans/2026-04-24-relicario-gen-ux-redesign.md
Normal file
@@ -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 `<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.
|
||||||
Reference in New Issue
Block a user