20 KiB
| phase | slug | status | shadcn_initialized | preset | created |
|---|---|---|---|---|---|
| 5 | finance-restyle | draft | true | new-york / neutral base / CSS variables | 2026-05-03 |
Phase 5 — UI Design Contract: Finance Restyle
Visual and interaction contract for the mobile Finance page restyle. Generated by gsd-ui-researcher. Consumed by gsd-ui-checker, gsd-planner, gsd-executor.
All decisions tagged [D-NN] are LOCKED in 05-CONTEXT.md and must not be re-litigated.
Design System
| Property | Value |
|---|---|
| Tool | shadcn/ui (new-york style) |
| Preset | components.json — new-york, neutral base, CSS variables, lucide icons |
| Component library | Radix UI (via shadcn) |
| Icon library | lucide-react |
| Font | IBM Plex Sans (sans), IBM Plex Mono (numeric/ID fields) |
Source: components.json (confirmed present), DESIGN.md §2, app/styles/brand.css.
Viewport Contract
| Property | Value |
|---|---|
| Reference device | iPhone 15 Pro — 393 × 852 CSS pixels |
| Max width constraint | max-w-lg mx-auto (from app/mobile/layout.tsx) |
| Shell chrome | HeaderBar (sticky, h-14 + pt-safe) + BottomNav (fixed h-16 + pb-safe) |
| Scrollable content area | <main> in layout — bottom padding = calc(theme(spacing.16)+env(safe-area-inset-bottom)) |
| Minimum viewport target | 360px width — no horizontal overflow at this width [D-12] |
Spacing Scale
Declared values (multiples of 4). Mirrors Phase 4 UI-SPEC exactly [D-10, D-11].
| Token | Value | Usage in this phase |
|---|---|---|
| xs | 4px | Icon gaps (gap-1), badge padding (px-2 py-1) |
| sm | 8px | Row internal gaps (gap-2), section-header gap (gap-2) |
| sm+ | 12px (3 × 4) | Between rows in a list (space-y-3), card internal gap (gap-3) [D-11] |
| md | 16px | Horizontal page padding (px-4), vertical page padding (py-4) [D-10] |
| lg | 24px | Between sections (space-y-6) [D-10] |
| xl | 32px | Empty/error-state vertical padding (py-8) |
| 2xl | 48px | Large empty-state full-screen centering (py-12) |
Touch-target exception: Refresh and Sync QBO icon buttons must reach a minimum 44 × 44 px tap target — use p-3 or min-h-[44px] [D-14].
Exceptions: None beyond the 44 px touch target rule.
Typography
Two weights only: font-normal (400) and font-semibold (600). font-medium (500) is NOT used in this phase [D-03].
| Role | Size class | Weight | Line Height | Font | Usage |
|---|---|---|---|---|---|
| KPI dollar value | text-2xl (24px) (4 summary tiles only) |
font-semibold (600) |
leading-tight (1.25) |
IBM Plex Sans | Total AR / Current / Overdue / Paid MTD tile values [D-02, D-04] |
| Row primary line | text-sm (14px) |
font-semibold (600) |
leading-snug (1.375) |
IBM Plex Sans | Customer name (left) + amount (right) in invoice/payment/customer rows [D-06, D-07] |
| Row secondary / metadata | text-xs (12px) |
font-normal (400) |
leading-normal (1.5) |
IBM Plex Sans | Invoice #, doc number, date, status chip, "Xd overdue" label [D-06] |
| KPI tile label | text-xs (12px) |
font-semibold (600) |
leading-none |
IBM Plex Sans | "TOTAL AR", "CURRENT", etc. — uppercase tracking-wider [D-02] |
| KPI tile caption | text-[10px] (10px) |
font-normal (400) |
leading-normal |
IBM Plex Sans | YTD annotation on Paid MTD tile, "N invoices" caption [D-02, D-04] |
| ID / badge | text-[10px] (10px) |
font-normal (400) |
leading-normal |
IBM Plex Mono | Aging bucket labels, count labels [D-04] |
| Section header | text-sm (14px) |
font-semibold (600) |
leading-none |
IBM Plex Sans | Section titles ("Open Invoices", "Top AR by Customer", etc.) [D-13] |
Heading note: Page-level "Finance" heading is removed in the restyle — the shell HeaderBar owns the brand mark. The page opens directly with the KPI tile grid. The lastSync timestamp renders as text-[10px] text-muted-foreground inline near the Refresh/Sync controls, not as an H1 sub-caption.
Color
All colors use CSS variable tokens from app/globals.css + app/styles/brand.css. Direct Tailwind palette references are used only for semantic status hues (aging tones) per the recipe in DESIGN.md §2.
| Role | Token / Class | Usage |
|---|---|---|
| Dominant surface (60%) | bg-background |
Page background, card background, row background |
| Secondary surface (30%) | bg-muted / bg-muted/50 |
Section count badge, row hover (hover:bg-muted/50), Sync progress banner background |
| Primary accent (10%) | text-primary / bg-primary |
KPI proportion bar fill, active tab underline (border-primary text-primary), Sync QBO button (bg-primary text-primary-foreground) |
| Muted text | text-muted-foreground |
All secondary row fields, section headers, last-sync timestamp, aging labels |
| Border | border-border |
Row dividers (divide-y), Card borders, Collapsible trigger borders |
| Destructive | text-destructive / border-l-destructive |
Error card, KpiCardMobile tone="attention" left border (Overdue tile) |
Accent reserved for: active tab indicator underline, Sync QBO primary button, KpiCardMobile proportion bar fill. NOT used for row hover, icon decoration, or section headers.
Aging Bucket Status Colors [D-08] — LOCKED
| Bucket | Tone | Classes |
|---|---|---|
| 1–30 days | Caution | text-amber-600 bg-amber-50 dark:bg-amber-950/30 border-amber-200 dark:border-amber-800 |
| 31–60 days | Warning | text-orange-600 bg-orange-50 dark:bg-orange-950/30 border-orange-200 dark:border-orange-800 |
| 60+ days | Critical | text-destructive bg-destructive/10 border-destructive/30 |
These use the DESIGN.md §2 status-hue recipe (bg-{hue}-500/15 text-{hue}-600) for acceptable contrast in both modes. 60+ days uses the --destructive CSS variable token, not a raw red palette value.
Invoice Status Colors [D-06]
| Status | Color |
|---|---|
| Overdue amount | text-destructive |
| Overdue "Xd overdue" label | text-destructive |
| Paid / collected amount | text-emerald-600 (payment rows only) |
| Current balance | inherit (text-foreground) |
Component Inventory
Primary Visual Anchor
The primary focal point per section is the customer name + amount pair on line 1 of each stacked row. The text-sm font-semibold weight on both the name (left) and currency amount (right) creates an immediate scan path — readers scan the amount column first, then the customer name. Section headers act as waypoints between data groups.
KPI Summary Tiles — 2×2 Grid [D-01, D-02]
Layout: grid grid-cols-2 gap-3 px-4 pt-4
Reuse KpiCardMobile from components/mobile/KpiCardMobile.tsx (Phase 3). No new card component needed.
| Tile | label |
value |
caption |
tone |
|---|---|---|---|---|
| Total AR | "TOTAL AR" |
fmt$(summary.total_ar) |
"{N} open invoices" |
"default" |
| Current | "CURRENT" |
fmt$(summary.current_balance) |
"{N} invoices" |
"default" |
| Overdue | "OVERDUE" |
fmt$(summary.overdue_balance) |
"{N} invoices" |
"attention" (destructive left border) |
| Paid MTD | "PAID MTD" |
fmt$(summary.paid_mtd) |
"YTD: {fmt$(paid_ytd)}" |
"default" |
Note: KpiCardMobile renders value via .toLocaleString() when numeric. Pass pre-formatted currency string (output of fmt$()) for dollar tiles — the component accepts string for value.
Aging Buckets — 3-Cell Row [D-08]
Layout: grid grid-cols-3 gap-2 inside a px-4 container. No horizontal scroll.
Each cell is a <Card> (or <div className="rounded-xl border p-3">) with:
[balance: text-sm font-semibold]
[label: text-[10px] text-muted-foreground]
[count: text-[10px] text-muted-foreground font-mono]
Visible only when summary.overdue_balance > 0.
Section header above: text-sm font-semibold + optional text-xs text-muted-foreground count badge.
Invoice / Payment List Rows — Stacked Card Rows [D-06]
Container: divide-y block inside CollapsibleContent. Max height max-h-80 overflow-y-auto for invoices; max-h-64 overflow-y-auto for payments (preserves existing scroll behavior).
Row structure (2-line layout):
[px-4 py-3 flex items-start justify-between gap-2]
[left: flex-1 min-w-0]
Line 1: customer name — text-sm font-semibold truncate
Line 2: invoice# · due date · status chip · "Xd overdue" — text-xs text-muted-foreground
[right: shrink-0 text-right]
Line 1: balance amount — text-sm font-semibold (text-destructive if overdue)
Line 2: txn date (payments only) — text-xs text-muted-foreground
No priority stripe (Finance has no priority taxonomy). Row hover: hover:bg-muted/50 transition-colors.
Top Customers List [D-07]
Same 2-line row structure as invoice rows but without the right-side date:
[px-4 py-3 flex items-center gap-3]
[left: flex-1 min-w-0]
Line 1: customer name — text-sm font-semibold truncate
Line 2: "{N} invoice(s)" — text-xs text-muted-foreground
[right: shrink-0 text-right]
Line 1: balance — text-sm font-semibold
[proportion bar: w-16 h-1 rounded-full bg-muted overflow-hidden]
[fill: h-full bg-primary/70 rounded-full, width = % of total_ar]
Monthly Revenue — Stacked List [D-09]
No chart. Renders as a divide-y list inside a Card (or bordered div).
Each row:
[px-4 py-3 flex items-center justify-between]
[left: month label — text-sm font-semibold]
[right: revenue — text-sm font-semibold + count — text-xs text-muted-foreground]
Section header: "Revenue — last 12 months" in text-sm font-semibold.
Visible only when monthly_revenue.length > 0.
Collapsible Section Triggers [D-16]
Use shadcn Collapsible + CollapsibleTrigger + CollapsibleContent. Replace existing bare <button onClick> pattern.
Trigger bar: w-full flex items-center justify-between px-4 py-4 hover:bg-muted/50 transition-colors
Left slot: icon (h-4 w-4 text-muted-foreground) + section label (text-sm font-semibold) + count badge (text-xs bg-muted rounded-full px-2 py-1 font-mono)
Right slot: ChevronDown (open) / ChevronRight (closed), h-4 w-4 text-muted-foreground
Invoice Tab Toggle [D-15]
Replace existing border-b-2 tab buttons with a shadcn-styled segmented chip pair.
Two <button> elements in a flex row, separated by border-b:
Active chip: flex-1 text-xs py-2 font-semibold border-b-2 border-primary text-primary transition-colors
Inactive chip: flex-1 text-xs py-2 font-normal border-b-2 border-transparent text-muted-foreground transition-colors
Labels: "Overdue ({N})" / "Current ({N})"
Page Header Controls [D-14]
Replace existing control row with icon-button + labeled action pattern:
Refresh button: <button aria-label="Refresh finance data"> — p-3 rounded-full hover:bg-muted/50 transition-colors disabled:opacity-40
- Icon:
RefreshCw h-4 w-4(addsanimate-spinclass whenloading)
Sync QBO button: <button aria-label="Sync from QuickBooks"> — flex items-center gap-2 px-3 py-2 rounded-xl bg-primary text-primary-foreground text-xs font-semibold disabled:opacity-50 hover:bg-primary/90 transition-colors
- Icon:
CloudDownload h-3.5 w-3.5(addsanimate-pulsewhensyncing) - Label:
"Sync QBO"(idle) /"Syncing…"(in-flight)
Last sync timestamp: text-[10px] text-muted-foreground inline below the page H1 (if present) or as a caption line next to the Refresh button. Not a tooltip — render inline.
Sync Progress Banner [D-17]
When syncMsg is non-null, render a banner in bg-muted text-xs text-muted-foreground rounded-xl px-3 py-2:
[flex items-center gap-2]
[RefreshCw h-3 w-3 animate-spin shrink-0]
[{syncMsg text}]
Skeleton Loading State [D-17]
Initial load renders:
- 4 KPI tile skeletons:
grid grid-cols-2 gap-3 px-4 pt-4— each aSkeleton h-20 rounded-xl - 1 aging row skeleton:
grid grid-cols-3 gap-2 px-4— threeSkeleton h-16 rounded-xl - 3 list row skeletons (x2 for both collapsibles): each row is:
[px-4 py-3 flex justify-between gap-2] [Skeleton h-4 w-2/3] [Skeleton h-4 w-16]
Use Skeleton from components/ui/skeleton.tsx directly.
Interaction Contracts
Collapsible State [D-16]
invoicesOpenandpaymentsOpenare localuseState<boolean>(false)— collapsed by default- Use shadcn
Collapsiblewithopen={invoicesOpen} onOpenChange={setInvoicesOpen} - No URL sync for collapsible state (Finance has no deep-link filter requirement)
Invoice Tab State [D-15]
tabstate:useState<'open' | 'overdue'>('overdue')— overdue default- Tab change is a local state mutation only (no URL sync, no data refetch)
- Switching tab immediately filters the already-fetched
open_invoicesarray client-side
Sync Flow [D-14]
syncAndRefresh()shows the progress banner during the poll loop (existing logic kept intact)- On completion:
toast.success("QuickBooks sync complete")via sonner - On failure:
toast.error("Sync failed — check QBO connection")via sonner - The progress banner disappears (
setSyncMsg(null)) after load + loadLastSync resolve
Empty State [D-19]
- When
summary.total_ar === 0ANDopen_invoices.length === 0: render neutral card with "No outstanding AR" copy — no empty tables - Other zero-data sections:
recent_payments.length === 0: render"—"inside the collapsible body (do not hide the collapsible header)top_customers.length === 0: hide the Top Customers section entirelymonthly_revenue.length === 0: hide the Monthly Revenue section entirely
Error State [D-18]
Full-page error (initial load failure): render a destructive-tinted card:
[rounded-xl border border-destructive/30 bg-destructive/10 px-4 py-6 text-center mx-4]
[text-sm font-semibold text-destructive] "Failed to load finance data"
[text-xs text-muted-foreground mt-1] "Check your connection and try again."
[Button variant="outline" size="sm" className="mt-4" onClick={load}] "Retry"
Transient sync failures: toast.error() only — no inline error banner.
Accessibility [D-14]
- Refresh icon button:
aria-label="Refresh finance data"(no visible text label) - Sync QBO button:
aria-label="Sync from QuickBooks"+ visible "Sync QBO" label - All collapsible triggers:
aria-expandedvia RadixCollapsibleTrigger(automatic) - Tab buttons: native
<button>witharia-pressed={tab === t}orrole="tab"pattern - Aging bucket cells: read-only, no interactive role needed
- Row containers: no interactive role (read-only list)
Copywriting Contract
All copy locked from 05-CONTEXT.md D-14 through D-19 and REQUIREMENTS.md FIN-01, FIN-02.
| Element | Copy | Source |
|---|---|---|
| Refresh button aria-label | "Refresh finance data" | D-14 |
| Sync QBO button label (idle) | "Sync QBO" | D-14 |
| Sync QBO button label (in-flight) | "Syncing…" | Existing (keep) |
| Sync progress — starting | "Starting sync…" | Existing (keep) |
| Sync progress — in progress | "Syncing with QuickBooks…" | Existing (keep) |
| Sync progress — 409 conflict | "Sync already in progress — refreshing data…" | Existing (keep) |
| Sync progress — refreshing | "Refreshing data…" | Existing (keep) |
| Sync success toast | "QuickBooks sync complete" | D-18 convention |
| Sync failure toast | "Sync failed — check QBO connection" | D-18 |
| Initial load failure heading | "Failed to load finance data" | D-18 |
| Initial load failure body | "Check your connection and try again." | D-18 |
| Load error retry button | "Retry" | D-18 |
| Empty state — no AR | "No outstanding AR" | D-19 |
| Empty payments (in collapsible) | "—" (em-dash, no additional copy) | D-19 |
| Section header — invoices | "Open Invoices" | Existing (keep) |
| Section header — payments | "Recent Payments" | Existing (keep) |
| Section header — customers | "Top AR by Customer" | Existing (keep) |
| Section header — aging | "Overdue Aging" | Existing (keep) |
| Section header — revenue | "Revenue — last 12 months" | Existing (keep) |
| Invoice tab — overdue | "Overdue ({N})" | Existing (keep) |
| Invoice tab — current | "Current ({N})" | Existing (keep) |
| KPI tile label — total AR | "TOTAL AR" | D-02 |
| KPI tile label — current | "CURRENT" | D-02 |
| KPI tile label — overdue | "OVERDUE" | D-02 |
| KPI tile label — paid MTD | "PAID MTD" | D-02 |
| Paid MTD caption | "YTD: {fmt$(paid_ytd)}" | D-02 |
Destructive actions: None in this phase. Finance page is entirely read-only; Sync QBO is a non-destructive sync trigger with no confirmation dialog required.
Component Files
Following the Phase 3/4 pattern (components in components/mobile/). Finance restyle is primarily a page-body rewrite with no new stand-alone components unless the FinanceRow pattern warrants extraction.
| File | Purpose | Action |
|---|---|---|
app/mobile/finance/page.tsx |
Main finance page — complete restyle | Rewrite in place |
components/mobile/KpiCardMobile.tsx |
2×2 KPI tile — reused directly | No changes [D-01] |
components/mobile/FinanceRow.tsx |
(optional) Shared 2-line row for invoices, payments, customers | Create if DRY saves ≥2 duplicated row implementations |
If FinanceRow is extracted it is an internal helper (components/mobile/FinanceRow.tsx), not a public export. No other public mobile components are added.
Component comment block convention (Phase 3 pattern):
/* ComponentName — phase 05 (FIN-NN).
* Purpose: one-line description.
* Props: ... */
API Shape Contract
API route (app/api/mobile/finance/route.ts) and the FinanceData interface in app/mobile/finance/page.tsx are unchanged [D-20, D-21].
The page continues to import type the response shape from the inline FinanceData interface already defined in page.tsx. No new exported interfaces needed.
Helper functions in the page file (fmt$(), fmtDate()) are preserved unchanged [D-05].
Registry Safety
| Registry | Blocks Used | Safety Gate |
|---|---|---|
| shadcn official | Card, CardContent, Collapsible, CollapsibleTrigger, CollapsibleContent, Skeleton, Button |
not required |
No third-party registries. All components are either shadcn official primitives already installed in components/ui/, existing Phase 3 mobile components in components/mobile/, or purpose-built helpers. Collapsible is confirmed installed at components/ui/collapsible.tsx.
What Stays Unchanged
Per D-20 through D-23 and phase boundary:
app/api/mobile/finance/route.ts— not touchedFinanceDatainterface (inline inpage.tsx) — not touchedfmt$()andfmtDate()helpers — preservedsyncAndRefresh()poll logic — preserved (reskin controls only)loadLastSync()logic — preservedinvoicesOpen/paymentsOpen/tabstate shape — preserved (just wired to shadcnCollapsible)app/mobile/layout.tsx— not touched (HeaderBar + BottomNav provided by Phase 2) [D-23]- BottomNav active-tab detection (
pathname.startsWith('/mobile/finance')) — already correct, no change
Checker Sign-Off
- Dimension 1 Copywriting: PASS
- Dimension 2 Visuals: PASS
- Dimension 3 Color: PASS
- Dimension 4 Typography: PASS
- Dimension 5 Spacing: PASS
- Dimension 6 Registry Safety: PASS
Approval: pending
Phase: 05-finance-restyle UI-SPEC created: 2026-05-03 UI-SPEC revised: 2026-05-03 (checker blocking fixes — typography 5→4 sizes, spacing non-multiples-of-4, iteration 2 — badge px-1.5/py-0.5 → px-2/py-1, gap-1.5 → gap-2) Source decisions: 05-CONTEXT.md D-01 through D-23 (all locked) Typography/spacing mirrors: 04-UI-SPEC.md (Phase 4 Tickets Restyle) KPI tile pattern mirrors: 03-01-SUMMARY.md (KpiCardMobile, Phase 3)