wulf-pulse/.planning/phases/05-finance-restyle/05-01-PLAN.md

16 KiB
Raw Blame History

phase plan type wave depends_on files_modified autonomous requirements must_haves
05-finance-restyle 01 execute 1
components/mobile/FinanceRow.tsx
components/mobile/FinanceSkeleton.tsx
true
FIN-01
FIN-02
truths artifacts key_links
A 2-line stacked row (customer + amount on line 1, secondary metadata on line 2) is rendered by a reusable FinanceRow component, used by both invoice rows and payment rows in Plan 02
An initial-load skeleton (4 KPI tiles + 1 aging row + 3 list-row skeletons) is rendered by a FinanceSkeleton component, ready for Plan 02 to drop into the loading branch
path provides exports
components/mobile/FinanceRow.tsx Reusable 2-line stacked row for invoice and payment lists (D-06)
FinanceRow
FinanceRowProps
path provides exports
components/mobile/FinanceSkeleton.tsx Initial-load skeleton matching final page layout (D-17)
FinanceSkeleton
from to via pattern
components/mobile/FinanceRow.tsx components/ui/skeleton.tsx (NOT used here — pure presentational) (no DB or fetch — pure presentational) export function FinanceRow
from to via pattern
components/mobile/FinanceSkeleton.tsx components/ui/skeleton.tsx import { Skeleton } from '@/components/ui/skeleton'
Extract two pure-presentational helper components for the Phase 5 Finance page rewrite: `FinanceRow` (the 2-line stacked card row used by both invoice and payment lists per UI-SPEC §"Invoice / Payment List Rows"), and `FinanceSkeleton` (the initial-load placeholder per UI-SPEC §"Skeleton Loading State", D-17).

Purpose: Plan 02 consumes both directly. Extracting first means Plan 02 can focus on composition (data plumbing, Collapsibles, KPI tiles, empty/error states) without inline duplication of the row JSX or a sprawling skeleton block. Both components are internal helpers — no public app-level export needed.

Output: Two new files in components/mobile/, no changes anywhere else.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/phases/05-finance-restyle/05-CONTEXT.md @.planning/phases/05-finance-restyle/05-UI-SPEC.md @.planning/phases/04-tickets-restyle/04-01-SUMMARY.md @CLAUDE.md

From components/ui/skeleton.tsx:

function Skeleton({ className, ...props }: React.ComponentProps<"div">): JSX.Element
// Renders: <div className="bg-accent animate-pulse rounded-md ..." />
export { Skeleton }

From components/mobile/KpiCardMobile.tsx (Phase 3 — reference only, NOT modified here):

export type KpiTone = 'default' | 'attention';
interface KpiCardMobileProps {
  label: string;
  value: number | string;
  caption?: string;
  tone?: KpiTone;
}
export function KpiCardMobile(props: KpiCardMobileProps): JSX.Element

Phase comment block convention (Phase 3/4 pattern — apply to both new files):

/* ComponentName — phase 05 (FIN-NN).
 * Purpose: one-line description.
 * Props: ... */
Task 1: Create FinanceRow component components/mobile/FinanceRow.tsx - .planning/phases/05-finance-restyle/05-UI-SPEC.md (sections: "Invoice / Payment List Rows — Stacked Card Rows [D-06]", "Typography", "Color") - .planning/phases/05-finance-restyle/05-CONTEXT.md (D-06) - components/mobile/KpiCardMobile.tsx (phase comment block convention) - app/mobile/finance/page.tsx (lines 262303, current invoice and payment row JSX — extract this shape) Create `components/mobile/FinanceRow.tsx` as a 'use client' file (matching `KpiCardMobile.tsx` pattern). Pure presentational — no fetch, no state.

Open with the phase comment block per Phase 3/4 convention:

'use client';

/* FinanceRow — phase 05 (FIN-02).
 * Purpose: 2-line stacked row used by Open Invoices and Recent Payments lists.
 *   Line 1: primary label (left, text-sm font-semibold truncate) + amount (right, text-sm font-semibold).
 *   Line 2: secondary metadata (left, text-xs text-muted-foreground) + optional date (right, text-xs text-muted-foreground).
 * Pure presentational — parent owns the data.
 * Props: see FinanceRowProps. */

