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.
activeTab'home' | 'expenses' | 'settle' | 'settings'unreadCountnumberhouseholdNamestringButton
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.
variant'primary' | 'secondary' | 'ghost' | 'destructive' | 'success'size'sm' | 'md' | 'lg'loadingbooleandisabledbooleaniconReact.ReactNodefullWidthbooleanonClick() => voidInput
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.
labelstringplaceholderstringvaluestringonChange(v: string) => voiderrorstringhelperstringprefixReact.ReactNodesuffixReact.ReactNodetype'text' | 'email' | 'number' | 'currency'disabledbooleanExpenseCard
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.
idstringdescriptionstringamountnumbercategory'rent' | 'utilities' | 'groceries' | 'dining' | 'subscriptions' | 'transport' | 'household' | 'other'paidByMembersplitMembersMember[]mySharenumberdateDateisRecurringbooleanreceiptUrlstringonEdit() => voidonDelete() => voidAddExpenseDrawer
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.
isOpenbooleanonClose() => voidonSave(expense: NewExpense) => voidmembersMember[]currentUserIdstringdefaultCategoryCategoryeditExpenseExpenseBalanceSummaryCard
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.
memberMembertotalPaidnumbertotalOwednumbernetBalancenumberisCurrentUserbooleancompactbooleanSettlementTransactionRow
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.
idstringpayerMemberreceiverMemberamountnumberisSettledbooleanonSettle(id: string) => voidvenmoUrlstringpaypalUrlstringzelleUrlstringFloatingActionButton
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.
onClick() => voidshowPulsebooleanisHiddenbooleanMemberAvatarGroup
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.
membersMember[]selectablebooleanselectedstring[]onToggle(memberId: string) => voidsize'sm' | 'md' | 'lg'showLabelsbooleanToast
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.
variant'success' | 'error' | 'info' | 'warning'messagestringdurationnumberaction{ label: string; onClick: () => void }onDismiss() => voidCategoryBadge
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).
category'rent' | 'utilities' | 'groceries' | 'dining' | 'subscriptions' | 'transport' | 'household' | 'other'compactbooleansize'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.
monthstringcategoryTotals{ category: Category; total: number }[]memberTotals{ member: Member; paid: number; share: number }[]onCategoryClick(category: Category) => voidcompactboolean