feat(native): replace device text-counter with animated progress bar

- DeviceProgressBar component: 6px pill-bar, Animated.timing (380ms) on count change, brandOrange at limit / success otherwise
- devices.tsx: swaps counterText block for <DeviceProgressBar> (Legend-only gating preserved)
- locales (de/en/fr): counter_some/counter_limit → progress_label + progress_at_limit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-15 23:37:28 +02:00
parent 701e32c36e
commit a9fb9273b8
5 changed files with 91 additions and 28 deletions

View File

@ -21,6 +21,7 @@ import { useUserPlan } from '../hooks/useUserPlan';
import { AppHeader } from '../components/AppHeader';
import { AddMacSheet } from '../components/devices/AddMacSheet';
import { AddWindowsSheet } from '../components/devices/AddWindowsSheet';
import { DeviceProgressBar } from '../components/devices/DeviceProgressBar';
// ─── Helpers ─────────────────────────────────────────────────────────────────
@ -411,16 +412,6 @@ export default function DevicesScreen() {
const currentDevice = mobileDevices.find((d) => d.isCurrent);
const subtitle = isLegend ? t('devices.subtitle_legend') : t('devices.subtitle_free');
const counterText = isLegend
? atDeviceLimit
? t('devices.counter_limit', { max: TOTAL_DEVICE_SLOTS })
: t('devices.counter_some', {
count: totalRegistered,
max: TOTAL_DEVICE_SLOTS,
remaining: TOTAL_DEVICE_SLOTS - totalRegistered,
})
: null;
async function handleRemoveProtected(id: string) {
try {
const { manualRemovalRequired } = await removeProtected(id);
@ -446,8 +437,8 @@ export default function DevicesScreen() {
}}
showsVerticalScrollIndicator={false}
>
{/* Subtitle + counter */}
<View style={{ gap: 4, marginBottom: -12 }}>
{/* Subtitle + progress */}
<View style={{ gap: 8, marginBottom: -12 }}>
<Text
style={{
fontSize: 13,
@ -458,16 +449,12 @@ export default function DevicesScreen() {
>
{subtitle}
</Text>
{counterText ? (
<Text
style={{
fontSize: 12,
color: atDeviceLimit ? colors.brandOrange : colors.textMuted,
fontFamily: 'Nunito_600SemiBold',
}}
>
{counterText}
</Text>
{isLegend ? (
<DeviceProgressBar
count={totalRegistered}
max={TOTAL_DEVICE_SLOTS}
atLimit={atDeviceLimit}
/>
) : null}
</View>

View File

@ -0,0 +1,76 @@
import { useEffect, useRef } from 'react';
import { Animated, Text, View } from 'react-native';
import { useTranslation } from 'react-i18next';
import { useColors } from '../../lib/theme';
interface DeviceProgressBarProps {
count: number;
max: number;
atLimit: boolean;
}
export function DeviceProgressBar({ count, max, atLimit }: DeviceProgressBarProps) {
const { t } = useTranslation();
const colors = useColors();
const fillAnim = useRef(new Animated.Value(0)).current;
const ratio = max > 0 ? Math.min(count / max, 1) : 0;
useEffect(() => {
Animated.timing(fillAnim, {
toValue: ratio,
duration: 380,
useNativeDriver: false,
}).start();
}, [ratio]);
const fillColor = atLimit ? colors.brandOrange : colors.success;
return (
<View style={{ gap: 5 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text
style={{
fontSize: 12,
color: atLimit ? colors.brandOrange : colors.textMuted,
fontFamily: 'Nunito_600SemiBold',
}}
>
{atLimit
? t('devices.progress_at_limit')
: t('devices.progress_label', { count, max })}
</Text>
<Text
style={{
fontSize: 11,
color: colors.textMuted,
fontFamily: 'Nunito_400Regular',
}}
>
{count}/{max}
</Text>
</View>
<View
style={{
height: 6,
borderRadius: 3,
backgroundColor: colors.surfaceElevated,
overflow: 'hidden',
}}
>
<Animated.View
style={{
height: '100%',
borderRadius: 3,
backgroundColor: fillColor,
width: fillAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '100%'],
}),
}}
/>
</View>
</View>
);
}

View File

@ -890,8 +890,8 @@
"remove_warning_title": "Profile manuell entfernen",
"remove_warning_body": "Wir können das Profile nicht aus der Ferne löschen. Auf dem Mac: Systemeinstellungen → Profile → ReBreak → Entfernen (Admin-Passwort nötig).",
"add_device": "Neues Gerät hinzufügen",
"counter_some": "%{count} von %{max} Geräten · noch %{remaining} frei",
"counter_limit": "Maximum erreicht — %{max} von %{max} Geräten",
"progress_label": "%{count} von %{max} Geräten",
"progress_at_limit": "Maximum erreicht",
"add_windows_enabled": "Windows-PC hinzufügen",
"windows_label_question": "Wie soll der Windows-PC heißen?",
"windows_label_default": "Windows-PC",

View File

@ -890,8 +890,8 @@
"remove_warning_title": "Remove profile manually",
"remove_warning_body": "We can't delete the profile remotely. On the Mac: System Settings → Profiles → ReBreak → Remove (admin password required).",
"add_device": "Add new device",
"counter_some": "%{count} of %{max} devices · %{remaining} more available",
"counter_limit": "Maximum reached — %{max} of %{max} devices",
"progress_label": "%{count} of %{max} devices",
"progress_at_limit": "Maximum reached",
"add_windows_enabled": "Add Windows PC",
"windows_label_question": "What should this Windows PC be called?",
"windows_label_default": "Windows PC",

View File

@ -887,8 +887,8 @@
"remove_warning_title": "Supprimer le profil manuellement",
"remove_warning_body": "Nous ne pouvons pas supprimer le profil à distance. Sur le Mac : Réglages système → Profils → ReBreak → Supprimer (mot de passe administrateur requis).",
"add_device": "Ajouter un appareil",
"counter_some": "%{count} sur %{max} appareils · encore %{remaining} disponible",
"counter_limit": "Maximum atteint — %{max} sur %{max} appareils",
"progress_label": "%{count} sur %{max} appareils",
"progress_at_limit": "Maximum atteint",
"add_windows_enabled": "Ajouter un PC Windows",
"windows_label_question": "Comment appeler ce PC Windows ?",
"windows_label_default": "PC Windows",