# Frontend React / Vite / Design ## Convention: Invoice Detail Selection Has One URL State Source - `InvoicePoolPage` reads its selected invoice from `useSearchParams().get("id")`. Upload success, list detail selection and return-to-list update that parameter through the router's functional setter, preserving other parameters and the existing replace semantics. - Do not mirror the ID into `useState` and synchronize both directions with effects depending on both values. An upload changes one side first; opposing effects then exchange old values, repeatedly unmounting the detail and making manual correction unusable. A visible OCR banner alone does not prove a usable detail. - Keep OCR polling, form edits and invoice permissions unchanged. The management correction case uses a finance identity with `invoice.manage`; ordinary employee reimbursement uploads remain separate coverage. - The retained `invoice-ocr-flow.spec.ts` correction case must execute in the managed mock/jobs profile: missing fields reject premature availability, correcting them makes the invoice available without reload or forced click, and returning to the list then reopening preserves the result and selected URL. A conditional skip cannot serve as this regression's evidence. ## Receipt Verification Dialog - Show document nature separately from extracted transaction status. A booked official debit receipt may use the actual booking date and checked-original declaration; do not ask the operator to invent a successful bank status. Show that basis to the reviewer and keep save/review separate from settlement. Non-final receipts or contradictory directions have no manual override. - Old evidence needing local document inspection exposes an explicit finance action in the existing dialog; retain history and explain the need to recheck. Reset document-proof choices along with corrections on refresh. Verify the actual desktop/mobile proof entry, required original check, applicable reviewer, final settlement and non-final refusal through the rendered dialogs. - When readiness is unmet and an active receipt has a system/key/snapshot failure, the batch banner must direct prerequisite repair, not an unavailable verification dialog. This is display only: a retained failed attachment must not block other sufficient evidence. Refresh stays read-only; controlled reupload retains history. - Reuse the receipt card and existing Dialog/Input/Button patterns. No separate receipt center. Show original and proposed summaries; account fields remain masked. Original-file preview keeps its existing authorized path. - Unchanged association submits page/item only. Corrections submit changed fields only; never round-trip account masks as bank accounts. Unedited fields are merged against the server's version-bound original evidence, not a client snapshot. - Detail query keys include actor/batch/attachment and forward AbortSignal. Disable background form refetch; explicit refresh clears selections, corrections and reason. Identity or batch switches close the dialog, and unmounted mutation callbacks must not refresh another identity's screen. - Backend canReview is an eligibility projection, not authorization. Hide review actions when false; actual review still checks role, permission and all participants. Save/review never call payment execution or upload. ## Employee Lender Source Selection - The default lender action uses `LoanEmployeeLenderDialog` to select an existing employee and confirm the debtor company, name, bank and masked account. Keep employee/company selectors explicitly labelled; do not add duplicate account/name inputs or infer the debtor solely from employment. Missing data directs qualified users to the existing employee-maintenance page; preserve an explicit non-employee route. - Source query keys include actor, employee and company and forward `AbortSignal`; hide cached summaries without current bank-read permission. Disable background form refetch, clear the confirmation on explicit reload/source changes, and require a separate explicit refresh for differing existing lender data. Show the in-flight block and the need to renew company-representative authority after refresh. - Both employee link and refresh writes pass the component's abort signal through `saveLoanAction(path, body, method, signal)`. Abort on unmount/identity change as well as checking actor before success callbacks: a callback guard alone cannot prevent a late 401 from replaying the old POST with the new session. This is a scoped fix, not a claim that all older application mutations have this lifecycle. - Browser acceptance uses the existing isolated loan runner with `RUN_ISOLATED_LOAN_BROWSER=1`: actual desktop/mobile employee selection, historical loan/balance setup and two-person authority confirmation. Test delayed 401 responses across real logout/login in one document, canceled old writes and unchanged lender/audit data; preserve successful same-identity refresh/retry and the original non-employee workflow. Synthetic data and bank tests do not grant production payment authority. ## Source Of Truth - Reminder cooldown hints render server UTC timestamps in Asia/Shanghai with an explicit Beijing-time label and second precision. Reuse formatDisplayDateTime's opt-in includeSeconds; existing callers retain minute precision. Both registered reason and legacy details paths reject malformed/non-finite timestamps without reflecting arbitrary values. Cover a UTC-to-Beijing cross-year case. Frontend visual and interaction design uses the installed `frontend-design` skill as the primary design authority. `docs/产品设计规则.md` remains a product experience and business-boundary fact source, not a visual, brand or aesthetic rulebook. For PC/mobile/WeCom pages, add Playwright or visual verification when reasonable. This spec keeps hyb-oa-specific React/Vite, shared-component, business-boundary and verification contracts. Do not copy broad aesthetic guidance from `frontend-design` into this file; use the skill directly for design judgment. ## Convention: Reimbursement Payment Handoff Consistency - Reimbursement desktop shells and payment detail opt into `detail-workspace-grid--payment`: a 320px aside at lg and above, single column below lg. Both fill the same outer page width; do not add a reimbursement-only `max-w-7xl` or centered inner wrapper. Verify both workspace left/right boundaries, reimbursement hero alignment and 320px asides at 1280/1440/1920, plus narrow-screen overflow. Equal aside widths alone do not prove equal page widths; smaller viewports can hide a max-width defect. Leave unrelated detail grids unchanged. - `BusinessDetailActionPanel({ inlineActions: true })` puts the primary action followed by the More trigger in a wrapping, content-width row. The default layout stays unchanged. Both detail pages use the existing DropdownMenu; menu items are portalled outside the card, so test via page-level menu roles. Keep disabled states, permission conditions, confirmations and mobile payment actions intact. Do not pass known-empty action components as children: a React element that renders null is still a nonempty slot and otherwise creates a phantom divider. - Linked reimbursement invoices use a desktop table with number preview, seller, date and right-aligned amount; narrow rows stack without duplicating IDs or preview controls. Keep OCR warnings, unlinked correction cards, attachment delete confirmation and evidence anchors. Test five synthetic invoices, long names/numbers, empty attachments, preview calls, narrow layout and the existing approval/detail consumers. - A compact payee-account reveal control stays beside the masked value as a 44px Eye button with title and aria-label `查看完整账号`. Preserve role/permission/status/account gates and the reason/audit dialog; icon clicks must not immediately request plaintext. Layout measurements wait for fonts and compare aligned centers; round subpixel box heights to hundredths before checking the 44px minimum (Chromium can return 43.99994 for a CSS 44px target). - `resolveReimbursementPaymentHandoff({ status, deletedAt, progress })` consumes the server's `payment_batch_created`, `payment_batch_review` and `payment_and_receipt` stages. It is a display projection, not a second payment eligibility or write state machine. - Only an explicit current creation stage followed by pending review/payment can advertise creation, and the caller still requires `payment.batch.manage`. Missing stages, unknown status or contradictory facts never produce a guessed creation action. Cancelled and failed batches use the existing stage explanation, not automatic recreation. - Read-only detail states (including reviewing, approved and paid) override global focus caching locally (`staleTime: 0`, refetch on mount and visibility return). Do not enable whole-app focus refetch or overwrite draft/rejected editors for this fix. A query error must not keep a stale actionable payment panel. - PC uses `asideContext` for current progress after `actionPanel`, with `lg:static` on the action card. Payment detail and reimbursement approval share this order; comments remain readable without horizontal overflow. Mobile retains its own safe-area action contract. - Test absent batch, review, export, receipt, paid, failed/cancelled, missing progress and no batch-read permission. Test create/review/export with real isolated roles, then return from batch to reimbursement. Include visibility return and refresh; a payment-only test does not prove the source detail is correct. - Wrong: `approved => create batch`, or `!targetHref => no batch`. Correct: stage-derived next step and one server-authorized destination, including a conservative unknown state. - Reimbursement desktop payment links use content width (`justify-self-start`) and the existing 44px button height, not grid stretching across the aside. The creation label is `创建批次`; it navigates to the existing pending view and never creates a batch itself. Keep mobile touch sizing and the generic action-panel layout unchanged. Verify actual desktop bounds and that navigation sends no batch-creation POST. - When `PaymentBatchesPage` retains its `lg:hidden` mobile action bar, its desktop action card must keep `hidden lg:block` as well as `lg:static`; changing stickiness must not remove responsive visibility. The adjacent progress stays visible. Cover the narrow PC route in addition to the dedicated mobile route, including receipt-upload action switching. ## Convention: Portal Query Eligibility Is Not Finance Display Eligibility - `MobilePortalPage` loads authenticated `GET /portal/home` independently of `canUseFinance`. That flag gates the finance card, not `businessHandoffs`; the server still filters each item by role, permissions, participation, department and maker-checker. - Test employee and CEO separately on PC and mobile, starting in a fresh browser context directly at the destination. Assert the mobile request actually occurs, the authorized link appears, processing removes it after reread, and failures show retry rather than an empty list. - A finance mobile mock plus an employee desktop test is not a role-by-device matrix. Real isolated authentication/HTTP/browser checks must exercise employee returned work and CEO independent review without granting either a finance role. - Portal response caches are employee-scoped: PC uses `["portal-home", me?.id]`, mobile uses `["mobile", "portal", "home", me?.id]`, and both queries require an existing identity. Keep these prefixes so existing invalidation callers still match; do not use previous-key placeholder data. `staleTime: 0` causes a refetch but does not isolate identities, and clearing PWA Cache Storage does not clear the singleton React Query cache. - Identity keys also require cancellation: both portal query functions consume their `signal` and pass it through `fetchPortalHome(signal?: AbortSignal)` to `apiRequestData`. Otherwise an old request's late 401 can refresh under the new session, return the new employee's payload and cache it under the old employee's key. Validate actual transport cancellation plus A→B→A with the final A response held, not only A→B or successful late responses. Same-identity 401 refresh must still succeed; do not disable authentication refresh globally to make the test pass. - Validate account changes within one browser context and document, using the UI logout/login flow rather than clearing caches, reloading or creating a new context. After the second identity is visible, old handoffs must remain absent while its response is held, after an error, through retry and after the old identity's delayed request completes. Assert that a request was actually held so a fast empty result cannot hide stale rendering; cover PC and mobile. Synthetic visibility events must reach the window listener (`bubbles: true` when dispatched on document). Real-auth fixtures must resolve profile fields from the authenticated ID; `request.employee` is not the full `/me` profile. - A scoped portal fix does not prove every application query or same-account permission change has correct cache lifecycle. Keep unrelated authentication/cache work separate, and preserve the distinction between synthetic API browser checks, real isolated authentication/HTTP and production validation. ## Scenario: Actionable Validation And Selectable Payment Channels ### 1. Scope / Trigger - New validation feedback, disabled selections, or payment creation controls. ### 2. Signatures - `resolveErrorMessage(ApiError)` prefers the explicit domain-scoped reason map. - `PaymentReadiness` is `{ ready, blockers: [{ reasonCode, entityId?, entityCode? }] }`. - Candidate query identity includes payment channel; combination query identity includes selected IDs and channel. Never reuse another selection's readiness. ### 3. Contracts - Unknown `VALID_001` uses actionable safe fallback, with a bounded diagnostic request ID only when present. Do not render raw message/details, IDs or paths. - Keep reason registration, client map and fixed concrete-text tests together. Structured invoice-count rendering remains narrowed and must tolerate malformed fields without exposing payloads. - Blocker text stays beside its row/action; use persistent text, `role=alert` or `aria-live`, and `aria-describedby` on disabled actions. Toast supplements, never replaces, the persistent explanation. Normal rows add no success noise. - The payment channel selector is available before any selection, otherwise bank-blocked rows make other supported channels unreachable. - A not-ready unselected row cannot be selected; a previously selected row that becomes invalid can still be deselected. Pending writes may disable edits. - Pending/error/not-ready combination state disables creation, and the mutation handler checks the same result. Server create rechecks independently. - Persisted payment failure summaries are not UI copy. Use the failure projection in `paymentBatchDisplay.ts`, associating risk reasons by batch ID and item ID; historical/unknown failure text and execution-risk details use safe fallback. Do not announce complete success when the execution result is `failed`. - Only authorized maintainers may receive maintenance links; other roles receive recovery-owner text. Do not instruct users to falsify their actual bank details. ### 4. Validation & Error Matrix - Missing company/bank/card or unsupported bank -> exact local row explanation. - Cross-company combination -> action-local explanation and disabled button. - Concurrent create refusal -> remain on the page with action-local safe text. - Unknown reason -> safe fallback, optional request ID, no empty identifier label. ### 5. Good/Base/Bad Cases - Good: switch to an existing non-bank channel before selection; deselect a row whose readiness changed without losing the rest of the selection. - Base: normal bank fixture remains selectable and submits for independent review. - Bad: hide the channel selector until a selectable bank row is chosen, or disable an already-selected invalid checkbox permanently. ### 6. Tests Required - Concrete message fixtures, unknown reasons with/without request IDs, and malformed structured invoice details; no broad message regex substitutions. - Local synthetic Playwright covers omissions, channel limits, combination block, concurrent failure, keyboard selection, accessible descriptions, channel-before- selection and deselection after invalidation. No real payment data is required. ### 7. Wrong vs Correct - Wrong: `disabled={!candidateReady}` for every checkbox. - Correct: block only new invalid selections while preserving deselection, and share the reason mapping between row, combination and create errors. ## Scenario: PC Shell And Detail Breakpoint Coordination ### 1. Scope / Trigger - Trigger: changing the desktop sidebar, app-shell content offset, topbar drawer entry, detail two-column grid or sticky detail action panel. - Purpose: prevent the shell and detail workspace from expanding at the same narrow breakpoint and leaving the business main column unusably small. ### 2. Signatures - Fixed sidebar: `.app-shell-sidebar` uses `xl:flex`. - Main content offset: `.app-shell-main` uses `xl:ml-72` outside mobile routes. - Topbar drawer trigger: `aria-label="打开菜单"` uses `xl:hidden`. - Detail workspace and action panel remain `lg:grid-cols-*` and `lg:sticky` unless a later task changes the complete contract. ### 3. Contracts - Sidebar visibility, main-content offset and drawer-trigger visibility switch at one shared breakpoint; never change only one of the three. - Evaluate detail widths after subtracting the fixed sidebar and page padding, not as a fraction of the full viewport. - At the narrowest viewport where the fixed sidebar appears, the main detail column is at least `560px`, the action column is at least `280px`, and the document has no horizontal overflow beyond a `2px` rendering tolerance. - Business identifiers and normal company-time date strings use DOM line-count or geometry assertions; `position: sticky` alone is not proof that the layout is usable. - A Playwright spec writes screenshots only to its own current output directory. Never reuse an archived Trellis task's evidence directory as a live test target. - Before a first screenshot after SPA navigation, assert the intended page identity and required controls are ready (for example, login heading and identifier visible, submit enabled). `goto`/load or font completion alone does not prove React has rendered the intended view. Preserve the original screenshot and test budget; do not convert capture failures into retries or assume a screenshot error proves authentication failed. - When comparing alignment inside an animated ancestor (for example `.page-scaffold`'s `page-reveal`), read all relevant `getBoundingClientRect()` values in one synchronous browser `evaluate`/`evaluateAll` callback. Sequential `boundingBox()` calls, or calls grouped with `Promise.all`, can observe different animation frames. Keep the original alignment assertions and pixel tolerances; do not hide the race with sleeps, retries or disabled page animations. ### 4. Validation & Error Matrix - Fixed sidebar visible while drawer trigger is also visible -> breakpoint contract failure. - Main content has the sidebar offset while the sidebar is hidden -> breakpoint contract failure. - Two columns exist but either column is below its minimum width -> layout failure, even if sticky CSS passes. - `documentElement.scrollWidth > clientWidth + 2` -> horizontal-overflow failure. - A routine test run modifies screenshots under another task's evidence directory -> output-contract failure; move the writer before regenerating any evidence. ### 5. Good/Base/Bad Cases - Good: `1100px` uses the topbar drawer and keeps the detail grid; `1280px` shows the fixed sidebar and still meets both column-width thresholds. - Base: mobile routes continue to use their existing mobile shell and sticky bottom actions. - Bad: move the detail grid from `xl` to `lg` while leaving the 288px app sidebar and content offset at `lg`, then validate only `position: sticky`. ### 6. Tests Required - A source-structure contract asserts the three shell classes use `xl`, while the shared detail grid and action panel retain `lg`. - A focused Playwright regression at `1280px` measures both detail columns, horizontal overflow and representative identifier/date line counts using realistic-length fixtures. - At least one synthetic workflow detail and one asset detail cover the shared layout; existing mobile regression remains green. - After screenshot-producing tests, assert archived task evidence remains clean in `git status`; current-suite screenshots may be retained only under that suite's own output directory. ### 7. Wrong vs Correct #### Wrong ```tsx