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>
This commit is contained in:
chahinebrini 2026-06-08 00:36:04 +02:00
parent c3478f4743
commit ca72437f18
2 changed files with 117 additions and 13 deletions

View File

@ -33,6 +33,7 @@ import { useUserPlan } from '../hooks/useUserPlan';
import { AppHeader } from '../components/AppHeader';
import { MagicSheet } from '../components/devices/MagicSheet';
import { DeviceSlotDonut, MOBILE_COLOR, DESKTOP_COLOR } from '../components/devices/DeviceSlotDonut';
import { DeviceDistributionBar } from '../components/devices/DeviceDistributionBar';
import { DeviceStatusPill } from '../components/devices/DeviceStatusPill';
import { DeviceDetailSheet, type DeviceDetail } from '../components/devices/DeviceDetailSheet';
import { deviceImage } from '../components/devices/deviceIcon';
@ -567,9 +568,9 @@ export default function DevicesScreen() {
}}
showsVerticalScrollIndicator={false}
>
{/* Slot-Ringe: Mobil · Gesamt (Verteilung) · Computer */}
<View style={{ marginBottom: -4 }}>
<View style={{ flexDirection: 'row', gap: 8, marginTop: 4 }}>
{/* Zwei Circles (Mobil/Computer) + animierter Gesamt-Verteilungs-Balken */}
<View style={{ gap: 18, marginBottom: -4 }}>
<View style={{ flexDirection: 'row', gap: 12, marginTop: 4 }}>
<DeviceSlotDonut
segments={[{ value: mobileCount, color: MOBILE_COLOR }]}
total={mobileLimit}
@ -582,17 +583,12 @@ export default function DevicesScreen() {
atLimit={atDesktopLimit}
label={t('devices.progress_desktop')}
/>
<DeviceSlotDonut
half
segments={[
{ value: mobileCount, color: MOBILE_COLOR },
{ value: desktopCount, color: DESKTOP_COLOR },
]}
total={mobileLimit + desktopLimit}
atLimit={mobileCount + desktopCount >= mobileLimit + desktopLimit}
label={t('devices.progress_total')}
/>
</View>
<DeviceDistributionBar
mobile={mobileCount}
desktop={desktopCount}
total={mobileLimit + desktopLimit}
/>
</View>
{/* Unified devices section: Mobile zuerst, dann Desktop */}

View File

@ -0,0 +1,108 @@
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>
);
}