Stripe-style UI overhaul + Expenses feed with month grouping (#15)
## Title Stripe-style UI overhaul + Expenses feed with month grouping ## Summary Rework the khata-ui frontend to a Stripe-grade design system and add a rich, monthly-grouped Expenses experience driven by react-openapi's shared, spec-configured field renderers. ## Highlights ### Design overhaul - New design system: single indigo (#635BFF), 6px radius, Inter type scale, streamlined surfaces/inputs/feedback tokens - Sticky app bar with brand-left header + inline footer, route titles - Split-panel auth pages, Stripe-style Home hero + feature cards - Toast provider + shared PageHeader/EmptyState UI primitives - Admin polish: right-aligned numerics, row-hover actions, skeletons, richer empty states, breadcrumbed fetch-request flows ### Expenses page - Monthly grouped feed with stat cards (total spent / this month / income) - Month cards as single-open accordions with red-spent / green-income totals - Flat transaction rows: logo, name/date, account, amount (no accordion) - Floating month–year selector pill (scroll-spy) that expands + scrolls to the selected month - Strict DD-MM-YYYY date parsing (day-first), no format guessing ### Shared react-openapi fields - New `CurrencyField` + `formatCurrency` (cached Intl, sign-colored, sign-less amounts; form editing reuses NumberField) - `resourceConfig.fieldTypes` override mechanism (amount → currency) - Expenses page rendered via ListCellRenderer / DetailFieldRenderer / applyDisplayFormat — zero custom field renderers - `resolveMediaUrl` resolves relative `/uploads/...` against the API base for every image field (Admin + Expenses logos) ### Fixes - Perf: memoized rows, formatter caches, single-open accordions — cuts slow-click INP and DOM-nesting warnings - Removed unused @mui/x-data-grid@7, which hoisted @mui/system@7 and crashed Box/createTheme at runtime (v5/v7 mix) ## Commits (10) -9808a1fdesign overhaul + expenses page -d6856a5fix expenses list performance + dom nesting -3dd833ashared currency field + expenses rendered from react-openapi fields -e8c585bfix expenses grouping to parse occurred_at as DD-MM-YYYY strictly -cc940eeexpense list: split month totals, sticky headers + month pill -47d799fexpense list: clickable month pill + fix scroll-to-month -537aef9resolve relative media URLs against the API base in react-openapi -8e9a13dexpense list: replace accordion with flat Stripe-style rows -62dd06cexpense list: month accordions + month-year selector; drop unused x-data-grid -806bd42dropped amount sign from CurrencyField.tsx Reviewed-on: #15 Co-authored-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com> Co-committed-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
This commit is contained in:
@@ -35,32 +35,37 @@ const defaultTheme = createTheme();
|
||||
|
||||
const customShadows: Shadows = [...defaultTheme.shadows];
|
||||
|
||||
/**
|
||||
* Stripe-like indigo brand. #635BFF ≈ hsl(243, 76%, 59%).
|
||||
* Single brand hue — no multi-colour accents.
|
||||
*/
|
||||
export const brand = {
|
||||
50: 'hsl(210, 100%, 95%)',
|
||||
100: 'hsl(210, 100%, 92%)',
|
||||
200: 'hsl(210, 100%, 80%)',
|
||||
300: 'hsl(210, 100%, 65%)',
|
||||
400: 'hsl(210, 98%, 48%)',
|
||||
500: 'hsl(210, 98%, 42%)',
|
||||
600: 'hsl(210, 98%, 55%)',
|
||||
700: 'hsl(210, 100%, 35%)',
|
||||
800: 'hsl(210, 100%, 16%)',
|
||||
900: 'hsl(210, 100%, 21%)',
|
||||
50: 'hsl(250, 100%, 98%)',
|
||||
100: 'hsl(250, 95%, 95%)',
|
||||
200: 'hsl(249, 92%, 90%)',
|
||||
300: 'hsl(248, 92%, 80%)',
|
||||
400: 'hsl(245, 90%, 72%)',
|
||||
500: 'hsl(243, 76%, 59%)',
|
||||
600: 'hsl(243, 75%, 51%)',
|
||||
700: 'hsl(243, 78%, 42%)',
|
||||
800: 'hsl(242, 76%, 30%)',
|
||||
900: 'hsl(241, 75%, 20%)',
|
||||
};
|
||||
|
||||
/** Cool, neutral grays. */
|
||||
export const gray = {
|
||||
50: 'hsl(220, 35%, 97%)',
|
||||
100: 'hsl(220, 30%, 94%)',
|
||||
200: 'hsl(220, 20%, 88%)',
|
||||
300: 'hsl(220, 20%, 80%)',
|
||||
400: 'hsl(220, 20%, 65%)',
|
||||
500: 'hsl(220, 20%, 42%)',
|
||||
600: 'hsl(220, 20%, 35%)',
|
||||
700: 'hsl(220, 20%, 25%)',
|
||||
750: 'hsl(220, 20%, 18%)',
|
||||
800: 'hsl(220, 30%, 6%)',
|
||||
850: 'hsl(220, 22%, 11%)',
|
||||
900: 'hsl(220, 35%, 3%)',
|
||||
200: 'hsl(220, 20%, 89%)',
|
||||
300: 'hsl(220, 17%, 80%)',
|
||||
400: 'hsl(220, 15%, 64%)',
|
||||
500: 'hsl(220, 14%, 45%)',
|
||||
600: 'hsl(220, 13%, 36%)',
|
||||
700: 'hsl(220, 13%, 27%)',
|
||||
750: 'hsl(220, 13%, 20%)',
|
||||
800: 'hsl(220, 12%, 12%)',
|
||||
850: 'hsl(220, 10%, 8%)',
|
||||
900: 'hsl(220, 9%, 6%)',
|
||||
};
|
||||
|
||||
export const green = {
|
||||
@@ -116,27 +121,39 @@ export const getDesignTokens = (mode: PaletteMode) => {
|
||||
palette: {
|
||||
mode,
|
||||
primary: {
|
||||
light: brand[200],
|
||||
main: brand[400],
|
||||
light: brand[400],
|
||||
main: brand[500],
|
||||
dark: brand[700],
|
||||
contrastText: brand[50],
|
||||
...(mode === 'dark' && {
|
||||
contrastText: brand[50],
|
||||
light: 'hsl(210, 50%, 65%)',
|
||||
main: 'hsl(210, 55%, 55%)',
|
||||
dark: 'hsl(210, 50%, 35%)',
|
||||
light: 'hsl(245, 85%, 70%)',
|
||||
main: 'hsl(243, 76%, 64%)',
|
||||
dark: 'hsl(243, 78%, 48%)',
|
||||
}),
|
||||
},
|
||||
secondary: {
|
||||
light: gray[300],
|
||||
main: gray[600],
|
||||
dark: gray[800],
|
||||
contrastText: gray[50],
|
||||
...(mode === 'dark' && {
|
||||
light: gray[200],
|
||||
main: gray[400],
|
||||
dark: gray[600],
|
||||
contrastText: gray[900],
|
||||
}),
|
||||
},
|
||||
info: {
|
||||
light: brand[100],
|
||||
main: brand[300],
|
||||
main: brand[400],
|
||||
dark: brand[600],
|
||||
contrastText: gray[50],
|
||||
...(mode === 'dark' && {
|
||||
contrastText: 'hsl(210, 30%, 80%)',
|
||||
light: 'hsl(210, 40%, 50%)',
|
||||
main: 'hsl(210, 35%, 40%)',
|
||||
dark: 'hsl(210, 30%, 25%)',
|
||||
light: 'hsl(245, 60%, 60%)',
|
||||
main: 'hsl(243, 55%, 52%)',
|
||||
dark: 'hsl(243, 50%, 38%)',
|
||||
}),
|
||||
},
|
||||
warning: {
|
||||
@@ -174,7 +191,7 @@ export const getDesignTokens = (mode: PaletteMode) => {
|
||||
},
|
||||
divider: mode === 'dark' ? 'hsla(0, 0%, 100%, 0.08)' : alpha(gray[300], 0.4),
|
||||
background: {
|
||||
default: 'hsl(0, 0%, 99%)',
|
||||
default: 'hsl(0, 0%, 100%)',
|
||||
paper: 'hsl(220, 35%, 97%)',
|
||||
...(mode === 'dark' && { default: darkBg, paper: darkPaper }),
|
||||
},
|
||||
@@ -208,242 +225,73 @@ export const getDesignTokens = (mode: PaletteMode) => {
|
||||
typography: {
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
h1: {
|
||||
fontSize: defaultTheme.typography.pxToRem(48),
|
||||
fontSize: defaultTheme.typography.pxToRem(40),
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: -0.5,
|
||||
lineHeight: 1.1,
|
||||
letterSpacing: '-0.025em',
|
||||
},
|
||||
h2: {
|
||||
fontSize: defaultTheme.typography.pxToRem(36),
|
||||
fontSize: defaultTheme.typography.pxToRem(32),
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: '-0.02em',
|
||||
},
|
||||
h3: {
|
||||
fontSize: defaultTheme.typography.pxToRem(30),
|
||||
lineHeight: 1.2,
|
||||
fontSize: defaultTheme.typography.pxToRem(26),
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.25,
|
||||
letterSpacing: '-0.015em',
|
||||
},
|
||||
h4: {
|
||||
fontSize: defaultTheme.typography.pxToRem(24),
|
||||
fontSize: defaultTheme.typography.pxToRem(20),
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.4,
|
||||
letterSpacing: '-0.01em',
|
||||
},
|
||||
h5: {
|
||||
fontSize: defaultTheme.typography.pxToRem(18),
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.4,
|
||||
letterSpacing: '-0.005em',
|
||||
},
|
||||
h6: {
|
||||
fontSize: defaultTheme.typography.pxToRem(16),
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
h5: {
|
||||
fontSize: defaultTheme.typography.pxToRem(20),
|
||||
fontWeight: 600,
|
||||
},
|
||||
h6: {
|
||||
fontSize: defaultTheme.typography.pxToRem(18),
|
||||
fontWeight: 600,
|
||||
},
|
||||
subtitle1: {
|
||||
fontSize: defaultTheme.typography.pxToRem(18),
|
||||
fontSize: defaultTheme.typography.pxToRem(16),
|
||||
fontWeight: 500,
|
||||
},
|
||||
subtitle2: {
|
||||
fontSize: defaultTheme.typography.pxToRem(14),
|
||||
fontSize: defaultTheme.typography.pxToRem(13),
|
||||
fontWeight: 500,
|
||||
},
|
||||
body1: {
|
||||
fontSize: defaultTheme.typography.pxToRem(14),
|
||||
fontSize: defaultTheme.typography.pxToRem(15),
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
body2: {
|
||||
fontSize: defaultTheme.typography.pxToRem(14),
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.45,
|
||||
},
|
||||
caption: {
|
||||
fontSize: defaultTheme.typography.pxToRem(12),
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.4,
|
||||
},
|
||||
overline: {
|
||||
fontSize: defaultTheme.typography.pxToRem(11),
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.5,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.06em',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 8,
|
||||
borderRadius: 6,
|
||||
},
|
||||
shadows: customShadows,
|
||||
};
|
||||
};
|
||||
|
||||
export const colorSchemes = {
|
||||
light: {
|
||||
palette: {
|
||||
primary: {
|
||||
light: brand[200],
|
||||
main: brand[400],
|
||||
dark: brand[700],
|
||||
contrastText: brand[50],
|
||||
},
|
||||
info: {
|
||||
light: brand[100],
|
||||
main: brand[300],
|
||||
dark: brand[600],
|
||||
contrastText: gray[50],
|
||||
},
|
||||
warning: {
|
||||
light: orange[300],
|
||||
main: orange[400],
|
||||
dark: orange[800],
|
||||
},
|
||||
error: {
|
||||
light: red[300],
|
||||
main: red[400],
|
||||
dark: red[800],
|
||||
},
|
||||
success: {
|
||||
light: green[300],
|
||||
main: green[400],
|
||||
dark: green[800],
|
||||
},
|
||||
grey: {
|
||||
...gray,
|
||||
},
|
||||
divider: alpha(gray[300], 0.4),
|
||||
background: {
|
||||
default: 'hsl(0, 0%, 99%)',
|
||||
paper: 'hsl(220, 35%, 97%)',
|
||||
},
|
||||
text: {
|
||||
primary: gray[800],
|
||||
secondary: gray[600],
|
||||
warning: orange[400],
|
||||
},
|
||||
action: {
|
||||
hover: alpha(gray[200], 0.2),
|
||||
selected: `${alpha(gray[200], 0.3)}`,
|
||||
},
|
||||
flows: {
|
||||
outflows: {
|
||||
primary: '#d32f2f',
|
||||
surface: '#fdecea',
|
||||
text: '#b71c1c',
|
||||
},
|
||||
inflows: {
|
||||
primary: '#2e7d32',
|
||||
surface: '#e8f5e9',
|
||||
text: '#1b5e20',
|
||||
},
|
||||
},
|
||||
baseShadow:
|
||||
'hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
palette: {
|
||||
primary: {
|
||||
contrastText: brand[50],
|
||||
light: 'hsl(210, 50%, 65%)',
|
||||
main: 'hsl(210, 55%, 55%)',
|
||||
dark: 'hsl(210, 50%, 35%)',
|
||||
},
|
||||
info: {
|
||||
contrastText: 'hsl(210, 30%, 80%)',
|
||||
light: 'hsl(210, 40%, 50%)',
|
||||
main: 'hsl(210, 35%, 40%)',
|
||||
dark: 'hsl(210, 30%, 25%)',
|
||||
},
|
||||
warning: {
|
||||
light: 'hsl(45, 60%, 55%)',
|
||||
main: 'hsl(45, 55%, 45%)',
|
||||
dark: 'hsl(45, 50%, 30%)',
|
||||
},
|
||||
error: {
|
||||
light: 'hsl(0, 55%, 60%)',
|
||||
main: 'hsl(0, 55%, 50%)',
|
||||
dark: 'hsl(0, 50%, 35%)',
|
||||
},
|
||||
success: {
|
||||
light: 'hsl(120, 40%, 55%)',
|
||||
main: 'hsl(120, 40%, 45%)',
|
||||
dark: 'hsl(120, 35%, 30%)',
|
||||
},
|
||||
grey: {
|
||||
...gray,
|
||||
},
|
||||
divider: 'hsla(0, 0%, 100%, 0.08)',
|
||||
background: {
|
||||
default: darkBg,
|
||||
paper: darkPaper,
|
||||
},
|
||||
text: {
|
||||
primary: 'hsl(0, 0%, 92%)',
|
||||
secondary: 'hsl(0, 0%, 60%)',
|
||||
},
|
||||
action: {
|
||||
hover: 'hsla(0, 0%, 100%, 0.06)',
|
||||
selected: 'hsla(0, 0%, 100%, 0.1)',
|
||||
},
|
||||
flows: {
|
||||
outflows: {
|
||||
primary: 'hsl(0, 55%, 60%)',
|
||||
surface: 'hsla(0, 35%, 25%, 0.6)',
|
||||
text: 'hsl(0, 60%, 80%)',
|
||||
},
|
||||
inflows: {
|
||||
primary: 'hsl(120, 40%, 55%)',
|
||||
surface: 'hsla(120, 25%, 22%, 0.6)',
|
||||
text: 'hsl(120, 40%, 78%)',
|
||||
},
|
||||
},
|
||||
baseShadow: '0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.3)',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const typography = {
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
h1: {
|
||||
fontSize: defaultTheme.typography.pxToRem(48),
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
h2: {
|
||||
fontSize: defaultTheme.typography.pxToRem(36),
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
},
|
||||
h3: {
|
||||
fontSize: defaultTheme.typography.pxToRem(30),
|
||||
lineHeight: 1.2,
|
||||
},
|
||||
h4: {
|
||||
fontSize: defaultTheme.typography.pxToRem(24),
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
h5: {
|
||||
fontSize: defaultTheme.typography.pxToRem(20),
|
||||
fontWeight: 600,
|
||||
},
|
||||
h6: {
|
||||
fontSize: defaultTheme.typography.pxToRem(18),
|
||||
fontWeight: 600,
|
||||
},
|
||||
subtitle1: {
|
||||
fontSize: defaultTheme.typography.pxToRem(18),
|
||||
},
|
||||
subtitle2: {
|
||||
fontSize: defaultTheme.typography.pxToRem(14),
|
||||
fontWeight: 500,
|
||||
},
|
||||
body1: {
|
||||
fontSize: defaultTheme.typography.pxToRem(14),
|
||||
},
|
||||
body2: {
|
||||
fontSize: defaultTheme.typography.pxToRem(14),
|
||||
fontWeight: 400,
|
||||
},
|
||||
caption: {
|
||||
fontSize: defaultTheme.typography.pxToRem(12),
|
||||
fontWeight: 400,
|
||||
},
|
||||
};
|
||||
|
||||
export const shape = {
|
||||
borderRadius: 8,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const defaultShadows: Shadows = [
|
||||
'none',
|
||||
'var(--template-palette-baseShadow)',
|
||||
...defaultTheme.shadows.slice(2),
|
||||
];
|
||||
export const shadows = defaultShadows;
|
||||
};
|
||||
Reference in New Issue
Block a user