16 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 05-finance-restyle | 01 | execute | 1 |
|
true |
|
|
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.mdFrom 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 262–303, 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-semiboldon 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) andtext-emerald-600(payment positive) — NO other colors per D-06 "Invoice Status Colors" table - ONLY two font weights used:
font-normal(default on<p>) andfont-semibold— NOfont-mediumper 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-yblock 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.
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:
-
KPI grid skeleton —
grid grid-cols-2 gap-3containing 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.) -
Aging row skeleton —
grid grid-cols-3 gap-2containing three<Skeleton className="h-16 rounded-xl" />blocks. (UI-SPEC item 2.) -
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-6per D-10, inner block gapsspace-y-3per D-11, KPI gridgap-3per D-11, aging gridgap-2per Spacing Scale "sm" - No labels, no text content — pure shapes
- Heights:
h-20for KPI tiles,h-16for aging cells,h-4for row text lines (matches Skeleton item heights from UI-SPEC literally) - Page-level
px-4 py-4per 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-pulseis 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.tsxreturns nothing - No props on the component:
grep -E "function FinanceSkeleton\(\s*\{" components/mobile/FinanceSkeleton.tsxreturns 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"returns0</acceptance_criteria>FinanceSkeleton.tsxexists, type-checks, exports a no-propFinanceSkeletonfunction, 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 officialSkeletonprimitive.
- File exists:
<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> |
<success_criteria>
- Both files compile under
npx tsc --noEmit --pretty FinanceRowrenders the 2-line stacked-row shape from UI-SPEC §"Invoice / Payment List Rows" with correct typography (text-sm font-semiboldline 1,text-xs text-muted-foregroundline 2) and supportsamountTone="destructive" | "positive" | "default"FinanceSkeletonrenders the exact layout from UI-SPEC §"Skeleton Loading State" withSkeletonprimitive, locked spacing (px-4 py-4 space-y-6,gap-3,gap-2,space-y-3), and the locked heights (h-20KPI,h-16aging,h-4row lines)- No file outside
components/mobile/is modified - Both files use the Phase 3/4 phase-comment-block convention </success_criteria>
import { FinanceRow, type FinanceRowProps } from '@/components/mobile/FinanceRow';
import { FinanceSkeleton } from '@/components/mobile/FinanceSkeleton';