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:
parent
c3478f4743
commit
ca72437f18
@ -33,6 +33,7 @@ import { useUserPlan } from '../hooks/useUserPlan';
|
|||||||
import { AppHeader } from '../components/AppHeader';
|
import { AppHeader } from '../components/AppHeader';
|
||||||
import { MagicSheet } from '../components/devices/MagicSheet';
|
import { MagicSheet } from '../components/devices/MagicSheet';
|
||||||
import { DeviceSlotDonut, MOBILE_COLOR, DESKTOP_COLOR } from '../components/devices/DeviceSlotDonut';
|
import { DeviceSlotDonut, MOBILE_COLOR, DESKTOP_COLOR } from '../components/devices/DeviceSlotDonut';
|
||||||
|
import { DeviceDistributionBar } from '../components/devices/DeviceDistributionBar';
|
||||||
import { DeviceStatusPill } from '../components/devices/DeviceStatusPill';
|
import { DeviceStatusPill } from '../components/devices/DeviceStatusPill';
|
||||||
import { DeviceDetailSheet, type DeviceDetail } from '../components/devices/DeviceDetailSheet';
|
import { DeviceDetailSheet, type DeviceDetail } from '../components/devices/DeviceDetailSheet';
|
||||||
import { deviceImage } from '../components/devices/deviceIcon';
|
import { deviceImage } from '../components/devices/deviceIcon';
|
||||||
@ -567,9 +568,9 @@ export default function DevicesScreen() {
|
|||||||
}}
|
}}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
{/* Slot-Ringe: Mobil · Gesamt (Verteilung) · Computer */}
|
{/* Zwei Circles (Mobil/Computer) + animierter Gesamt-Verteilungs-Balken */}
|
||||||
<View style={{ marginBottom: -4 }}>
|
<View style={{ gap: 18, marginBottom: -4 }}>
|
||||||
<View style={{ flexDirection: 'row', gap: 8, marginTop: 4 }}>
|
<View style={{ flexDirection: 'row', gap: 12, marginTop: 4 }}>
|
||||||
<DeviceSlotDonut
|
<DeviceSlotDonut
|
||||||
segments={[{ value: mobileCount, color: MOBILE_COLOR }]}
|
segments={[{ value: mobileCount, color: MOBILE_COLOR }]}
|
||||||
total={mobileLimit}
|
total={mobileLimit}
|
||||||
@ -582,17 +583,12 @@ export default function DevicesScreen() {
|
|||||||
atLimit={atDesktopLimit}
|
atLimit={atDesktopLimit}
|
||||||
label={t('devices.progress_desktop')}
|
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>
|
</View>
|
||||||
|
<DeviceDistributionBar
|
||||||
|
mobile={mobileCount}
|
||||||
|
desktop={desktopCount}
|
||||||
|
total={mobileLimit + desktopLimit}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Unified devices section: Mobile zuerst, dann Desktop */}
|
{/* Unified devices section: Mobile zuerst, dann Desktop */}
|
||||||
|
|||||||
108
apps/rebreak-native/components/devices/DeviceDistributionBar.tsx
Normal file
108
apps/rebreak-native/components/devices/DeviceDistributionBar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user