mirror of
https://github.com/arkorty/Expensso.git
synced 2026-03-17 16:41:43 +00:00
640 lines
21 KiB
TypeScript
640 lines
21 KiB
TypeScript
/**
|
|
* NetWorthScreen — MD3 refactored.
|
|
* Replaces system Modals with CustomBottomSheet.
|
|
* Uses MD3 theme tokens (useTheme), Reanimated animations, and haptic feedback.
|
|
*/
|
|
|
|
import React, {useCallback, useRef, useState} from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
Pressable,
|
|
Alert,
|
|
RefreshControl,
|
|
StatusBar,
|
|
} from 'react-native';
|
|
import {useFocusEffect} from '@react-navigation/native';
|
|
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
|
import {useTranslation} from 'react-i18next';
|
|
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
|
import {LineChart} from 'react-native-gifted-charts';
|
|
import Animated, {FadeIn, FadeInDown, FadeInUp} from 'react-native-reanimated';
|
|
|
|
import {
|
|
SectionHeader,
|
|
EmptyState,
|
|
CustomBottomSheet,
|
|
BottomSheetInput,
|
|
BottomSheetChipSelector,
|
|
triggerHaptic,
|
|
} from '../components';
|
|
import type {CustomBottomSheetHandle} from '../components';
|
|
import {useNetWorthStore, useSettingsStore} from '../store';
|
|
import {getNetWorthHistory} from '../db';
|
|
import {formatCurrency, formatCompact} from '../utils';
|
|
import {useTheme} from '../theme';
|
|
import type {MD3Theme} from '../theme';
|
|
import {Asset, AssetType, Liability, LiabilityType, NetWorthSnapshot} from '../types';
|
|
|
|
const ASSET_TYPES: AssetType[] = [
|
|
'Bank', 'Stocks', 'Gold', 'EPF', 'Mutual Funds', 'Fixed Deposit', 'PPF', 'Real Estate', 'Other',
|
|
];
|
|
const LIABILITY_TYPES: LiabilityType[] = [
|
|
'Home Loan', 'Car Loan', 'Personal Loan', 'Education Loan', 'Credit Card', 'Other',
|
|
];
|
|
|
|
const ASSET_ICONS: Record<AssetType, string> = {
|
|
Bank: 'bank',
|
|
Stocks: 'chart-line',
|
|
Gold: 'gold',
|
|
EPF: 'shield-account',
|
|
'Real Estate': 'home-city',
|
|
'Mutual Funds': 'chart-areaspline',
|
|
'Fixed Deposit': 'safe',
|
|
PPF: 'piggy-bank',
|
|
Other: 'dots-horizontal',
|
|
};
|
|
|
|
const ASSET_ICON_COLORS: Record<AssetType, string> = {
|
|
Bank: '#1E88E5',
|
|
Stocks: '#7E57C2',
|
|
Gold: '#D4AF37',
|
|
EPF: '#00ACC1',
|
|
'Real Estate': '#8D6E63',
|
|
'Mutual Funds': '#26A69A',
|
|
'Fixed Deposit': '#3949AB',
|
|
PPF: '#43A047',
|
|
Other: '#78909C',
|
|
};
|
|
|
|
const LIABILITY_ICON_COLORS: Record<LiabilityType, {icon: string; color: string}> = {
|
|
'Home Loan': {icon: 'home-city', color: '#EF6C00'},
|
|
'Car Loan': {icon: 'car', color: '#5E35B1'},
|
|
'Personal Loan': {icon: 'account-cash', color: '#E53935'},
|
|
'Education Loan': {icon: 'school', color: '#1E88E5'},
|
|
'Credit Card': {icon: 'credit-card', color: '#D81B60'},
|
|
Other: {icon: 'alert-circle-outline', color: '#757575'},
|
|
};
|
|
|
|
const NetWorthScreen: React.FC = () => {
|
|
const {t} = useTranslation();
|
|
const baseCurrency = useSettingsStore(s => s.baseCurrency);
|
|
const theme = useTheme();
|
|
const s = makeStyles(theme);
|
|
const {colors, spacing, shape} = theme;
|
|
const insets = useSafeAreaInsets();
|
|
|
|
const assetSheetRef = useRef<CustomBottomSheetHandle>(null);
|
|
const liabilitySheetRef = useRef<CustomBottomSheetHandle>(null);
|
|
|
|
const {
|
|
assets,
|
|
liabilities,
|
|
totalAssets,
|
|
totalLiabilities,
|
|
netWorth,
|
|
isLoading,
|
|
loadNetWorth,
|
|
addAsset,
|
|
removeAsset,
|
|
editAsset,
|
|
addLiability,
|
|
removeLiability,
|
|
editLiability,
|
|
takeSnapshot,
|
|
} = useNetWorthStore();
|
|
|
|
const [history, setHistory] = useState<NetWorthSnapshot[]>([]);
|
|
const [editingAsset, setEditingAsset] = useState<Asset | null>(null);
|
|
const [editingLiability, setEditingLiability] = useState<Liability | null>(null);
|
|
|
|
// Asset form
|
|
const [assetName, setAssetName] = useState('');
|
|
const [assetType, setAssetType] = useState<AssetType>('Bank');
|
|
const [assetValue, setAssetValue] = useState('');
|
|
const [assetNote, setAssetNote] = useState('');
|
|
|
|
// Liability form
|
|
const [liabName, setLiabName] = useState('');
|
|
const [liabType, setLiabType] = useState<LiabilityType>('Home Loan');
|
|
const [liabAmount, setLiabAmount] = useState('');
|
|
const [liabRate, setLiabRate] = useState('');
|
|
const [liabEmi, setLiabEmi] = useState('');
|
|
const [liabNote, setLiabNote] = useState('');
|
|
|
|
const loadAll = useCallback(async () => {
|
|
await loadNetWorth();
|
|
const hist = await getNetWorthHistory(12);
|
|
setHistory(hist);
|
|
}, [loadNetWorth]);
|
|
|
|
useFocusEffect(useCallback(() => { loadAll(); }, [loadAll]));
|
|
|
|
// Chart data
|
|
const lineData = history.map(snap => ({
|
|
value: snap.netWorth,
|
|
label: snap.snapshotDate.slice(5, 10),
|
|
dataPointText: formatCompact(snap.netWorth, baseCurrency),
|
|
}));
|
|
|
|
// Asset type chip options
|
|
const assetTypeOptions = ASSET_TYPES.map(at => ({
|
|
value: at,
|
|
label: at,
|
|
icon: ASSET_ICONS[at],
|
|
}));
|
|
|
|
const liabTypeOptions = LIABILITY_TYPES.map(lt => ({
|
|
value: lt,
|
|
label: lt,
|
|
}));
|
|
|
|
// ── Save Asset ──────────────────────────────────────────────────
|
|
|
|
const handleSaveAsset = async () => {
|
|
const val = parseFloat(assetValue);
|
|
if (!assetName.trim() || isNaN(val) || val <= 0) {
|
|
Alert.alert('Invalid', 'Please enter a valid name and value.');
|
|
return;
|
|
}
|
|
if (editingAsset) {
|
|
await editAsset(editingAsset.id, {
|
|
name: assetName.trim(),
|
|
type: assetType,
|
|
currentValue: val,
|
|
note: assetNote,
|
|
});
|
|
} else {
|
|
await addAsset({
|
|
name: assetName.trim(),
|
|
type: assetType,
|
|
currentValue: val,
|
|
currency: baseCurrency,
|
|
note: assetNote,
|
|
});
|
|
}
|
|
triggerHaptic('notificationSuccess');
|
|
await takeSnapshot(baseCurrency);
|
|
setAssetName(''); setAssetValue(''); setAssetNote(''); setEditingAsset(null);
|
|
assetSheetRef.current?.dismiss();
|
|
loadAll();
|
|
};
|
|
|
|
// ── Save Liability ──────────────────────────────────────────────
|
|
|
|
const handleSaveLiability = async () => {
|
|
const amt = parseFloat(liabAmount);
|
|
if (!liabName.trim() || isNaN(amt) || amt <= 0) {
|
|
Alert.alert('Invalid', 'Please enter a valid name and amount.');
|
|
return;
|
|
}
|
|
if (editingLiability) {
|
|
await editLiability(editingLiability.id, {
|
|
name: liabName.trim(),
|
|
type: liabType,
|
|
outstandingAmount: amt,
|
|
interestRate: parseFloat(liabRate) || 0,
|
|
emiAmount: parseFloat(liabEmi) || 0,
|
|
note: liabNote,
|
|
});
|
|
} else {
|
|
await addLiability({
|
|
name: liabName.trim(),
|
|
type: liabType,
|
|
outstandingAmount: amt,
|
|
currency: baseCurrency,
|
|
interestRate: parseFloat(liabRate) || 0,
|
|
emiAmount: parseFloat(liabEmi) || 0,
|
|
note: liabNote,
|
|
});
|
|
}
|
|
triggerHaptic('notificationSuccess');
|
|
await takeSnapshot(baseCurrency);
|
|
setLiabName(''); setLiabAmount(''); setLiabRate('');
|
|
setLiabEmi(''); setLiabNote(''); setEditingLiability(null);
|
|
liabilitySheetRef.current?.dismiss();
|
|
loadAll();
|
|
};
|
|
|
|
// ── Delete / Edit handlers ──────────────────────────────────────
|
|
|
|
const handleDeleteAsset = (asset: Asset) => {
|
|
Alert.alert('Delete Asset', `Remove "${asset.name}"?`, [
|
|
{text: 'Cancel', style: 'cancel'},
|
|
{text: 'Delete', style: 'destructive', onPress: () => {
|
|
triggerHaptic('impactMedium');
|
|
removeAsset(asset.id);
|
|
}},
|
|
]);
|
|
};
|
|
|
|
const handleDeleteLiability = (liab: Liability) => {
|
|
Alert.alert('Delete Liability', `Remove "${liab.name}"?`, [
|
|
{text: 'Cancel', style: 'cancel'},
|
|
{text: 'Delete', style: 'destructive', onPress: () => {
|
|
triggerHaptic('impactMedium');
|
|
removeLiability(liab.id);
|
|
}},
|
|
]);
|
|
};
|
|
|
|
const handleEditAsset = (asset: Asset) => {
|
|
setEditingAsset(asset);
|
|
setAssetName(asset.name);
|
|
setAssetType(asset.type as AssetType);
|
|
setAssetValue(asset.currentValue.toString());
|
|
setAssetNote(asset.note || '');
|
|
assetSheetRef.current?.present();
|
|
};
|
|
|
|
const handleEditLiability = (liab: Liability) => {
|
|
setEditingLiability(liab);
|
|
setLiabName(liab.name);
|
|
setLiabType(liab.type as LiabilityType);
|
|
setLiabAmount(liab.outstandingAmount.toString());
|
|
setLiabRate(liab.interestRate.toString());
|
|
setLiabEmi(liab.emiAmount.toString());
|
|
setLiabNote(liab.note || '');
|
|
liabilitySheetRef.current?.present();
|
|
};
|
|
|
|
const handleOpenAddAsset = () => {
|
|
setEditingAsset(null);
|
|
setAssetName(''); setAssetType('Bank'); setAssetValue(''); setAssetNote('');
|
|
assetSheetRef.current?.present();
|
|
};
|
|
|
|
const handleOpenAddLiability = () => {
|
|
setEditingLiability(null);
|
|
setLiabName(''); setLiabType('Home Loan'); setLiabAmount('');
|
|
setLiabRate(''); setLiabEmi(''); setLiabNote('');
|
|
liabilitySheetRef.current?.present();
|
|
};
|
|
|
|
// ── Render ──────────────────────────────────────────────────────
|
|
|
|
return (
|
|
<SafeAreaView style={s.screen} edges={['top', 'left', 'right']}>
|
|
<StatusBar
|
|
barStyle={theme.isDark ? 'light-content' : 'dark-content'}
|
|
backgroundColor={colors.background}
|
|
/>
|
|
<Animated.View entering={FadeIn.duration(300)} style={s.header}>
|
|
<Text style={s.headerTitle}>{t('netWorth.title')}</Text>
|
|
</Animated.View>
|
|
|
|
<ScrollView
|
|
style={{flex: 1}}
|
|
contentContainerStyle={{paddingBottom: 60 + insets.bottom}}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={<RefreshControl refreshing={isLoading} onRefresh={loadAll} />}>
|
|
|
|
{/* Hero Card */}
|
|
<Animated.View entering={FadeInDown.springify().damping(18)} style={s.heroCard}>
|
|
<Text style={s.heroLabel}>{t('dashboard.netWorth')}</Text>
|
|
<Text
|
|
style={[s.heroValue, {color: netWorth >= 0 ? colors.success : colors.error}]}
|
|
numberOfLines={1}
|
|
adjustsFontSizeToFit>
|
|
{formatCurrency(netWorth, baseCurrency)}
|
|
</Text>
|
|
<View style={s.heroSplit}>
|
|
<View style={s.heroSplitItem}>
|
|
<Icon name="trending-up" size={16} color={colors.success} />
|
|
<Text style={[s.heroSplitLabel, {color: colors.onSurfaceVariant}]}>Assets</Text>
|
|
<Text style={[s.heroSplitValue, {color: colors.success}]}>
|
|
{formatCurrency(totalAssets, baseCurrency)}
|
|
</Text>
|
|
</View>
|
|
<View style={[s.heroSplitDivider, {backgroundColor: colors.outlineVariant}]} />
|
|
<View style={s.heroSplitItem}>
|
|
<Icon name="trending-down" size={16} color={colors.error} />
|
|
<Text style={[s.heroSplitLabel, {color: colors.onSurfaceVariant}]}>Liabilities</Text>
|
|
<Text style={[s.heroSplitValue, {color: colors.error}]}>
|
|
{formatCurrency(totalLiabilities, baseCurrency)}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</Animated.View>
|
|
|
|
{/* Chart */}
|
|
{lineData.length > 1 && (
|
|
<Animated.View entering={FadeInUp.duration(400).delay(100)}>
|
|
<SectionHeader title={t('netWorth.growth')} />
|
|
<View style={s.chartCard}>
|
|
<LineChart
|
|
data={lineData}
|
|
curved
|
|
color={colors.primary}
|
|
thickness={2}
|
|
dataPointsColor={colors.primary}
|
|
startFillColor={colors.primary}
|
|
endFillColor={colors.primary + '05'}
|
|
areaChart
|
|
yAxisTextStyle={{fontSize: 11, color: colors.onSurfaceVariant}}
|
|
xAxisLabelTextStyle={{fontSize: 11, color: colors.onSurfaceVariant}}
|
|
hideRules
|
|
yAxisThickness={0}
|
|
xAxisThickness={0}
|
|
noOfSections={4}
|
|
isAnimated
|
|
/>
|
|
</View>
|
|
</Animated.View>
|
|
)}
|
|
|
|
{/* Assets List */}
|
|
<Animated.View entering={FadeInUp.duration(400).delay(200)}>
|
|
<SectionHeader
|
|
title={t('netWorth.assets')}
|
|
actionLabel={t('common.add')}
|
|
onAction={handleOpenAddAsset}
|
|
/>
|
|
<View style={s.listCard}>
|
|
{assets.length > 0 ? (
|
|
assets.map((asset, idx) => {
|
|
const iconColor = ASSET_ICON_COLORS[asset.type as AssetType] || colors.primary;
|
|
const iconName = ASSET_ICONS[asset.type as AssetType] || 'dots-horizontal';
|
|
return (
|
|
<Pressable
|
|
key={asset.id}
|
|
style={[s.listItem, idx < assets.length - 1 && s.listItemBorder]}
|
|
onPress={() => handleEditAsset(asset)}
|
|
onLongPress={() => handleDeleteAsset(asset)}
|
|
android_ripple={{color: colors.primary + '12'}}>
|
|
<View style={[s.listIcon, {backgroundColor: iconColor + '1A'}]}>
|
|
<Icon name={iconName} size={20} color={iconColor} />
|
|
</View>
|
|
<View style={s.listDetails}>
|
|
<Text style={s.listName}>{asset.name}</Text>
|
|
<Text style={s.listType}>{asset.type}</Text>
|
|
</View>
|
|
<Text style={[s.listValue, {color: colors.success}]}>
|
|
{formatCurrency(asset.currentValue, baseCurrency)}
|
|
</Text>
|
|
</Pressable>
|
|
);
|
|
})
|
|
) : (
|
|
<EmptyState icon="wallet-plus" title={t('netWorth.noAssets')} />
|
|
)}
|
|
</View>
|
|
</Animated.View>
|
|
|
|
{/* Liabilities List */}
|
|
<Animated.View entering={FadeInUp.duration(400).delay(300)}>
|
|
<SectionHeader
|
|
title={t('netWorth.liabilities')}
|
|
actionLabel={t('common.add')}
|
|
onAction={handleOpenAddLiability}
|
|
/>
|
|
<View style={s.listCard}>
|
|
{liabilities.length > 0 ? (
|
|
liabilities.map((liab, idx) => {
|
|
const liabVisual = LIABILITY_ICON_COLORS[liab.type as LiabilityType] || LIABILITY_ICON_COLORS.Other;
|
|
return (
|
|
<Pressable
|
|
key={liab.id}
|
|
style={[s.listItem, idx < liabilities.length - 1 && s.listItemBorder]}
|
|
onPress={() => handleEditLiability(liab)}
|
|
onLongPress={() => handleDeleteLiability(liab)}
|
|
android_ripple={{color: colors.primary + '12'}}>
|
|
<View style={[s.listIcon, {backgroundColor: liabVisual.color + '1A'}]}>
|
|
<Icon name={liabVisual.icon} size={20} color={liabVisual.color} />
|
|
</View>
|
|
<View style={s.listDetails}>
|
|
<Text style={s.listName}>{liab.name}</Text>
|
|
<Text style={s.listType}>
|
|
{liab.type} · {liab.interestRate}% · EMI {formatCompact(liab.emiAmount, baseCurrency)}
|
|
</Text>
|
|
</View>
|
|
<Text style={[s.listValue, {color: colors.error}]}>
|
|
{formatCurrency(liab.outstandingAmount, baseCurrency)}
|
|
</Text>
|
|
</Pressable>
|
|
);
|
|
})
|
|
) : (
|
|
<EmptyState icon="credit-card-off" title={t('netWorth.noLiabilities')} />
|
|
)}
|
|
</View>
|
|
</Animated.View>
|
|
</ScrollView>
|
|
|
|
{/* ── Asset Bottom Sheet ─────────────────────────────────────── */}
|
|
<CustomBottomSheet
|
|
ref={assetSheetRef}
|
|
title={editingAsset ? 'Edit Asset' : t('netWorth.addAsset')}
|
|
snapPoints={['80%']}
|
|
headerLeft={{label: t('common.cancel'), onPress: () => assetSheetRef.current?.dismiss()}}
|
|
headerRight={{label: t('common.save'), onPress: handleSaveAsset}}>
|
|
|
|
<BottomSheetInput
|
|
label={t('netWorth.assetName')}
|
|
value={assetName}
|
|
onChangeText={setAssetName}
|
|
placeholder="e.g. HDFC Savings"
|
|
/>
|
|
|
|
<BottomSheetChipSelector
|
|
label={t('netWorth.assetType')}
|
|
options={assetTypeOptions}
|
|
selected={assetType}
|
|
onSelect={at => setAssetType(at as AssetType)}
|
|
/>
|
|
|
|
<BottomSheetInput
|
|
label={t('netWorth.currentValue')}
|
|
value={assetValue}
|
|
onChangeText={setAssetValue}
|
|
placeholder="0"
|
|
keyboardType="decimal-pad"
|
|
prefix={baseCurrency === 'INR' ? '\u20B9' : baseCurrency === 'USD' ? '$' : '\u20AC'}
|
|
/>
|
|
|
|
<BottomSheetInput
|
|
label="Note"
|
|
value={assetNote}
|
|
onChangeText={setAssetNote}
|
|
placeholder="Optional note"
|
|
/>
|
|
</CustomBottomSheet>
|
|
|
|
{/* ── Liability Bottom Sheet ─────────────────────────────────── */}
|
|
<CustomBottomSheet
|
|
ref={liabilitySheetRef}
|
|
title={editingLiability ? 'Edit Liability' : t('netWorth.addLiability')}
|
|
snapPoints={['90%']}
|
|
headerLeft={{label: t('common.cancel'), onPress: () => liabilitySheetRef.current?.dismiss()}}
|
|
headerRight={{label: t('common.save'), onPress: handleSaveLiability}}>
|
|
|
|
<BottomSheetInput
|
|
label={t('netWorth.liabilityName')}
|
|
value={liabName}
|
|
onChangeText={setLiabName}
|
|
placeholder="e.g. SBI Home Loan"
|
|
/>
|
|
|
|
<BottomSheetChipSelector
|
|
label={t('netWorth.liabilityType')}
|
|
options={liabTypeOptions}
|
|
selected={liabType}
|
|
onSelect={lt => setLiabType(lt as LiabilityType)}
|
|
/>
|
|
|
|
<BottomSheetInput
|
|
label={t('netWorth.outstandingAmount')}
|
|
value={liabAmount}
|
|
onChangeText={setLiabAmount}
|
|
placeholder="0"
|
|
keyboardType="decimal-pad"
|
|
prefix={baseCurrency === 'INR' ? '\u20B9' : baseCurrency === 'USD' ? '$' : '\u20AC'}
|
|
/>
|
|
|
|
<BottomSheetInput
|
|
label={t('netWorth.interestRate')}
|
|
value={liabRate}
|
|
onChangeText={setLiabRate}
|
|
placeholder="e.g. 8.5"
|
|
keyboardType="decimal-pad"
|
|
prefix="%"
|
|
/>
|
|
|
|
<BottomSheetInput
|
|
label={t('netWorth.emiAmount')}
|
|
value={liabEmi}
|
|
onChangeText={setLiabEmi}
|
|
placeholder="0"
|
|
keyboardType="decimal-pad"
|
|
prefix={baseCurrency === 'INR' ? '\u20B9' : baseCurrency === 'USD' ? '$' : '\u20AC'}
|
|
/>
|
|
|
|
<BottomSheetInput
|
|
label="Note"
|
|
value={liabNote}
|
|
onChangeText={setLiabNote}
|
|
placeholder="Optional note"
|
|
/>
|
|
</CustomBottomSheet>
|
|
</SafeAreaView>
|
|
);
|
|
};
|
|
|
|
export default NetWorthScreen;
|
|
|
|
// ─── Styles ────────────────────────────────────────────────────────────
|
|
|
|
function makeStyles(theme: MD3Theme) {
|
|
const {colors, typography, elevation, shape, spacing} = theme;
|
|
return StyleSheet.create({
|
|
screen: {flex: 1, backgroundColor: colors.background},
|
|
header: {
|
|
paddingHorizontal: spacing.xl,
|
|
paddingTop: spacing.lg,
|
|
paddingBottom: spacing.sm,
|
|
},
|
|
headerTitle: {
|
|
...typography.headlineMedium,
|
|
color: colors.onSurface,
|
|
fontWeight: '700',
|
|
},
|
|
heroCard: {
|
|
backgroundColor: colors.surfaceContainerLow,
|
|
marginHorizontal: spacing.xl,
|
|
marginTop: spacing.md,
|
|
borderRadius: shape.extraLarge,
|
|
borderWidth: 1.5,
|
|
borderColor: colors.outlineVariant,
|
|
padding: spacing.xl,
|
|
...elevation.level2,
|
|
},
|
|
heroLabel: {
|
|
...typography.labelSmall,
|
|
color: colors.onSurfaceVariant,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 1,
|
|
},
|
|
heroValue: {
|
|
...typography.displaySmall,
|
|
fontWeight: '800',
|
|
marginTop: spacing.xs,
|
|
},
|
|
heroSplit: {
|
|
flexDirection: 'row',
|
|
marginTop: spacing.lg,
|
|
paddingTop: spacing.lg,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.outlineVariant + '40',
|
|
},
|
|
heroSplitItem: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
gap: 2,
|
|
},
|
|
heroSplitDivider: {
|
|
width: 1,
|
|
marginHorizontal: spacing.md,
|
|
},
|
|
heroSplitLabel: {
|
|
...typography.bodySmall,
|
|
},
|
|
heroSplitValue: {
|
|
...typography.titleSmall,
|
|
fontWeight: '700',
|
|
},
|
|
chartCard: {
|
|
backgroundColor: colors.surfaceContainerLow,
|
|
marginHorizontal: spacing.xl,
|
|
borderRadius: shape.large,
|
|
borderWidth: 1,
|
|
borderColor: colors.outlineVariant,
|
|
padding: spacing.xl,
|
|
alignItems: 'center',
|
|
...elevation.level1,
|
|
},
|
|
listCard: {
|
|
backgroundColor: colors.surfaceContainerLow,
|
|
marginHorizontal: spacing.xl,
|
|
borderRadius: shape.large,
|
|
borderWidth: 1,
|
|
borderColor: colors.outlineVariant,
|
|
overflow: 'hidden',
|
|
...elevation.level1,
|
|
},
|
|
listItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingVertical: spacing.lg,
|
|
paddingHorizontal: spacing.lg,
|
|
},
|
|
listItemBorder: {
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.outlineVariant + '30',
|
|
},
|
|
listIcon: {
|
|
width: 42,
|
|
height: 42,
|
|
borderRadius: 4,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
listDetails: {flex: 1, marginLeft: spacing.md},
|
|
listName: {
|
|
...typography.bodyLarge,
|
|
color: colors.onSurface,
|
|
fontWeight: '600',
|
|
},
|
|
listType: {
|
|
...typography.bodySmall,
|
|
color: colors.onSurfaceVariant,
|
|
marginTop: 2,
|
|
},
|
|
listValue: {
|
|
...typography.titleSmall,
|
|
fontWeight: '700',
|
|
},
|
|
});
|
|
}
|