import { cn } from '@/lib/utils';

Export the props interface and component:

export interface FinanceRowProps {
  /** Primary label on line 1 (e.g., customer name) */
  primary: string;
  /** Amount string already formatted (e.g., "$1,234") — caller passes fmt$(value) */
  amount: string;
  /** Secondary metadata on line 2 (e.g., "#1234 · Due May 1" or just a date) */
  secondary?: React.ReactNode;
  /** Optional right-side date on line 2 (used by payment rows; invoice rows put date in `secondary`) */
  rightSecondary?: React.ReactNode;
  /** When true, amount renders in destructive color (overdue invoices). */
  amountTone?: 'default' | 'destructive' | 'positive';
}

export function FinanceRow({ primary, amount, secondary, rightSecondary, amountTone = 'default' }: FinanceRowProps) {
  const amountClass = cn(
    'text-sm font-semibold shrink-0',
    amountTone === 'destructive' && 'text-destructive',
    amountTone === 'positive' && 'text-emerald-600'
  );
  return (
    <div className="px-4 py-3 flex items-start justify-between gap-2 hover:bg-muted/50 transition-colors">
      <div className="flex-1 min-w-0">
        <p className="text-sm font-semibold truncate">{primary}</p>
        {secondary && <p className="text-xs text-muted-foreground mt-0.5">{secondary}</p>}
      </div>
      <div className="shrink-0 text-right">
        <p className={amountClass}>{amount}</p>
        {rightSecondary && <p className="text-xs text-muted-foreground mt-0.5">{rightSecondary}</p>}
      </div>
    </div>
  );
}

Hard rules from UI-SPEC (do NOT deviate):

  • Line 1 weight: text-sm font-semibold on BOTH primary and amount (per Typography table)
  • Line 2 weight: text-xs font-normal text-muted-foreground (per Typography table)
  • Container: px-4 py-3 flex items-start justify-between gap-2
  • Hover: hover:bg-muted/50 transition-colors (per Color §"Secondary surface")
  • Amount tones: only text-destructive (overdue) and text-emerald-600 (payment positive) — NO other colors per D-06 "Invoice Status Colors" table
  • ONLY two font weights used: font-normal (default on <p>) and font-semibold — NO font-medium per D-03

Do NOT add:

  • A priority left-stripe (Finance has no priority taxonomy per UI-SPEC §"Invoice / Payment List Rows")
  • A rounded card border on the row itself — the parent divide-y block owns the border (per UI-SPEC §"Container: divide-y block inside CollapsibleContent")
  • Any onClick / Link wrapping — rows are read-only per D-22

Per CLAUDE.md: kebab-case applies to file names broadly; the established convention in components/mobile/ (Phase 2/3/4) is PascalCase filenames matching the exported component (KpiCardMobile.tsx, TicketFilterStrip.tsx, etc.). Match that convention — file is FinanceRow.tsx. npx tsc --noEmit --pretty 2>&1 | grep -E "FinanceRow|components/mobile/FinanceRow" || echo "OK: no FinanceRow type errors" <acceptance_criteria> - File exists: test -f components/mobile/FinanceRow.tsx - Exports: grep -E "^export (interface FinanceRowProps|function FinanceRow)" components/mobile/FinanceRow.tsx | wc -l returns 2 - Phase comment block present: grep -q "FinanceRow — phase 05" components/mobile/FinanceRow.tsx - Uses only the two locked font weights: grep -E "font-medium|font-bold" components/mobile/FinanceRow.tsx returns nothing (per D-03; font-semibold is allowed, font-normal is the default and need not appear literally) - Hover class present: grep -q "hover:bg-muted/50" components/mobile/FinanceRow.tsx - No priority stripe colors leaked from Phase 4: grep -E "border-red-500|border-orange-400|border-amber-400|border-slate-300" components/mobile/FinanceRow.tsx returns nothing - No onClick / Link / router import: grep -E "onClick|next/link|useRouter" components/mobile/FinanceRow.tsx returns nothing - Container padding matches D-11 / UI-SPEC: grep -q "px-4 py-3" components/mobile/FinanceRow.tsx - TypeScript clean for this file: npx tsc --noEmit --pretty 2>&1 | grep -c "components/mobile/FinanceRow.tsx" returns 0 </acceptance_criteria> FinanceRow.tsx exists, type-checks, exports FinanceRow + FinanceRowProps, renders the 2-line shape from UI-SPEC §"Invoice / Payment List Rows" with correct typography (text-sm font-semibold on line 1, text-xs text-muted-foreground on line 2), supports amountTone for destructive/positive variants, no priority stripe, no interactive handlers.

