PingToProd

Design Specification

RoomieTab

Design

Design Specification

Color Palette

Primary

primary-50

#EEF2FF

primary-100

#E0E7FF

primary-200

#C7D2FE

primary-300

#A5B4FC

primary-400

#818CF8

primary-500

#6366F1

primary-600

#4F46E5

primary-700

#4338CA

primary-800

#3730A3

primary-900

#312E81

Neutral

gray-50

#F9FAFB

gray-100

#F3F4F6

gray-200

#E5E7EB

gray-300

#D1D5DB

gray-400

#9CA3AF

gray-500

#6B7280

gray-600

#4B5563

gray-700

#374151

gray-800

#1F2937

gray-900

#111827

Semantic

success-500

#059669

success-100

#DCFCE7

error-500

#DC2626

error-100

#FEE2E2

warning-500

#D97706

warning-100

#FEF3C7

info-500

#0EA5E9

info-100

#E0F2FE

teal-500

#0D9488

teal-100

#CCFBF1

Typography

0

{
  "name": "display",
  "fontFamily": "Caveat, cursive",
  "fontSize": "2.25rem",
  "fontWeight": "700",
  "lineHeight": "1.15",
  "tailwindClass": "font-['Caveat'] text-4xl font-bold leading-tight"
}

1

{
  "name": "heading-1",
  "fontFamily": "Inter, system-ui",
  "fontSize": "1.875rem",
  "fontWeight": "700",
  "lineHeight": "1.2",
  "tailwindClass": "text-3xl font-bold leading-snug"
}

2

{
  "name": "heading-2",
  "fontFamily": "Inter, system-ui",
  "fontSize": "1.5rem",
  "fontWeight": "600",
  "lineHeight": "1.3",
  "tailwindClass": "text-2xl font-semibold"
}

3

{
  "name": "heading-3",
  "fontFamily": "Caveat, cursive",
  "fontSize": "1.25rem",
  "fontWeight": "600",
  "lineHeight": "1.35",
  "tailwindClass": "font-['Caveat'] text-xl font-semibold"
}

4

{
  "name": "body-lg",
  "fontFamily": "Inter, system-ui",
  "fontSize": "1rem",
  "fontWeight": "400",
  "lineHeight": "1.6",
  "tailwindClass": "text-base leading-relaxed"
}

5

{
  "name": "body",
  "fontFamily": "Inter, system-ui",
  "fontSize": "0.9375rem",
  "fontWeight": "400",
  "lineHeight": "1.5",
  "tailwindClass": "text-[15px] leading-normal"
}

6

{
  "name": "body-sm",
  "fontFamily": "Inter, system-ui",
  "fontSize": "0.875rem",
  "fontWeight": "400",
  "lineHeight": "1.5",
  "tailwindClass": "text-sm"
}

7

{
  "name": "caption",
  "fontFamily": "Inter, system-ui",
  "fontSize": "0.75rem",
  "fontWeight": "400",
  "lineHeight": "1.4",
  "tailwindClass": "text-xs text-gray-500"
}

8

{
  "name": "mono",
  "fontFamily": "JetBrains Mono, monospace",
  "fontSize": "0.875rem",
  "fontWeight": "500",
  "lineHeight": "1.5",
  "tailwindClass": "font-mono text-sm"
}

9

{
  "name": "sketch-ui",
  "fontFamily": "Caveat, cursive",
  "fontSize": "1rem",
  "fontWeight": "600",
  "lineHeight": "1.4",
  "tailwindClass": "font-['Caveat'] font-semibold"
}

Component Catalog

NavBar

Bottom navigation bar on mobile (fixed, above safe-area inset) and top horizontal nav on desktop (md+). Renders four primary destinations: Home (dashboard), Expenses, Settle, Settings. Active tab has an indigo top-border indicator on mobile and an underline on desktop. Notification badge overlays the Expenses icon when there are unread entries.

mobile-bottomdesktop-topwith-badge
activeTab'home' | 'expenses' | 'settle' | 'settings'
unreadCountnumber
householdNamestring

Button

Multi-variant action button with minimum 44px touch target. All variants use slightly irregular border-radius for hand-drawn feel and a coloured offset shadow. Loading state replaces children with a spinner. Icon slot available on left or right.

primarysecondaryghostdestructivesuccessicon-onlyloading
variant'primary' | 'secondary' | 'ghost' | 'destructive' | 'success'
size'sm' | 'md' | 'lg'
loadingboolean
disabledboolean
iconReact.ReactNode
fullWidthboolean
onClick() => void

Input

Labelled text input with helper text, error state, and prefix/suffix slots. Currency mode prefixes a $ sign and sets numeric keyboard on mobile. Error state adds a red border and shows an inline message below. Minimum height 44px for touch compliance.

defaultcurrencywith-prefixerrordisabledsearch
labelstring
placeholderstring
valuestring
onChange(v: string) => void
errorstring
helperstring
prefixReact.ReactNode
suffixReact.ReactNode
type'text' | 'email' | 'number' | 'currency'
disabledboolean

