chahinebrini ca72437f18 fix(native): zwei Circles + animierter Gesamt-Verteilungs-Balken drunter
Statt Half-Donut (Höhen-Mismatch mit Circles): zwei volle Circles (Mobil/Computer)
+ darunter ein eigener animierter Balken (grün/blau-Segmente, gleiche Easing/Dauer
wie die Ringe) mit Legende. Kein native-Default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 00:36:04 +02:00

109 lines
3.2 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { Animated, Easing, Text, View } from 'react-native';
import { useTranslation } from 'react-i18next';
import { useColors } from '../../lib/theme';
import { MOBILE_COLOR, DESKTOP_COLOR } from './DeviceSlotDonut';
/**
* Animierter Verteilungs-Balken (Gesamt) — gleiche Design-Sprache wie die
* Slot-Ringe (grün=Mobil, blau=Computer, gleiche Easing/Dauer). Sitzt unter
* den beiden Circles und zeigt die Aufteilung mobil/stationär.
*/
export function DeviceDistributionBar({
mobile,
desktop,
total,
}: {
mobile: number;
desktop: number;
total: number;
}) {
const { t } = useTranslation();
const colors = useColors();
const safeTotal = Math.max(1, total);
const used = mobile + desktop;
const anim = useRef(new Animated.Value(0)).current;
useEffect(() => {
anim.setValue(0);
Animated.timing(anim, {
toValue: 1,
duration: 1400,
easing: Easing.out(Easing.cubic),
useNativeDriver: false,
}).start();
}, [mobile, desktop, total, anim]);
const mobileW = anim.interpolate({
inputRange: [0, 1],
outputRange: ['0%', `${(mobile / safeTotal) * 100}%`],
});
const desktopW = anim.interpolate({
inputRange: [0, 1],
outputRange: ['0%', `${(desktop / safeTotal) * 100}%`],
});
return (
<View style={{ gap: 8 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text
style={{
fontSize: 11,
color: colors.textMuted,
fontFamily: 'Nunito_700Bold',
textTransform: 'uppercase',
letterSpacing: 0.8,
}}
>
{t('devices.progress_total')}
</Text>
<Text style={{ fontSize: 14, color: colors.text, fontFamily: 'Nunito_900Black', letterSpacing: -0.3 }}>
{used}
<Text style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_700Bold' }}>
/{total}
</Text>
</Text>
</View>
<View
style={{
flexDirection: 'row',
height: 10,
borderRadius: 5,
backgroundColor: colors.surfaceElevated,
overflow: 'hidden',
}}
>
<Animated.View style={{ width: mobileW, height: '100%', backgroundColor: MOBILE_COLOR }} />
<Animated.View style={{ width: desktopW, height: '100%', backgroundColor: DESKTOP_COLOR }} />
</View>
<View style={{ flexDirection: 'row', gap: 16 }}>
<Legend color={MOBILE_COLOR} label={t('devices.progress_mobile')} count={mobile} colors={colors} />
<Legend color={DESKTOP_COLOR} label={t('devices.progress_desktop')} count={desktop} colors={colors} />
</View>
</View>
);
}
function Legend({
color,
label,
count,
colors,
}: {
color: string;
label: string;
count: number;
colors: ReturnType<typeof useColors>;
}) {
return (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: color }} />
<Text style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_600SemiBold' }}>
{label} {count}
</Text>
</View>
);
}