Task 2: Create FinanceSkeleton component components/mobile/FinanceSkeleton.tsx - .planning/phases/05-finance-restyle/05-UI-SPEC.md (section: "Skeleton Loading State [D-17]") - .planning/phases/05-finance-restyle/05-CONTEXT.md (D-10, D-11, D-17) - components/ui/skeleton.tsx (Skeleton primitive) Create `components/mobile/FinanceSkeleton.tsx` as a 'use client' file. Pure presentational placeholder matching the final page's spacing.

Open with the phase comment block:

'use client';

/* FinanceSkeleton — phase 05 (FIN-01).
 * Purpose: initial-load placeholder for /mobile/finance.
 *   Layout: 4 KPI tile skeletons (2×2 grid) + 1 aging row (3 cells) + 2 list-row skeleton blocks (3 rows each).
 * Pure presentational — no props.
 * UI-SPEC §"Skeleton Loading State [D-17]". */

import { Skeleton } from '@/components/ui/skeleton';

Export a single FinanceSkeleton function component (no props):

Top-level wrapper matches page container per D-10: <div className="px-4 py-4 space-y-6">

Three blocks inside, separated by the parent space-y-6:

  1. KPI grid skeletongrid grid-cols-2 gap-3 containing four <Skeleton className="h-20 rounded-xl" /> blocks. (UI-SPEC §"Skeleton Loading State" item 1: "4 KPI tile skeletons: grid grid-cols-2 gap-3 px-4 pt-4 — each a Skeleton h-20 rounded-xl"; the parent provides px-4 already, drop the inner px-4/pt-4 to avoid double padding.)

  2. Aging row skeletongrid grid-cols-3 gap-2 containing three <Skeleton className="h-16 rounded-xl" /> blocks. (UI-SPEC item 2.)

  3. Two list-row blocks — render the same 3-row skeleton block twice (one for invoices, one for payments). Each block is a <div className="space-y-3"> containing 3 rows. Each row matches UI-SPEC item 3:

    <div className="px-4 py-3 flex justify-between gap-2">
      <Skeleton className="h-4 w-2/3" />
      <Skeleton className="h-4 w-16" />
    </div>
    

Hard rules from UI-SPEC:

  • ONLY use <Skeleton> from @/components/ui/skeleton — do not roll a custom pulse div per D-17
  • Spacing: outer space-y-6 per D-10, inner block gaps space-y-3 per D-11, KPI grid gap-3 per D-11, aging grid gap-2 per Spacing Scale "sm"
  • No labels, no text content — pure shapes
  • Heights: h-20 for KPI tiles, h-16 for aging cells, h-4 for row text lines (matches Skeleton item heights from UI-SPEC literally)
  • Page-level px-4 py-4 per D-10 — owned by the wrapper, not nested