ExpenseCard

Displays a single expense row in the expenses list. Left: category emoji icon in a tinted circle. Center: description, CategoryBadge, payer name with avatar, date. Right: total amount bold, current user's share in muted caption. On mobile, swipe-left reveals Edit and Delete action buttons. Recurring expenses show a πŸ”„ badge.

defaultrecurringwith-receiptsettledswiped-open
idstring
descriptionstring
amountnumber
category'rent' | 'utilities' | 'groceries' | 'dining' | 'subscriptions' | 'transport' | 'household' | 'other'
paidByMember
splitMembersMember[]
mySharenumber
dateDate
isRecurringboolean
receiptUrlstring
onEdit() => void
onDelete() => void

AddExpenseDrawer

Primary expense entry UI. Renders as a bottom-sheet drawer on mobile (slides up from bottom, handle bar at top) and a centered modal on desktop. Amount input is large (48px font) and auto-focused on open. Paid-by and split-among sections show circular member avatars as toggles. Split type tabs switch between Equal/Exact/Percentage/Shares. Category picker is a horizontal scrollable pill row. Save CTA shows computed per-person amount in real time.

createeditmobile-drawerdesktop-modal
isOpenboolean
onClose() => void
onSave(expense: NewExpense) => void
membersMember[]
currentUserIdstring
defaultCategoryCategory
editExpenseExpense

BalanceSummaryCard

Shows one member's monthly financial position. Header: avatar + name. Three stat pills: Total Paid, Your Share, Net Balance. Net balance is bold green (positive, owed to them) or bold red (negative, they owe). Used in a 2-column grid on the dashboard and as full-width rows in the settlement screen.

defaultcurrent-usercompactpositive-balancenegative-balancezero-balance
memberMember
totalPaidnumber
totalOwednumber
netBalancenumber
isCurrentUserboolean
compactboolean

SettlementTransactionRow

A single debt-simplification transaction. Shows payer avatar+name on left, dashed arrow with bold amount in centre, receiver avatar+name on right. Below: row of payment deep-link buttons (Venmo blue, PayPal navy, Zelle purple). A checkbox on the far left marks it as settled β€” on check, the row fades and gets a strikethrough with a green Settled badge.

pendingsettledcurrent-user-payscurrent-user-receives
idstring
payerMember
receiverMember
amountnumber
isSettledboolean
onSettle(id: string) => void
venmoUrlstring
paypalUrlstring
zelleUrlstring

FloatingActionButton

56Γ—56px circular button fixed above the bottom nav bar (bottom: 80px, right: 20px). Shows a + icon. On first app visit, plays a gentle scale-pulse animation 3 times to draw attention. Triggers the AddExpenseDrawer. Hidden when AddExpenseDrawer is open to avoid Z-index overlap.

defaultpulsinghidden
onClick() => void
showPulseboolean
isHiddenboolean

MemberAvatarGroup

Renders up to 5 circular member avatars in an overlapping stack (each offset -8px from previous). Each avatar shows a photo or colour-coded initials. In selectable mode (used inside AddExpenseDrawer), clicking an avatar toggles it selected/unselected with an indigo ring and a 1.1Γ— scale. Unselected avatars are 40% opacity.

display-onlyselectablecompact-stackwith-labels
membersMember[]
selectableboolean
selectedstring[]
onToggle(memberId: string) => void
size'sm' | 'md' | 'lg'
showLabelsboolean

Toast

Notification toast that slides down from the top of the viewport. Auto-dismisses after 3 seconds (configurable). Variants use coloured left-border and matching icon. The success variant for expense deletion includes an Undo button that re-creates the expense via optimistic state rollback.

successerrorinfowarningwith-undo
variant'success' | 'error' | 'info' | 'warning'
messagestring
durationnumber
action{ label: string; onClick: () => void }
onDismiss() => void

CategoryBadge

Pill badge for an expense category. Each of the 8 categories has a fixed emoji, background tint, and text colour derived from the category's semantic hue. Supports an icon-only compact mode for tight spaces (e.g. inside ExpenseCard on narrow viewports).

rentutilitiesgroceriesdiningsubscriptionstransporthouseholdothercompact
category'rent' | 'utilities' | 'groceries' | 'dining' | 'subscriptions' | 'transport' | 'household' | 'other'
compactboolean
size'sm' | 'md'

MonthlyBreakdownChart

Two-panel visual summary rendered as an SVG donut chart (left) plus a category legend with amounts (right), and below it a grouped horizontal bar chart comparing each member's total paid vs total share. The donut's centre label shows total month spend. Clicking a category legend row filters the expense list.

fullcompact-donutbar-only
monthstring
categoryTotals{ category: Category; total: number }[]
memberTotals{ member: Member; paid: number; share: number }[]
onCategoryClick(category: Category) => void
compactboolean