# Account Menu and Task Dialogs 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-side account sheet with an anchored account popover plus focused profile, redeem, and logout-confirmation dialogs. **Architecture:** Keep account/profile data and API mutations in `frontend/src/app/page.tsx`, but split presentation state into `accountMenuOpen` and `activeAccountDialog`. The sidebar trigger owns the non-modal menu; the credit button and system fallbacks route directly to the redeem dialog; each modal renders only its own task and shares one focus-trapped overlay contract. **Tech Stack:** Next.js 16 App Router, React 19, TypeScript 5, vanilla CSS design tokens, Node test runner, existing Lucide icons. ## Global Constraints - Preserve the warm editorial design tokens in `frontend/DESIGN.md`; add no raw colors, arbitrary shadow recipes, or new dependencies. - The sidebar account trigger opens a popover; the upper-right credit trigger opens the redeem dialog directly. - The administrator-only `管理兑换码` action navigates to `/admin/codes` in the same tab. - `退出登录` must open a confirmation dialog; only `确认退出` signs out. - Missing-profile and insufficient-credit flows open their task dialogs directly. - Popover rows and dialog controls retain a minimum 44px target. - Support outside click where appropriate, Escape, focus trapping, focus return, keyboard navigation, and reduced motion. - Remove the former right-side account sheet markup, CSS, and overloaded state. - Do not commit or push unrelated working-tree changes. --- ### Task 1: Lock the interaction contract and design-system primitives **Files:** - Modify: `frontend/tests/starter-questions.test.ts` - Modify: `frontend/DESIGN.md` **Interfaces:** - Consumes: existing source-contract test helpers `pageSource`, `globalStyles`, and `sourceBetween`. - Produces: failing assertions for the account popover, direct redeem route, three dialogs, and removal of the sheet; documented `Account popover`, `Profile dialog`, `Redeem dialog`, and `Logout dialog` primitives. - [ ] **Step 1: Add failing structural tests** Append tests that inspect behavior-bearing names and relationships rather than prose copy: ```ts test("routes account actions through a popover and focused dialogs", () => { assert.match(pageSource, /const \[accountMenuOpen, setAccountMenuOpen\] = useState\(false\)/); assert.match(pageSource, /const \[activeAccountDialog, setActiveAccountDialog\] = useState\(null\)/); assert.match(pageSource, /className="account-menu"/); assert.match(pageSource, /onClick=\{\(\) => openAccountDialog\("profile"\)\}/); assert.match(pageSource, /onClick=\{\(\) => openAccountDialog\("redeem"/); assert.match(pageSource, /onClick=\{\(\) => openAccountDialog\("logout"\)\}/); }); test("removes the monolithic account sheet", () => { assert.doesNotMatch(pageSource, /profile-overlay|profile-dialog|openAccount\(/); assert.doesNotMatch(globalStyles, /\.profile-overlay|\.profile-dialog/); }); test("keeps admin navigation separate from task dialogs", () => { const menu = sourceBetween(pageSource, 'className="account-menu"', '\n '); assert.match(menu, /href="\/admin\/codes"/); assert.match(menu, /account\?\.isAdmin/); }); ``` - [ ] **Step 2: Run the targeted tests and confirm the expected red state** Run: `npm test -- --test-name-pattern="account|sheet|admin navigation"` Expected: the new tests fail because `accountMenuOpen`, `activeAccountDialog`, `.account-menu`, and focused-dialog routes do not exist yet. - [ ] **Step 3: Update the design contract before styling** Replace `### Account sheet` in `frontend/DESIGN.md` with four documented primitives: ```md ### Account popover - **Structure:** identity header, profile action, redeem action with balance, administrator-only code-management link, divider, and logout action. - **Surface:** 280px elevated canvas popover anchored above the sidebar account trigger; warm hairline, existing elevated shadow, no nested cards. - **States:** closed, open, hover, focus-visible, administrator, signing-out transition. - **Accessibility:** `aria-expanded`, `aria-controls`, menu semantics, 44px rows, outside-click and Escape dismissal, focus return. ### Profile dialog - **Structure:** profile title, existing profile fields, inline result, save action. - **Width:** 560px desktop maximum with viewport-safe spacing and internal scrolling. - **States:** open, invalid, saving, success, error. ### Redeem dialog - **Structure:** current balance, redemption-code form, inline result. - **Width:** 420px desktop maximum. - **States:** open, submitting, success, error. ### Logout dialog - **Structure:** confirmation title and explanation, cancel action, destructive confirm action. - **States:** open, signing out, error. ``` - [ ] **Step 4: Verify the documentation diff** Run: `git diff --check -- frontend/DESIGN.md frontend/tests/starter-questions.test.ts` Expected: exit 0 with no whitespace errors. --- ### Task 2: Split surface state and route every account entry correctly **Files:** - Modify: `frontend/src/app/page.tsx` **Interfaces:** - Consumes: existing `Profile`, `Account`, `redeem`, `saveProfile`, `signOut`, `keepFocusWithin`, `accountTrigger`, `redeemInput`, and `closeButton` behavior. - Produces: `type AccountDialog = "profile" | "redeem" | "logout" | null`, `accountMenuOpen`, `activeAccountDialog`, `toggleAccountMenu`, `openAccountDialog`, and `closeAccountDialog`. - [ ] **Step 1: Replace the overloaded state and refs** Add the dialog type beside other UI-state types: ```ts type AccountDialog = "profile" | "redeem" | "logout" | null; ``` Replace `profileOpen` and `redeemOpen` with: ```ts const [accountMenuOpen, setAccountMenuOpen] = useState(false); const [activeAccountDialog, setActiveAccountDialog] = useState(null); ``` Replace the sheet ref with explicit surface refs: ```ts const accountMenu = useRef(null); const accountDialog = useRef(null); const creditTrigger = useRef(null); const dialogReturnTarget = useRef(null); ``` - [ ] **Step 2: Add explicit open and close handlers** Replace `openAccount` and `closeAccount` with handlers shaped as follows: ```ts function toggleAccountMenu() { setActiveAccountDialog(null); setAccountMenuOpen((current) => !current); } function openAccountDialog(dialog: Exclude, returnTarget = accountTrigger.current) { dialogReturnTarget.current = returnTarget; setAccountMenuOpen(false); setAccountError(""); if (dialog === "profile") { if (profileComplete) setProfileDraft(profile); setProfileNotice(""); } if (dialog === "redeem") { setRedeemError(""); setRedeemMessage(""); } setActiveAccountDialog(dialog); } function closeAccountDialog() { if (signingOut) return; setActiveAccountDialog(null); const returnTarget = dialogReturnTarget.current; window.requestAnimationFrame(() => returnTarget?.focus()); } ``` - [ ] **Step 3: Replace the sheet focus effect with menu dismissal and dialog focus trapping** Use one document listener for the non-modal popover: ```ts useEffect(() => { if (!accountMenuOpen) return; const dismissMenu = (event: MouseEvent) => { if (!accountMenu.current?.contains(event.target as Node)) setAccountMenuOpen(false); }; const closeOnEscape = (event: globalThis.KeyboardEvent) => { if (event.key !== "Escape") return; setAccountMenuOpen(false); window.requestAnimationFrame(() => accountTrigger.current?.focus()); }; document.addEventListener("mousedown", dismissMenu); window.addEventListener("keydown", closeOnEscape); return () => { document.removeEventListener("mousedown", dismissMenu); window.removeEventListener("keydown", closeOnEscape); }; }, [accountMenuOpen]); ``` Use `activeAccountDialog` for modal initial focus, Escape, and `keepFocusWithin`. Focus `redeemInput` for redeem and `closeButton` for profile/logout. - [ ] **Step 4: Route system fallbacks directly** Replace every `openAccount(true)` call with: ```ts openAccountDialog("redeem", creditTrigger.current); ``` Replace the incomplete-profile branch with: ```ts setProfileDraft(profile); setProfileNotice("请先补充出生资料,才能进行星盘计算。"); openAccountDialog("profile"); ``` Use `activeAccountDialog === null` in the onboarding focus effect and `activeAccountDialog !== null` for modal background inertness. - [ ] **Step 5: Run targeted tests** Run: `npm test -- --test-name-pattern="account|sheet|admin navigation"` Expected: the state/routing assertions pass; markup or CSS assertions may remain red until Tasks 3 and 4. --- ### Task 3: Replace the sheet markup with the account popover and three dialogs **Files:** - Modify: `frontend/src/app/page.tsx` **Interfaces:** - Consumes: Task 2 state, handlers, refs, existing profile and redeem forms, and `/admin/codes` route. - Produces: `.account-menu`, `.account-modal-overlay`, `.account-modal`, `.profile-modal`, `.redeem-modal`, and `.logout-modal` markup. - [ ] **Step 1: Add the anchored popover beside the sidebar account trigger** Wrap the trigger and menu in the existing `.sidebar-footer`. Add `aria-expanded`, `aria-controls`, and a rotating chevron state. Render the menu only while open: ```tsx {accountMenuOpen && (