Do NOT add:

  • A wrapper Card around any block (KPI tiles are skeletons of KpiCardMobile's outer shape, but in skeleton form a plain rounded-xl Skeleton block IS the placeholder per UI-SPEC item 1 — do not add <Card>)
  • Animation other than what <Skeleton> already provides (animate-pulse is in the primitive)
  • Variants or props — caller does not parameterize this skeleton npx tsc --noEmit --pretty 2>&1 | grep -E "FinanceSkeleton|components/mobile/FinanceSkeleton" || echo "OK: no FinanceSkeleton type errors" <acceptance_criteria>
    • File exists: test -f components/mobile/FinanceSkeleton.tsx
    • Exports FinanceSkeleton: grep -q "^export function FinanceSkeleton" components/mobile/FinanceSkeleton.tsx
    • Imports Skeleton primitive: grep -q "from '@/components/ui/skeleton'" components/mobile/FinanceSkeleton.tsx
    • Phase comment block present: grep -q "FinanceSkeleton — phase 05" components/mobile/FinanceSkeleton.tsx
    • Uses 2×2 KPI grid: grep -q "grid-cols-2" components/mobile/FinanceSkeleton.tsx
    • Uses 3-cell aging grid: grep -q "grid-cols-3" components/mobile/FinanceSkeleton.tsx
    • Outer spacing per D-10: grep -q "space-y-6" components/mobile/FinanceSkeleton.tsx
    • Row spacing per D-11: grep -q "space-y-3" components/mobile/FinanceSkeleton.tsx
    • Page padding per D-10: grep -q "px-4 py-4" components/mobile/FinanceSkeleton.tsx
    • Exact KPI height: grep -q "h-20" components/mobile/FinanceSkeleton.tsx
    • Exact aging height: grep -q "h-16" components/mobile/FinanceSkeleton.tsx
    • No Card import (per UI-SPEC item 1): grep -E "from '@/components/ui/card'" components/mobile/FinanceSkeleton.tsx returns nothing
    • No props on the component: grep -E "function FinanceSkeleton\(\s*\{" components/mobile/FinanceSkeleton.tsx returns nothing (the regex matches only if props destructuring is present — its absence indicates a no-prop component)
    • TypeScript clean for this file: npx tsc --noEmit --pretty 2>&1 | grep -c "components/mobile/FinanceSkeleton.tsx" returns 0 </acceptance_criteria> FinanceSkeleton.tsx exists, type-checks, exports a no-prop FinanceSkeleton function, renders the exact layout from UI-SPEC §"Skeleton Loading State" (2×2 KPI grid + 3-cell aging row + two 3-row list-skeleton blocks) with the locked spacing tokens (D-10/D-11) and the official Skeleton primitive.

<threat_model>

Trust Boundaries

Boundary Description
(none introduced) Both new files are pure presentational React components with no I/O, no fetch, no DOM event handlers, no auth surface, no schema change

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-05-01 Tampering components/mobile/FinanceRow.tsx props accept Caller passes already-formatted strings (amount); no innerHTML, no dangerouslySetInnerHTML, React JSX text escaping covers XSS. The secondary and rightSecondary props are typed React.ReactNode so the parent (Plan 02) controls what is rendered — risk is identical to existing TicketRow patterns from Phase 4.
T-05-02 Information Disclosure components/mobile/FinanceSkeleton.tsx accept No props, no data flow. Component renders only static class strings and the Skeleton primitive — no path for sensitive data to reach DOM.
</threat_model>
Phase-level checks (run after both tasks): - `npx tsc --noEmit --pretty` — no errors related to either new file - `grep -l "FinanceRow\|FinanceSkeleton" components/mobile/` — both files indexed - No edit to any file outside `components/mobile/`: `git diff --name-only HEAD | grep -v "^components/mobile/Finance" | grep -v "^.planning/"` returns nothing (excluding planning artifacts)

<success_criteria>

  • Both files compile under npx tsc --noEmit --pretty
  • FinanceRow renders the 2-line stacked-row shape from UI-SPEC §"Invoice / Payment List Rows" with correct typography (text-sm font-semibold line 1, text-xs text-muted-foreground line 2) and supports amountTone="destructive" | "positive" | "default"
  • FinanceSkeleton renders the exact layout from UI-SPEC §"Skeleton Loading State" with Skeleton primitive, locked spacing (px-4 py-4 space-y-6, gap-3, gap-2, space-y-3), and the locked heights (h-20 KPI, h-16 aging, h-4 row lines)
  • No file outside components/mobile/ is modified
  • Both files use the Phase 3/4 phase-comment-block convention </success_criteria>
After completion, create `.planning/phases/05-finance-restyle/05-01-SUMMARY.md` per the GSD summary template, including the exported interface signatures Plan 02 will import:
import { FinanceRow, type FinanceRowProps } from '@/components/mobile/FinanceRow';
import { FinanceSkeleton } from '@/components/mobile/FinanceSkeleton';