--- name: user-docs description: Write or update the end-user documentation at docs.moneymatter.app (packages/docs, Astro Starlight) and capture its light/dark screenshots from the local dev app with a throwaway test user. Use when a feature is added or its UI/behaviour changes and the docs or screenshots need to follow, when asked to "update the docs", "add a docs page", "retake screenshots", or when `npm run screenshots:missing` lists gaps. --- # User docs & screenshots The docs site is `packages/docs` — a standalone npm project (not a root workspace). Articles are `src/content/docs/
/*.mdx`; screenshots are `public/screenshots/
/.png` plus `.dark.png`. ## Hard rules - **Never use the `mcp__moneymatter__*` tools** for docs work. They act on the owner's real production account. - **Never sign in as the owner or reuse their session.** Only the throwaway test user (`docs-test@example.com`), created by the seed script. - **No direct database access.** The local DB also holds the owner's data. Create data through the API as the test user; reach unreachable states by mocking responses in the browser (see `mocks.mjs`), never by editing rows. - Taking screenshots of the running app is the explicit purpose of this skill, so browser automation is allowed here — but only with the capture toolkit below, not for general "visual verification". - No git commits. ## 1. Find what to change - A feature changed: search the articles for its UI labels and route, e.g. `grep -rn "Plan & billing\|/settings/plan-billing" packages/docs/src/content/docs`. Every `` on those pages is a retake candidate. - New feature: pick the section (sidebar groups autogenerate from directories, ordered by `sidebar.order` in frontmatter), or add a directory and a sidebar group in `astro.config.mjs`. - `npm run screenshots:missing` (in `packages/docs`) lists every referenced screenshot that has no file, with its `alt` and `hint`. ## 2. Write the article - Match the existing articles: second person, short paragraphs, UI labels in **bold** exactly as the English UI shows them, numbered procedures inside ``. - Plus-only features: `sidebar: { badge: { text: 'Plus', variant: 'tip' } }` in frontmatter and `` inline. - Screenshot tag — `alt` is what the image actually shows; `hint` tells a human how to recapture it: ```mdx ``` - **Fact-check every claim against the code**, not memory: labels from `packages/frontend/src/i18n/locales/chunks/en/**` (read via the `i18n-editor` agent — locale files are hook-blocked), behaviour from the component, limits and rules from the backend service. Drift found before: a button documented as "Select period" actually shows the chosen range ("Current month") unless the provider can't load history; a popover documented as listing per-account statuses only does so while a sync runs. ## 3. Capture screenshots Prerequisites: the dev frontend and backend are running (ports come from the root `.env.development.local`: `PORT`, `APPLICATION_PORT`), root `npm install` done (Playwright resolves from the root `node_modules`). ```bash cd packages/docs npm run screenshots:seed ``` The seed is idempotent. It signs up or signs in the test user, sets USD as base currency, connects **SimpleFIN's public demo** (Savings + Checking with real-looking synced transactions), creates the manual **Everyday Checking** account with three expenses, attaches a fake receipt JPG and invoice PDF to "Office chair", adds a passkey through a virtual authenticator, dismisses Quick Start, adds the Net Worth widget and saves the session to `scripts/capture/.state.json`. `DOCS_USER_EMAIL=… npm run screenshots:seed` seeds a separate user. Then write a small per-shot script in your scratch/temp directory (not in the repo) importing the toolkit by absolute path: ```js import { APP, bothThemes, clipAround, innermost, open, shot } from '/packages/docs/scripts/capture/helper.mjs'; import { ENTITLEMENTS, mockEntitlements } from '/packages/docs/scripts/capture/mocks.mjs'; const { browser, page } = await open({ height: 1300 }); await page.goto(APP + '/settings/security/password'); await page.getByPlaceholder('Enter new password').fill('Correct-Horse-42!'); const card = innermost({ page, has: ['Security Settings', page.getByText('Password requirements')] }); await bothThemes({ page, capture: ({ dark }) => shot({ page, target: card, src: '/screenshots/settings/change-password.png', dark }), }); await browser.close(); ``` Conventions: - **Always both themes** via `bothThemes`; it emulates the OS color scheme, which the app follows. - **Element crops** pass `target` (30px padding so shadows and rounded corners show). Several elements (trigger + popover, header + strip) → `clip: await clipAround({ locators: [...] })`. Full window only when the hint says so. - Default viewport 1440×1000 at 2× scale. If `shot` says the target is taller than the viewport, `open({ height: 1300 })`. - **Open every image you wrote with the Read tool** before moving on: check the crop edges (no half-cut cards, no sidebar or header slivers), stray toasts, broken logos, and that it matches the `alt`. ### Reaching specific states | State | How | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | First run (`/welcome`, empty dashboard with Quick Start) | Use a fresh `DOCS_USER_EMAIL`; sign up with `open({ fresh: true })` and capture before running the seed (the seed confirms the currency and dismisses Quick Start). | | Bank connection, synced transactions, bank-synced edit dialog | Seeded SimpleFIN demo. | | Link a manual account to a bank | Seeded **Everyday Checking** + the demo's unconnected account. Open the dialog, never submit it. | | Another active session | Second `chromium.launch()` context with an iPhone Safari `userAgent` that signs in, then capture the sessions tab from the main context. | | Active subscription, trial ended / read-only | `mockEntitlements({ page, entitlements: ENTITLEMENTS.activePlusYearly() })` or `ENTITLEMENTS.trialEnded()`; for the "See plans" toast add `mockPlanRequiredOnSave({ page })` and save an edit. | | Enable Banking consent expiring/expired | `mockEnableBankingConsent({ page, connectionId, daysLeft })` on the seeded SimpleFIN connection. | | Anything else unreachable | Find the response the component reads (its store or `useQuery`), rewrite just that response with `page.route` → `route.fetch()` → `route.fulfill({ response, json })`. Add it to `mocks.mjs` if it will be reused. | ### Gotchas - Sign-up always lands on "Check your email", but with `RESEND_API_KEY` empty the account can sign in immediately. - Never wait for `networkidle`: the app keeps an SSE stream open. Use `locator.waitFor()` plus short fixed waits. - `getByText` matches the sidebar first (account names, "SimpleFIN"); scope with `innermost`, `.last()`, or a nearby unique text. - `getByText` does not see `