feat(01-01): implement palette.ts as single source of truth for category colors

- Export CategoryType union type with 7 category strings
- Export CategoryShades interface with light/medium/base fields
- Export palette Record with oklch values matching --chart-* CSS tokens
- Export headerGradient() returning CSSProperties with linear-gradient
- Export overviewHeaderGradient() with multi-stop sky/lavender/green gradient
- Export amountColorClass() for text-success/text-warning/text-destructive
- All 20 unit tests pass
This commit is contained in:
2026-03-11 20:52:41 +01:00
parent d5fc10de46
commit 6859b30347

102
frontend/src/lib/palette.ts Normal file
View File

@@ -0,0 +1,102 @@
import type React from 'react'
export type CategoryType =
| 'income'
| 'bill'
| 'variable_expense'
| 'debt'
| 'saving'
| 'investment'
| 'carryover'
export interface CategoryShades {
light: string
medium: string
base: string
}
export const palette: Record<CategoryType, CategoryShades> = {
income: {
light: 'oklch(0.96 0.04 145)',
medium: 'oklch(0.88 0.08 145)',
base: 'oklch(0.76 0.14 145)',
},
bill: {
light: 'oklch(0.96 0.03 250)',
medium: 'oklch(0.88 0.07 250)',
base: 'oklch(0.76 0.12 250)',
},
variable_expense: {
light: 'oklch(0.97 0.04 85)',
medium: 'oklch(0.90 0.08 85)',
base: 'oklch(0.80 0.14 85)',
},
debt: {
light: 'oklch(0.96 0.04 15)',
medium: 'oklch(0.88 0.08 15)',
base: 'oklch(0.76 0.13 15)',
},
saving: {
light: 'oklch(0.95 0.04 280)',
medium: 'oklch(0.87 0.08 280)',
base: 'oklch(0.75 0.13 280)',
},
investment: {
light: 'oklch(0.96 0.04 320)',
medium: 'oklch(0.88 0.07 320)',
base: 'oklch(0.76 0.12 320)',
},
carryover: {
light: 'oklch(0.96 0.03 210)',
medium: 'oklch(0.88 0.06 210)',
base: 'oklch(0.76 0.11 210)',
},
}
/**
* Returns a CSSProperties object with a linear-gradient background
* using the light and medium shades of the given category type.
*/
export function headerGradient(type: CategoryType): React.CSSProperties {
const shades = palette[type]
return {
background: `linear-gradient(to right, ${shades.light}, ${shades.medium})`,
}
}
/**
* Returns a CSSProperties object with a multi-stop linear-gradient
* for the FinancialOverview header (sky via lavender to green).
*/
export function overviewHeaderGradient(): React.CSSProperties {
return {
background: `linear-gradient(to right, ${palette.carryover.light}, ${palette.saving.light}, ${palette.income.light})`,
}
}
export interface AmountColorClassOpts {
type: CategoryType
actual: number
budgeted: number
isIncome?: boolean
isAvailable?: boolean
}
/**
* Returns a Tailwind utility class for coloring a monetary amount:
* - isIncome or isAvailable: positive → 'text-success', negative → 'text-destructive', zero → ''
* - Expense: actual > budgeted → 'text-warning', else → ''
*/
export function amountColorClass(opts: AmountColorClassOpts): string {
const { actual, budgeted, isIncome, isAvailable } = opts
if (isIncome || isAvailable) {
if (actual > 0) return 'text-success'
if (actual < 0) return 'text-destructive'
return ''
}
// Expense path
if (actual > budgeted) return 'text-warning'
return ''
}