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 { AppHeader } from '../components/AppHeader';
import { AddMacSheet } from '../components/devices/AddMacSheet'; import { AddMacSheet } from '../components/devices/AddMacSheet';
import { AddWindowsSheet } from '../components/devices/AddWindowsSheet'; import { AddWindowsSheet } from '../components/devices/AddWindowsSheet';
import { DeviceProgressBar } from '../components/devices/DeviceProgressBar';
// ─── Helpers ───────────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────────
@ -411,16 +412,6 @@ export default function DevicesScreen() {
const currentDevice = mobileDevices.find((d) => d.isCurrent); const currentDevice = mobileDevices.find((d) => d.isCurrent);
const subtitle = isLegend ? t('devices.subtitle_legend') : t('devices.subtitle_free'); 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) { async function handleRemoveProtected(id: string) {
try { try {
const { manualRemovalRequired } = await removeProtected(id); const { manualRemovalRequired } = await removeProtected(id);
@ -446,8 +437,8 @@ export default function DevicesScreen() {
}} }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
{/* Subtitle + counter */} {/* Subtitle + progress */}
<View style={{ gap: 4, marginBottom: -12 }}> <View style={{ gap: 8, marginBottom: -12 }}>
<Text <Text
style={{ style={{
fontSize: 13, fontSize: 13,
@ -458,16 +449,12 @@ export default function DevicesScreen() {
> >
{subtitle} {subtitle}
</Text> </Text>
{counterText ? ( {isLegend ? (
<Text <DeviceProgressBar
style={{ count={totalRegistered}
fontSize: 12, max={TOTAL_DEVICE_SLOTS}
color: atDeviceLimit ? colors.brandOrange : colors.textMuted, atLimit={atDeviceLimit}
fontFamily: 'Nunito_600SemiBold', />
}}
>
{counterText}
</Text>
) : null} ) : null}
</View> </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_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).", "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", "add_device": "Neues Gerät hinzufügen",
"counter_some": "%{count} von %{max} Geräten · noch %{remaining} frei", "progress_label": "%{count} von %{max} Geräten",
"counter_limit": "Maximum erreicht — %{max} von %{max} Geräten", "progress_at_limit": "Maximum erreicht",
"add_windows_enabled": "Windows-PC hinzufügen", "add_windows_enabled": "Windows-PC hinzufügen",
"windows_label_question": "Wie soll der Windows-PC heißen?", "windows_label_question": "Wie soll der Windows-PC heißen?",
"windows_label_default": "Windows-PC", "windows_label_default": "Windows-PC",

View File

@ -890,8 +890,8 @@
"remove_warning_title": "Remove profile manually", "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).", "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", "add_device": "Add new device",
"counter_some": "%{count} of %{max} devices · %{remaining} more available", "progress_label": "%{count} of %{max} devices",
"counter_limit": "Maximum reached — %{max} of %{max} devices", "progress_at_limit": "Maximum reached",
"add_windows_enabled": "Add Windows PC", "add_windows_enabled": "Add Windows PC",
"windows_label_question": "What should this Windows PC be called?", "windows_label_question": "What should this Windows PC be called?",
"windows_label_default": "Windows PC", "windows_label_default": "Windows PC",

View File

@ -887,8 +887,8 @@
"remove_warning_title": "Supprimer le profil manuellement", "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).", "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", "add_device": "Ajouter un appareil",
"counter_some": "%{count} sur %{max} appareils · encore %{remaining} disponible", "progress_label": "%{count} sur %{max} appareils",
"counter_limit": "Maximum atteint — %{max} sur %{max} appareils", "progress_at_limit": "Maximum atteint",
"add_windows_enabled": "Ajouter un PC Windows", "add_windows_enabled": "Ajouter un PC Windows",
"windows_label_question": "Comment appeler ce PC Windows ?", "windows_label_question": "Comment appeler ce PC Windows ?",
"windows_label_default": "PC Windows", "windows_label_default": "PC Windows",