import AsyncStorage from "@react-native-async-storage/async-storage";
import { LinearGradient } from "expo-linear-gradient";
import { StatusBar } from "expo-status-bar";
import React, { useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
Alert,
Pressable,
SafeAreaView,
ScrollView,
Share,
StyleSheet,
Switch,
Text,
TextInput,
View,
useWindowDimensions,
} from "react-native";
import {
Activity,
Apple,
Bell,
Bot,
Camera,
Check,
ChevronRight,
CircleUserRound,
Download,
Droplets,
Footprints,
HeartPulse,
Home,
Lightbulb,
Minus,
Moon,
Plus,
Scale,
Send,
ShieldCheck,
Sparkles,
Sprout,
TrendingUp,
UserRound,
Utensils,
} from "lucide-react-native";
type LucideIcon = any;
type Tab = "today" | "journal" | "progress" | "coach" | "profile";
type Entry = {
date: string;
iso?: string;
weight: number;
water: number;
steps: number;
glucose?: number;
sleep?: number;
meals?: number;
mood?: string;
};
type ProfileData = {
name: string;
goal: string;
journey: string;
glucoseUnit: "mg/dL" | "mmol/L";
reminders: boolean;
complete: boolean;
};
type ChatMessage = { id: string; role: "user" | "assistant"; content: string };
const C = {
ink: "#15211c",
muted: "#637169",
cream: "#f6f7f2",
white: "#fff",
green: "#1d6b50",
mint: "#dcefe5",
coral: "#ef835f",
yellow: "#f4d58d",
line: "#e5e9e3",
};
const seed: Entry[] = [
{ date: "Lun", weight: 91.4, water: 1500, steps: 6100, glucose: 112 },
{ date: "Mar", weight: 91.2, water: 1800, steps: 7400, glucose: 108 },
{ date: "Mer", weight: 91.1, water: 1600, steps: 6900, glucose: 110 },
{ date: "Jeu", weight: 90.9, water: 2000, steps: 8200, glucose: 105 },
{ date: "Ven", weight: 90.8, water: 2100, steps: 8600, glucose: 104 },
{ date: "Sam", weight: 90.7, water: 1750, steps: 7200, glucose: 107 },
{ date: "Auj.", weight: 90.6, water: 1250, steps: 6240, glucose: 103 },
];
function Card({ children, style }: any) {
return {children};
}
function Pill({ children, tone = "green" }: any) {
return (
{children}
);
}
function IconTile({
icon: Icon,
color = C.green,
bg = C.mint,
size = 22,
}: any) {
return (
);
}
function Metric({ icon, label, value, sub, onPress }: any) {
const content = (
<>
{label}
{value}
{sub}
>
);
return onPress ? (
[s.metric, pressed && s.pressed]}
>
{content}
) : (
{content}
);
}
function ProgressBar({ value, color = C.green }: any) {
return (
);
}
function Nav({ tab, setTab }: any) {
const items: any[] = [
["today", Home, "Aujourd’hui"],
["journal", Plus, "Journal"],
["progress", TrendingUp, "Progrès"],
["coach", Sparkles, "Coach"],
["profile", CircleUserRound, "Profil"],
];
return (
{items.map(([id, Icon, label]) => {
const active = tab === id;
return (
setTab(id)}
style={({ pressed }) => [
s.navItem,
active && s.navItemActive,
pressed && s.pressed,
]}
>
{label}
);
})}
);
}
export default function App() {
const [tab, setTab] = useState("today");
const [entries, setEntries] = useState(seed);
const [water, setWater] = useState(1250);
const [steps, setSteps] = useState(6240);
const [weight, setWeight] = useState("90.6");
const [glucose, setGlucose] = useState("103");
const [sleep, setSleep] = useState("7.2");
const [meals, setMeals] = useState(2);
const [mood, setMood] = useState("🙂");
const [saved, setSaved] = useState(false);
const [loaded, setLoaded] = useState(false);
const [profile, setProfile] = useState({
name: "",
goal: "Retrouver mon équilibre",
journey: "Prévention",
glucoseUnit: "mg/dL",
reminders: false,
complete: false,
});
const { width } = useWindowDimensions();
useEffect(() => {
AsyncStorage.getItem("metabloom-v2")
.then((v) => {
if (v) {
const d = JSON.parse(v);
setEntries(
Array.isArray(d.entries) && d.entries.length ? d.entries : seed,
);
setWater(typeof d.water === "number" ? d.water : 1250);
setSteps(typeof d.steps === "number" ? d.steps : 6240);
setProfile(d.profile || profile);
}
})
.catch(() => {})
.finally(() => setLoaded(true));
}, []);
useEffect(() => {
if (loaded)
AsyncStorage.setItem(
"metabloom-v2",
JSON.stringify({ entries, water, steps, profile }),
).catch(() => {});
}, [entries, water, steps, profile, loaded]);
const score = Math.round(
(Math.min(water / 2000, 1) + Math.min(steps / 8000, 1) + 0.82 + 0.75) * 25,
);
const save = () => {
const w = parseFloat(weight.replace(",", "."));
const g = parseFloat(glucose.replace(",", "."));
const sl = parseFloat(sleep.replace(",", "."));
if (!Number.isFinite(w) || w < 30 || w > 300) {
Alert.alert(
"Valeur à vérifier",
"Saisis un poids compris entre 30 et 300 kg.",
);
return;
}
const glucoseBounds =
profile.glucoseUnit === "mmol/L" ? [1.1, 33.3] : [20, 600];
if (
glucose.trim() &&
(!Number.isFinite(g) || g < glucoseBounds[0] || g > glucoseBounds[1])
) {
Alert.alert(
"Valeur à vérifier",
"La glycémie saisie semble hors de la plage de saisie autorisée. Vérifie l’unité et la valeur.",
);
return;
}
if (!Number.isFinite(sl) || sl < 0 || sl > 24) {
Alert.alert(
"Valeur à vérifier",
"Le sommeil doit être compris entre 0 et 24 heures.",
);
return;
}
const now = new Date();
setEntries((x) => [
...x.slice(-29),
{
date: "Auj.",
iso: now.toISOString(),
weight: w,
water,
steps,
glucose: Number.isFinite(g) ? g : undefined,
sleep: sl,
meals,
mood,
},
]);
setSaved(true);
setTimeout(() => setSaved(false), 2200);
};
const reset = () => {
setEntries(seed);
setWater(1250);
setSteps(6240);
setProfile({ ...profile, complete: false });
AsyncStorage.removeItem("metabloom-v2");
};
const content = useMemo(
() =>
tab === "today" ? (
) : tab === "journal" ? (
) : tab === "progress" ? (
) : tab === "coach" ? (
) : (
),
[
tab,
score,
water,
steps,
weight,
glucose,
sleep,
meals,
mood,
saved,
entries,
profile,
],
);
if (!loaded) return ;
if (!profile.complete)
return ;
return (
900 ? 1120 : 680 }]}>
{content}
);
}
function Onboarding({ profile, setProfile }: any) {
const [step, setStep] = useState(0);
const journeys: [[string, LucideIcon]] | any = [
["Prévention", Sprout],
["Perte de poids", Scale],
["Diabète type 2", HeartPulse],
["Traitement GLP-1", Sparkles],
];
return (
BIENVENUE SUR METABLOOM
{step === 0
? "Prenons soin de toi, simplement."
: "Quel parcours te ressemble ?"}
{step === 0
? "Quelques informations suffisent pour personnaliser ton expérience. Tu gardes le contrôle de tes données."
: "Tu pourras modifier ce choix à tout moment. MetaBloom ne pose aucun diagnostic."}
{step === 0 ? (
<>
Comment souhaites-tu être appelé·e ?
setProfile({ ...profile, name })}
style={s.input}
/>
{!profile.name.trim() && (
Ce nom permettra de personnaliser ton accueil.
)}
Ton objectif principal
setProfile({ ...profile, goal })}
style={s.input}
/>
>
) : (
{journeys.map(([j, Icon]: [string, LucideIcon]) => (
setProfile({ ...profile, journey: j })}
style={({ pressed }) => [
s.choice,
profile.journey === j && s.choiceActive,
pressed && s.pressed,
]}
>
{j}
Conseils éducatifs et habitudes adaptés
{profile.journey === j ? (
) : (
)}
))}
)}
À SAVOIR
MetaBloom est un outil de bien-être. Il ne remplace pas un
professionnel de santé et ne modifie jamais un traitement.
step === 0 ? setStep(1) : setProfile({ ...profile, complete: true })
}
style={({ pressed }) => [
s.primary,
step === 0 && !profile.name.trim() && { opacity: 0.4 },
pressed && s.pressed,
]}
>
{step === 0 ? "Continuer" : "Accepter et commencer"}
{step === 1 && (
setStep(0)} style={s.secondary}>
Retour
)}
);
}
function Header({ eyebrow, title, right }: any) {
return (
{eyebrow}
{title}
{right && {right}}
);
}
function Today({ name, score, water, setWater, steps, setSteps, go }: any) {
const date = new Intl.DateTimeFormat("fr-FR", {
weekday: "long",
day: "numeric",
month: "long",
})
.format(new Date())
.toUpperCase();
return (
{name[0]?.toUpperCase()}
}
/>
Aujourd’hui
Chaque petit pas compte.
Tu avances à ton rythme, sans pression. Encore une action douce pour
compléter ta journée.
go("journal")} style={s.heroButton}>
Ajouter une donnée →
{score}
/ 100
Mes essentiels
Objectifs ajustables
setWater((x: number) => Math.min(x + 250, 2500))}
/>
setSteps((x: number) => x + 500)}
/>
PETIT CONSEIL DU JOUR
Une marche de 10 minutes après le repas ?
Un peu de mouvement aide à se sentir mieux. Adapte toujours l’effort
à ta situation.
MetaBloom accompagne vos habitudes. Il ne remplace ni un diagnostic, ni
un professionnel de santé.
);
}
function Journal({
weight,
setWeight,
glucose,
setGlucose,
glucoseUnit,
water,
setWater,
steps,
setSteps,
sleep,
setSleep,
meals,
setMeals,
mood,
setMood,
save,
saved,
}: any) {
return (
Hors ligne}
/>
Le poids et le sommeil sont nécessaires pour enregistrer la journée. Le
reste est facultatif.
Mesures du jour
Poids
kg
Glycémie facultatif · saisie manuelle
{glucoseUnit}
Ces valeurs servent uniquement à ton suivi personnel. Aucune
interprétation médicale n’est fournie.
Habitudes
setWater(Math.max(0, water - 250))}
plus={() => setWater(water + 250)}
/>
setSteps(Math.max(0, steps - 500))}
plus={() => setSteps(steps + 500)}
/>
Bien-être
Sommeil
heures
Repas équilibrés
{[0, 1, 2, 3].map((n) => (
setMeals(n)}
style={[s.segmentItem, meals === n && s.segmentActive]}
>
{n}
))}
Humeur
{["😔", "😐", "🙂", "😄"].map((x) => (
setMood(x)}
style={[s.segmentItem, mood === x && s.segmentActive]}
>
{x}
))}
REPAS
Scanner ou photographier un repas
La confirmation des aliments restera toujours nécessaire. L’analyse
automatique arrivera avec une base nutritionnelle validée.
[s.primary, pressed && s.pressed]}
>
{saved && }
{saved ? "Journée enregistrée" : "Enregistrer la journée"}
);
}
function Stepper({ icon, label, value, minus, plus }: any) {
const Icon = icon === "💧" ? Droplets : Footprints;
return (
{label}
{value}
[s.stepBtn, pressed && s.pressed]}
>
[s.stepBtn, pressed && s.pressed]}
>
);
}
function Progress({ entries }: any) {
const recent: Entry[] = entries.slice(-7);
if (recent.length === 0)
return (
Tes tendances apparaîtront ici
Ajoute au moins deux journées pour suivre ton évolution, sans
pression.
);
const weights = recent.map((e) => e.weight).filter(Number.isFinite);
const min = Math.min(...weights);
const max = Math.max(...weights);
const avg = (key: "water" | "steps") =>
Math.round(
recent.reduce((a, e) => a + (Number(e[key]) || 0), 0) / recent.length,
);
const rawDelta =
recent.length > 1 ? recent.at(-1)!.weight - recent[0].weight : 0;
return (
{rawDelta <= 0 ? "Baisse" : "Hausse"}{" "}
{Math.abs(rawDelta).toFixed(1).replace(".", ",")} kg
}
/>
Observe les tendances, pas la perfection.
Tendance de poids
{recent.at(-1)!.weight} kg
Données personnelles
{recent.map((e, i) => (
{e.weight}
{e.date}
))}
{(avg("water") / 1000).toFixed(1).replace(".", ",")} L
moyenne d’eau
{avg("steps").toLocaleString("fr-FR")}
pas en moyenne
À RETENIR
Une semaine régulière 🌱
Tu as bougé au moins 6 000 pas chaque jour. Cette régularité compte
davantage qu’une journée parfaite.
);
}
function Coach({ journey }: any) {
const lessons: [[LucideIcon, string, string]] | any = [
[Utensils, "Composer une assiette rassasiante", "4 min"],
[Footprints, "Bouger après un repas, à ton rythme", "3 min"],
[Lightbulb, "Sortir du tout ou rien", "5 min"],
];
return (
{journey}}
/>
Pour aller plus loin
{lessons.map(([Icon, title, duration]: [LucideIcon, string, string]) => (
[s.card, s.lesson, pressed && s.pressed]}
>
{title}
{duration} · contenu éducatif
))}
CONNEXIONS SANTÉ
Apple Santé & Health Connect
Disponible prochainement après activation des autorisations natives.
);
}
function PegChat({ journey }: { journey: string }) {
const apiBase = (
process.env.EXPO_PUBLIC_API_URL || "https://api.metabloom.vicode.agency"
).replace(/\/$/, "");
const [messages, setMessages] = useState([
{
id: "welcome",
role: "assistant",
content: `Bonjour, je suis Peg. Je peux t’aider à trouver de petites actions adaptées à ton parcours ${journey}, sans jugement. De quoi aimerais-tu parler aujourd’hui ?`,
},
]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const suggestions = [
"Une idée de petit-déjeuner",
"Comment rester motivé·e ?",
"Une action simple aujourd’hui",
];
const send = async (text = input) => {
const clean = text.trim();
if (!clean || loading) return;
const userMessage: ChatMessage = {
id: `u-${Date.now()}`,
role: "user",
content: clean,
};
const next = [...messages, userMessage];
setMessages(next);
setInput("");
setLoading(true);
try {
const createGuestToken = async () => {
const guestResponse = await fetch(`${apiBase}/auth/guest`, {
method: "POST",
});
if (!guestResponse.ok) throw new Error("session indisponible");
const guest = await guestResponse.json();
const guestToken = String(guest.access_token || "");
if (!guestToken) throw new Error("jeton absent");
await AsyncStorage.setItem("metabloom-auth-token", guestToken);
return guestToken;
};
let token = await AsyncStorage.getItem("metabloom-auth-token");
if (!token) {
token = await createGuestToken();
}
const requestPeg = (accessToken: string) => fetch(`${apiBase}/coach/chat`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
message: clean,
journey,
history: next
.slice(0, -1)
.slice(-8)
.map(({ role, content }) => ({ role, content })),
})
});
let response = await requestPeg(token);
if (response.status === 401) {
await AsyncStorage.removeItem("metabloom-auth-token");
token = await createGuestToken();
response = await requestPeg(token);
}
if (!response.ok) throw new Error("service indisponible");
const data = await response.json();
const content = String(data.reply || data.message || "").trim();
if (!content) throw new Error("réponse vide");
setMessages((x) => [
...x,
{ id: `a-${Date.now()}`, role: "assistant", content },
]);
} catch {
setMessages((x) => [
...x,
{
id: `e-${Date.now()}`,
role: "assistant",
content:
"Je ne peux pas joindre le moteur IA pour le moment. Tu peux réessayer plus tard ou consulter les micro-leçons ci-dessous. En cas de symptôme ou d’urgence, contacte un professionnel ou les services d’urgence.",
},
]);
} finally {
setLoading(false);
}
};
return (
Peg
Coach bien-être · conseils éducatifs
{messages.map((message) => (
{message.role === "assistant" ? (
) : (
)}
{message.content}
))}
{loading && (
Peg réfléchit…
)}
{suggestions.map((item) => (
send(item)}
style={({ pressed }) => [s.suggestion, pressed && s.pressed]}
>
{item}
))}
send()}
style={({ pressed }) => [
s.sendButton,
(!input.trim() || loading) && s.sendDisabled,
pressed && s.pressed,
]}
>
Peg peut se tromper. Elle ne remplace pas un professionnel de santé et
ne donne aucun diagnostic ni conseil de traitement.
);
}
function Profile({ profile, setProfile, entries, reset }: any) {
const exportData = async () => {
const csv = [
"date,poids,eau_ml,pas,glycemie",
...entries.map((e: Entry) =>
[e.iso || e.date, e.weight, e.water, e.steps, e.glucose || ""].join(
",",
),
),
].join("\n");
await Share.share({ title: "Export MetaBloom", message: csv });
};
return (
{profile.name[0]?.toUpperCase()}
{profile.name}
{profile.goal} · {profile.journey}
Préférences
Unité glycémie
{profile.glucoseUnit}
setProfile({
...profile,
glucoseUnit: profile.glucoseUnit === "mg/dL" ? "mmol/L" : "mg/dL",
})
}
style={({ pressed }) => [s.smallButton, pressed && s.pressed]}
>
Changer
Rappels doux
{profile.reminders ? "Activés à 19 h" : "Désactivés"}
setProfile({ ...profile, reminders })}
trackColor={{ true: C.green }}
/>
[s.secondaryCard, pressed && s.pressed]}
>
Exporter mes données CSV
SÉCURITÉ & CONFIDENTIALITÉ
Tes données restent sous ton contrôle.
Tu peux les exporter ou les supprimer à tout moment. La
synchronisation en ligne n’est pas activée dans cette version.
Alert.alert(
"Effacer les données locales ?",
"Cette action supprime les données de cet appareil et réinitialise l’application.",
[
{ text: "Annuler" },
{ text: "Effacer", style: "destructive", onPress: reset },
],
)
}
style={s.danger}
>
Effacer les données de cet appareil
En cas de malaise ou d’urgence, contactez immédiatement les services
d’urgence. MetaBloom ne fournit pas de conseil thérapeutique.
);
}
const s = StyleSheet.create({
safe: { flex: 1, backgroundColor: C.cream },
shell: { flex: 1, width: "100%", alignSelf: "center" },
page: { padding: 20, paddingTop: 26, paddingBottom: 140, gap: 16 },
pressed: { opacity: 0.72, transform: [{ scale: 0.98 }] },
header: {
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "space-between",
alignItems: "center",
gap: 12,
marginBottom: 4,
},
headerCopy: { flexGrow: 1, flexShrink: 1, minWidth: 180 },
headerAction: { flexShrink: 1, maxWidth: "100%" },
eyebrow: {
fontSize: 11,
fontWeight: "800",
letterSpacing: 1.4,
color: C.green,
flexShrink: 1,
},
title: {
fontSize: 28,
lineHeight: 35,
fontWeight: "800",
color: C.ink,
letterSpacing: -0.7,
marginTop: 4,
flexShrink: 1,
},
avatar: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: C.yellow,
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
},
avatarText: { fontWeight: "800", color: C.ink, fontSize: 17 },
hero: {
borderRadius: 28,
padding: 22,
minHeight: 230,
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
gap: 14,
overflow: "hidden",
},
heroTitle: {
fontSize: 27,
lineHeight: 34,
fontWeight: "800",
color: C.white,
marginTop: 18,
maxWidth: 360,
flexShrink: 1,
},
heroText: {
fontSize: 14,
lineHeight: 21,
color: "#dcebe4",
marginTop: 8,
maxWidth: 400,
flexShrink: 1,
},
heroButton: {
backgroundColor: C.white,
borderRadius: 14,
paddingVertical: 14,
paddingHorizontal: 16,
alignSelf: "flex-start",
marginTop: 18,
minHeight: 48,
justifyContent: "center",
maxWidth: "100%",
},
heroButtonText: {
color: C.green,
fontWeight: "800",
textAlign: "center",
flexShrink: 1,
},
ring: {
width: 96,
height: 96,
borderWidth: 8,
borderColor: C.yellow,
borderRadius: 50,
alignItems: "center",
justifyContent: "center",
backgroundColor: "#ffffff13",
flexShrink: 0,
},
ringBig: { color: C.white, fontWeight: "900", fontSize: 28 },
ringSmall: { color: "#dcebe4", fontSize: 11 },
pill: {
borderRadius: 99,
paddingHorizontal: 11,
paddingVertical: 6,
alignSelf: "flex-start",
maxWidth: "100%",
flexShrink: 1,
},
pillText: { fontSize: 11, lineHeight: 16, fontWeight: "800", flexShrink: 1 },
sectionHead: {
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "space-between",
alignItems: "center",
gap: 6,
},
h2: {
fontSize: 19,
lineHeight: 25,
fontWeight: "800",
color: C.ink,
flexShrink: 1,
},
caption: { fontSize: 12, lineHeight: 18, color: C.muted, flexShrink: 1 },
grid: { flexDirection: "row", flexWrap: "wrap", gap: 12 },
metric: {
backgroundColor: C.white,
borderRadius: 20,
padding: 17,
flexGrow: 1,
flexBasis: 130,
minWidth: 0,
minHeight: 154,
borderWidth: 1,
borderColor: C.line,
},
iconTile: {
width: 44,
height: 44,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
marginBottom: 12,
flexShrink: 0,
},
metricLabel: {
fontSize: 12,
lineHeight: 18,
fontWeight: "700",
color: C.muted,
flexShrink: 1,
},
metricValue: {
fontSize: 22,
lineHeight: 29,
fontWeight: "900",
color: C.ink,
marginTop: 3,
flexShrink: 1,
},
metricSub: {
fontSize: 11,
lineHeight: 17,
color: C.muted,
marginTop: 3,
flexShrink: 1,
},
card: {
backgroundColor: C.white,
borderRadius: 22,
padding: 19,
borderWidth: 1,
borderColor: C.line,
minWidth: 0,
},
tip: {
flexDirection: "row",
flexWrap: "wrap",
gap: 14,
backgroundColor: "#fffaf0",
},
tipTag: {
fontSize: 10,
lineHeight: 16,
fontWeight: "900",
letterSpacing: 1.1,
color: C.green,
marginBottom: 6,
flexShrink: 1,
},
tipTitle: {
fontSize: 17,
lineHeight: 23,
fontWeight: "800",
color: C.ink,
marginBottom: 5,
flexShrink: 1,
},
body: { fontSize: 13, lineHeight: 20, color: C.muted, flexShrink: 1 },
disclaimer: {
fontSize: 11,
lineHeight: 17,
textAlign: "center",
color: C.muted,
paddingHorizontal: 8,
flexShrink: 1,
},
nav: {
position: "absolute",
bottom: 0,
left: 0,
right: 0,
minHeight: 86,
backgroundColor: "#fffffffa",
borderTopWidth: 1,
borderColor: C.line,
flexDirection: "row",
alignItems: "stretch",
paddingTop: 8,
paddingBottom: 8,
paddingHorizontal: 4,
},
navItem: {
flex: 1,
minWidth: 0,
minHeight: 62,
alignItems: "center",
justifyContent: "center",
gap: 4,
borderRadius: 16,
paddingHorizontal: 2,
},
navItemActive: { backgroundColor: "#edf7f1" },
navLabel: {
fontSize: 10,
lineHeight: 14,
fontWeight: "700",
color: "#829087",
textAlign: "center",
flexShrink: 1,
},
active: { color: C.green },
lead: {
fontSize: 15,
lineHeight: 23,
color: C.muted,
marginTop: -8,
flexShrink: 1,
},
inputLabel: {
fontSize: 13,
lineHeight: 20,
fontWeight: "800",
color: C.ink,
marginTop: 17,
marginBottom: 7,
flexShrink: 1,
},
optional: { fontWeight: "500", color: C.muted },
inputRow: {
minHeight: 56,
borderWidth: 1.5,
borderColor: "#d8ded8",
borderRadius: 14,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 15,
backgroundColor: C.white,
gap: 8,
},
input: {
flexGrow: 1,
flexShrink: 1,
minWidth: 0,
fontSize: 18,
fontWeight: "800",
color: C.ink,
outlineStyle: "none",
} as any,
unit: {
fontWeight: "700",
color: C.muted,
flexShrink: 0,
textAlign: "right",
},
helper: {
fontSize: 11,
lineHeight: 17,
color: C.muted,
marginTop: 12,
flexShrink: 1,
},
stepper: {
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
gap: 12,
paddingVertical: 13,
borderBottomWidth: 1,
borderBottomColor: C.line,
},
stepValue: {
fontSize: 17,
lineHeight: 23,
fontWeight: "800",
color: C.ink,
marginTop: 2,
flexShrink: 1,
},
stepBtn: {
width: 48,
height: 48,
borderRadius: 14,
backgroundColor: C.mint,
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
},
stepBtnText: { fontSize: 24, color: C.green, fontWeight: "700" },
primary: {
backgroundColor: C.green,
borderRadius: 16,
minHeight: 54,
padding: 16,
alignItems: "center",
justifyContent: "center",
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
maxWidth: "100%",
},
primaryText: {
color: C.white,
fontSize: 15,
lineHeight: 21,
fontWeight: "900",
textAlign: "center",
flexShrink: 1,
},
track: {
height: 8,
borderRadius: 4,
backgroundColor: "#ffffff3d",
overflow: "hidden",
},
fill: { height: 8, borderRadius: 4 },
bigNumber: {
fontSize: 28,
lineHeight: 36,
fontWeight: "900",
color: C.ink,
marginTop: 4,
flexShrink: 1,
},
bigUnit: { fontSize: 14, color: C.muted },
chart: {
height: 175,
flexDirection: "row",
alignItems: "flex-end",
justifyContent: "space-between",
marginTop: 20,
},
barCol: { flex: 1, minWidth: 0, alignItems: "center" },
bar: {
width: 18,
maxWidth: "70%",
backgroundColor: C.green,
borderRadius: 8,
marginVertical: 6,
},
barValue: { fontSize: 9, lineHeight: 13, color: C.muted },
barLabel: {
fontSize: 9,
lineHeight: 13,
color: C.muted,
textAlign: "center",
},
stat: { flexGrow: 1, flexBasis: 130, minWidth: 0 },
statIcon: { fontSize: 22 },
empty: { alignItems: "center", paddingVertical: 34 },
insight: { backgroundColor: C.mint },
profile: {
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
gap: 15,
},
avatarLarge: {
width: 62,
height: 62,
borderRadius: 22,
backgroundColor: C.yellow,
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
},
avatarLargeText: { fontSize: 23, fontWeight: "900", color: C.ink },
setting: {
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
gap: 13,
padding: 15,
},
chevron: { fontSize: 28, color: C.muted },
safety: { backgroundColor: C.mint },
danger: {
minHeight: 48,
padding: 16,
alignItems: "center",
justifyContent: "center",
},
dangerText: {
color: "#a23d33",
fontWeight: "800",
textAlign: "center",
flexShrink: 1,
},
onboard: {
padding: 24,
paddingTop: 54,
paddingBottom: 50,
gap: 16,
maxWidth: 560,
width: "100%",
alignSelf: "center",
},
logo: {
width: 58,
height: 58,
borderRadius: 20,
backgroundColor: C.green,
alignItems: "center",
justifyContent: "center",
marginBottom: 8,
},
logoText: { color: C.white, fontSize: 28, fontWeight: "900" },
onboardTitle: {
fontSize: 34,
lineHeight: 42,
fontWeight: "900",
color: C.ink,
letterSpacing: -1,
flexShrink: 1,
},
choice: {
backgroundColor: C.white,
borderWidth: 1,
borderColor: C.line,
borderRadius: 18,
padding: 15,
minHeight: 76,
flexDirection: "row",
alignItems: "center",
gap: 12,
},
choiceActive: {
borderWidth: 2,
borderColor: C.green,
backgroundColor: C.mint,
},
choiceEmoji: { fontSize: 24 },
check: { fontSize: 18, fontWeight: "900", color: C.green },
secondary: {
minHeight: 48,
padding: 14,
alignItems: "center",
justifyContent: "center",
},
secondaryText: { color: C.green, fontWeight: "800", textAlign: "center" },
segment: { flexDirection: "row", gap: 8 },
segmentItem: {
minHeight: 48,
flex: 1,
minWidth: 0,
borderRadius: 13,
backgroundColor: C.cream,
alignItems: "center",
justifyContent: "center",
borderWidth: 1,
borderColor: C.line,
paddingVertical: 8,
},
segmentActive: { backgroundColor: C.green, borderColor: C.green },
segmentText: { fontWeight: "800", color: C.ink },
scan: {
backgroundColor: "#fff8ed",
borderStyle: "dashed",
borderColor: "#d8a960",
},
coachHero: { borderRadius: 26, padding: 22, gap: 14 },
coachProgress: {
fontSize: 12,
lineHeight: 18,
fontWeight: "700",
color: "#eee8f7",
flexShrink: 1,
},
lesson: {
flexDirection: "row",
alignItems: "center",
gap: 12,
minHeight: 82,
},
lessonIcon: {
width: 48,
height: 48,
borderRadius: 15,
backgroundColor: C.cream,
alignItems: "center",
justifyContent: "center",
},
smallButton: {
minHeight: 44,
paddingHorizontal: 12,
paddingVertical: 9,
borderRadius: 11,
backgroundColor: C.mint,
justifyContent: "center",
maxWidth: "100%",
flexShrink: 1,
},
smallButtonText: {
color: C.green,
fontWeight: "800",
fontSize: 12,
lineHeight: 18,
textAlign: "center",
},
secondaryCard: {
backgroundColor: C.white,
borderRadius: 16,
borderWidth: 1,
borderColor: C.green,
minHeight: 52,
padding: 16,
alignItems: "center",
justifyContent: "center",
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
},
secondaryCardText: {
color: C.green,
fontWeight: "900",
textAlign: "center",
flexShrink: 1,
},
flexCopy: { flex: 1, minWidth: 0, flexShrink: 1 },
pegCard: { backgroundColor: C.white, borderRadius: 24, borderWidth: 1, borderColor: C.line, overflow: "hidden" },
pegHeader: { padding: 16, flexDirection: "row", alignItems: "center", gap: 12, borderBottomWidth: 1, borderBottomColor: C.line },
pegAvatar: { width: 46, height: 46, borderRadius: 16, backgroundColor: "#7659a6", alignItems: "center", justifyContent: "center", flexShrink: 0 },
pegName: { fontSize: 18, lineHeight: 23, fontWeight: "900", color: C.ink },
pegStatus: { fontSize: 11, lineHeight: 16, color: C.muted, flexShrink: 1 },
chatList: { padding: 15, gap: 12, backgroundColor: "#fbfcfa" },
messageRow: { flexDirection: "row", alignItems: "flex-end", gap: 8, maxWidth: "92%" },
messageRowUser: { alignSelf: "flex-end", flexDirection: "row-reverse" },
messageAvatar: { width: 28, height: 28, borderRadius: 10, backgroundColor: C.mint, alignItems: "center", justifyContent: "center", flexShrink: 0 },
messageAvatarUser: { backgroundColor: "#eee8f7" },
bubble: { backgroundColor: C.white, borderRadius: 16, borderBottomLeftRadius: 5, paddingHorizontal: 13, paddingVertical: 10, borderWidth: 1, borderColor: C.line, minWidth: 0, flexShrink: 1 },
bubbleUser: { backgroundColor: "#7659a6", borderColor: "#7659a6", borderBottomLeftRadius: 16, borderBottomRightRadius: 5 },
bubbleText: { fontSize: 14, lineHeight: 20, color: C.ink, flexShrink: 1 },
bubbleTextUser: { color: C.white },
typing: { flexDirection: "row", alignItems: "center", gap: 8, paddingLeft: 36 },
suggestions: { gap: 8, paddingHorizontal: 15, paddingVertical: 12 },
suggestion: { borderRadius: 99, borderWidth: 1, borderColor: "#cddfd5", backgroundColor: "#f4faf6", paddingHorizontal: 13, paddingVertical: 9, maxWidth: 220 },
suggestionText: { fontSize: 12, lineHeight: 17, fontWeight: "700", color: C.green, flexShrink: 1 },
composer: { marginHorizontal: 15, marginBottom: 10, minHeight: 54, borderWidth: 1.5, borderColor: "#d8ded8", borderRadius: 17, backgroundColor: C.white, flexDirection: "row", alignItems: "flex-end", gap: 8, padding: 6, paddingLeft: 13 },
chatInput: { flex: 1, minWidth: 0, maxHeight: 110, fontSize: 15, lineHeight: 21, color: C.ink, paddingVertical: 9, outlineStyle: "none" } as any,
sendButton: { width: 42, height: 42, borderRadius: 14, backgroundColor: "#7659a6", alignItems: "center", justifyContent: "center", flexShrink: 0 },
sendDisabled: { opacity: 0.4 },
pegDisclaimer: { fontSize: 10, lineHeight: 15, color: C.muted, textAlign: "center", paddingHorizontal: 18, paddingBottom: 15, flexShrink: 1 },
});