Files
metabloom/App.tsx
T
AI Company c1f2848830
MetaBloom CI / app (push) Canceled after 0s
MetaBloom CI / api (push) Canceled after 0s
deploy: prepare permanent HTTPS MetaBloom API
2026-07-21 22:40:40 +00:00

1762 lines
50 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <View style={[s.card, style]}>{children}</View>;
}
function Pill({ children, tone = "green" }: any) {
return (
<View
style={[
s.pill,
{ backgroundColor: tone === "coral" ? "#fff0eb" : C.mint },
]}
>
<Text
style={[s.pillText, { color: tone === "coral" ? "#9b432a" : C.green }]}
>
{children}
</Text>
</View>
);
}
function IconTile({
icon: Icon,
color = C.green,
bg = C.mint,
size = 22,
}: any) {
return (
<View style={[s.iconTile, { backgroundColor: bg }]}>
<Icon size={size} color={color} strokeWidth={2.2} />
</View>
);
}
function Metric({ icon, label, value, sub, onPress }: any) {
const content = (
<>
<IconTile icon={icon} />
<Text style={s.metricLabel}>{label}</Text>
<Text style={s.metricValue}>{value}</Text>
<Text style={s.metricSub}>{sub}</Text>
</>
);
return onPress ? (
<Pressable
accessibilityRole="button"
accessibilityLabel={`${label}, ${value}. Ajouter`}
onPress={onPress}
style={({ pressed }) => [s.metric, pressed && s.pressed]}
>
{content}
</Pressable>
) : (
<View accessibilityLabel={`${label}, ${value}, ${sub}`} style={s.metric}>
{content}
</View>
);
}
function ProgressBar({ value, color = C.green }: any) {
return (
<View style={s.track}>
<View
style={[
s.fill,
{ width: `${Math.min(value, 100)}%`, backgroundColor: color },
]}
/>
</View>
);
}
function Nav({ tab, setTab }: any) {
const items: any[] = [
["today", Home, "Aujourdhui"],
["journal", Plus, "Journal"],
["progress", TrendingUp, "Progrès"],
["coach", Sparkles, "Coach"],
["profile", CircleUserRound, "Profil"],
];
return (
<View accessibilityRole="tablist" style={s.nav}>
{items.map(([id, Icon, label]) => {
const active = tab === id;
return (
<Pressable
accessibilityLabel={label}
accessibilityRole="tab"
accessibilityState={{ selected: active }}
key={id}
onPress={() => setTab(id)}
style={({ pressed }) => [
s.navItem,
active && s.navItemActive,
pressed && s.pressed,
]}
>
<Icon
size={22}
color={active ? C.green : "#829087"}
strokeWidth={active ? 2.6 : 2}
/>
<Text style={[s.navLabel, active && s.active]}>{label}</Text>
</Pressable>
);
})}
</View>
);
}
export default function App() {
const [tab, setTab] = useState<Tab>("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<ProfileData>({
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 lunité 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" ? (
<Today
name={profile.name}
score={score}
water={water}
setWater={setWater}
steps={steps}
setSteps={setSteps}
go={setTab}
/>
) : tab === "journal" ? (
<Journal
weight={weight}
setWeight={setWeight}
glucose={glucose}
setGlucose={setGlucose}
glucoseUnit={profile.glucoseUnit}
water={water}
setWater={setWater}
steps={steps}
setSteps={setSteps}
sleep={sleep}
setSleep={setSleep}
meals={meals}
setMeals={setMeals}
mood={mood}
setMood={setMood}
save={save}
saved={saved}
/>
) : tab === "progress" ? (
<Progress entries={entries} />
) : tab === "coach" ? (
<Coach journey={profile.journey} />
) : (
<Profile
profile={profile}
setProfile={setProfile}
entries={entries}
reset={reset}
/>
),
[
tab,
score,
water,
steps,
weight,
glucose,
sleep,
meals,
mood,
saved,
entries,
profile,
],
);
if (!loaded) return <SafeAreaView style={s.safe} />;
if (!profile.complete)
return <Onboarding profile={profile} setProfile={setProfile} />;
return (
<SafeAreaView style={s.safe}>
<StatusBar style="dark" />
<View style={[s.shell, { maxWidth: width > 900 ? 1120 : 680 }]}>
{content}
<Nav tab={tab} setTab={setTab} />
</View>
</SafeAreaView>
);
}
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 (
<SafeAreaView style={s.safe}>
<StatusBar style="dark" />
<ScrollView contentContainerStyle={s.onboard}>
<View style={s.logo}>
<Sprout size={28} color={C.white} />
</View>
<Text style={s.eyebrow}>BIENVENUE SUR METABLOOM</Text>
<Text style={s.onboardTitle}>
{step === 0
? "Prenons soin de toi, simplement."
: "Quel parcours te ressemble ?"}
</Text>
<Text style={s.lead}>
{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."}
</Text>
{step === 0 ? (
<>
<Text style={s.inputLabel}>
Comment souhaites-tu être appelé·e ?
</Text>
<View style={s.inputRow}>
<TextInput
accessibilityLabel="Prénom ou pseudonyme"
placeholder="Prénom ou pseudonyme"
value={profile.name}
onChangeText={(name) => setProfile({ ...profile, name })}
style={s.input}
/>
</View>
{!profile.name.trim() && (
<Text style={s.helper}>
Ce nom permettra de personnaliser ton accueil.
</Text>
)}
<Text style={s.inputLabel}>Ton objectif principal</Text>
<View style={s.inputRow}>
<TextInput
accessibilityLabel="Objectif principal"
value={profile.goal}
onChangeText={(goal) => setProfile({ ...profile, goal })}
style={s.input}
/>
</View>
</>
) : (
<View style={{ gap: 10 }}>
{journeys.map(([j, Icon]: [string, LucideIcon]) => (
<Pressable
key={j}
accessibilityRole="radio"
accessibilityState={{ selected: profile.journey === j }}
onPress={() => setProfile({ ...profile, journey: j })}
style={({ pressed }) => [
s.choice,
profile.journey === j && s.choiceActive,
pressed && s.pressed,
]}
>
<IconTile icon={Icon} />
<View style={{ flex: 1 }}>
<Text style={s.tipTitle}>{j}</Text>
<Text style={s.body}>
Conseils éducatifs et habitudes adaptés
</Text>
</View>
{profile.journey === j ? (
<Check size={22} color={C.green} />
) : (
<View style={{ width: 22 }} />
)}
</Pressable>
))}
</View>
)}
<Card style={s.safety}>
<Text style={s.tipTag}>À SAVOIR</Text>
<Text style={s.body}>
MetaBloom est un outil de bien-être. Il ne remplace pas un
professionnel de santé et ne modifie jamais un traitement.
</Text>
</Card>
<Pressable
disabled={step === 0 && !profile.name.trim()}
onPress={() =>
step === 0 ? setStep(1) : setProfile({ ...profile, complete: true })
}
style={({ pressed }) => [
s.primary,
step === 0 && !profile.name.trim() && { opacity: 0.4 },
pressed && s.pressed,
]}
>
<Text style={s.primaryText}>
{step === 0 ? "Continuer" : "Accepter et commencer"}
</Text>
</Pressable>
{step === 1 && (
<Pressable onPress={() => setStep(0)} style={s.secondary}>
<Text style={s.secondaryText}>Retour</Text>
</Pressable>
)}
</ScrollView>
</SafeAreaView>
);
}
function Header({ eyebrow, title, right }: any) {
return (
<View style={s.header}>
<View style={s.headerCopy}>
<Text style={s.eyebrow}>{eyebrow}</Text>
<Text style={s.title}>{title}</Text>
</View>
{right && <View style={s.headerAction}>{right}</View>}
</View>
);
}
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 (
<ScrollView
contentContainerStyle={s.page}
showsVerticalScrollIndicator={false}
>
<Header
eyebrow={date}
title={`Bonjour ${name}`}
right={
<View style={s.avatar}>
<Text style={s.avatarText}>{name[0]?.toUpperCase()}</Text>
</View>
}
/>
<LinearGradient colors={["#1d6b50", "#2d8063"]} style={s.hero}>
<View style={{ flex: 1 }}>
<Pill>Aujourdhui</Pill>
<Text style={s.heroTitle}>Chaque petit pas compte.</Text>
<Text style={s.heroText}>
Tu avances à ton rythme, sans pression. Encore une action douce pour
compléter ta journée.
</Text>
<Pressable onPress={() => go("journal")} style={s.heroButton}>
<Text style={s.heroButtonText}>Ajouter une donnée </Text>
</Pressable>
</View>
<View style={s.ring}>
<Text style={s.ringBig}>{score}</Text>
<Text style={s.ringSmall}>/ 100</Text>
</View>
</LinearGradient>
<View style={s.sectionHead}>
<Text style={s.h2}>Mes essentiels</Text>
<Text style={s.caption}>Objectifs ajustables</Text>
</View>
<View style={s.grid}>
<Metric
icon={Droplets}
label="Hydratation"
value={`${water / 1000} L`}
sub="sur 2 L"
onPress={() => setWater((x: number) => Math.min(x + 250, 2500))}
/>
<Metric
icon={Footprints}
label="Mouvement"
value={steps.toLocaleString("fr-FR")}
sub="sur 8 000 pas"
onPress={() => setSteps((x: number) => x + 500)}
/>
<Metric
icon={Utensils}
label="Repas équilibrés"
value="2 / 3"
sub="très bien"
/>
<Metric icon={Moon} label="Sommeil" value="7 h 12" sub="objectif 8 h" />
</View>
<Card style={s.tip}>
<IconTile icon={Lightbulb} bg={C.yellow} />
<View style={{ flex: 1 }}>
<Text style={s.tipTag}>PETIT CONSEIL DU JOUR</Text>
<Text style={s.tipTitle}>
Une marche de 10 minutes après le repas ?
</Text>
<Text style={s.body}>
Un peu de mouvement aide à se sentir mieux. Adapte toujours leffort
à ta situation.
</Text>
</View>
</Card>
<Text style={s.disclaimer}>
MetaBloom accompagne vos habitudes. Il ne remplace ni un diagnostic, ni
un professionnel de santé.
</Text>
</ScrollView>
);
}
function Journal({
weight,
setWeight,
glucose,
setGlucose,
glucoseUnit,
water,
setWater,
steps,
setSteps,
sleep,
setSleep,
meals,
setMeals,
mood,
setMood,
save,
saved,
}: any) {
return (
<ScrollView
contentContainerStyle={s.page}
keyboardShouldPersistTaps="handled"
>
<Header
eyebrow="SUIVI COMPLET"
title="Mon journal"
right={<Pill>Hors ligne</Pill>}
/>
<Text style={s.lead}>
Le poids et le sommeil sont nécessaires pour enregistrer la journée. Le
reste est facultatif.
</Text>
<Card>
<Text style={s.h2}>Mesures du jour</Text>
<Text style={s.inputLabel}>Poids</Text>
<View style={s.inputRow}>
<TextInput
accessibilityLabel="Poids en kilogrammes, requis"
value={weight}
onChangeText={setWeight}
keyboardType="decimal-pad"
style={s.input}
/>
<Text style={s.unit}>kg</Text>
</View>
<Text style={s.inputLabel}>
Glycémie <Text style={s.optional}>facultatif · saisie manuelle</Text>
</Text>
<View style={s.inputRow}>
<TextInput
accessibilityLabel="Glycémie facultative"
value={glucose}
onChangeText={setGlucose}
keyboardType="decimal-pad"
style={s.input}
/>
<Text style={s.unit}>{glucoseUnit}</Text>
</View>
<Text style={s.helper}>
Ces valeurs servent uniquement à ton suivi personnel. Aucune
interprétation médicale nest fournie.
</Text>
</Card>
<Card>
<Text style={s.h2}>Habitudes</Text>
<Stepper
icon="💧"
label="Eau"
value={`${water} ml`}
minus={() => setWater(Math.max(0, water - 250))}
plus={() => setWater(water + 250)}
/>
<Stepper
icon="👟"
label="Pas"
value={steps.toLocaleString("fr-FR")}
minus={() => setSteps(Math.max(0, steps - 500))}
plus={() => setSteps(steps + 500)}
/>
</Card>
<Card>
<Text style={s.h2}>Bien-être</Text>
<Text style={s.inputLabel}>Sommeil</Text>
<View style={s.inputRow}>
<TextInput
accessibilityLabel="Durée de sommeil"
value={sleep}
onChangeText={setSleep}
keyboardType="decimal-pad"
style={s.input}
/>
<Text style={s.unit}>heures</Text>
</View>
<Text style={s.inputLabel}>Repas équilibrés</Text>
<View style={s.segment}>
{[0, 1, 2, 3].map((n) => (
<Pressable
accessibilityLabel={`${n} repas équilibrés`}
key={n}
onPress={() => setMeals(n)}
style={[s.segmentItem, meals === n && s.segmentActive]}
>
<Text style={[s.segmentText, meals === n && { color: C.white }]}>
{n}
</Text>
</Pressable>
))}
</View>
<Text style={s.inputLabel}>Humeur</Text>
<View style={s.segment}>
{["😔", "😐", "🙂", "😄"].map((x) => (
<Pressable
accessibilityLabel={`Humeur ${x}`}
key={x}
onPress={() => setMood(x)}
style={[s.segmentItem, mood === x && s.segmentActive]}
>
<Text style={{ fontSize: 21 }}>{x}</Text>
</Pressable>
))}
</View>
</Card>
<Card style={s.scan}>
<IconTile icon={Camera} bg="#fff0d7" color="#9b6327" />
<Text style={s.tipTag}>REPAS</Text>
<Text style={s.tipTitle}>Scanner ou photographier un repas</Text>
<Text style={s.body}>
La confirmation des aliments restera toujours nécessaire. Lanalyse
automatique arrivera avec une base nutritionnelle validée.
</Text>
</Card>
<Pressable
accessibilityRole="button"
accessibilityLabel={
saved ? "Journée enregistrée" : "Enregistrer la journée"
}
onPress={save}
style={({ pressed }) => [s.primary, pressed && s.pressed]}
>
{saved && <Check size={20} color={C.white} />}
<Text style={s.primaryText}>
{saved ? "Journée enregistrée" : "Enregistrer la journée"}
</Text>
</Pressable>
</ScrollView>
);
}
function Stepper({ icon, label, value, minus, plus }: any) {
const Icon = icon === "💧" ? Droplets : Footprints;
return (
<View style={s.stepper}>
<IconTile icon={Icon} />
<View style={{ flexGrow: 1, flexShrink: 1, minWidth: 70 }}>
<Text style={s.metricLabel}>{label}</Text>
<Text style={s.stepValue}>{value}</Text>
</View>
<Pressable
accessibilityRole="button"
accessibilityLabel={`Diminuer ${label}`}
onPress={minus}
style={({ pressed }) => [s.stepBtn, pressed && s.pressed]}
>
<Minus size={20} color={C.green} />
</Pressable>
<Pressable
accessibilityRole="button"
accessibilityLabel={`Augmenter ${label}`}
onPress={plus}
style={({ pressed }) => [s.stepBtn, pressed && s.pressed]}
>
<Plus size={20} color={C.green} />
</Pressable>
</View>
);
}
function Progress({ entries }: any) {
const recent: Entry[] = entries.slice(-7);
if (recent.length === 0)
return (
<ScrollView contentContainerStyle={s.page}>
<Header eyebrow="MES TENDANCES" title="Mes progrès" />
<Card style={s.empty}>
<IconTile icon={TrendingUp} />
<Text style={s.tipTitle}>Tes tendances apparaîtront ici</Text>
<Text style={s.body}>
Ajoute au moins deux journées pour suivre ton évolution, sans
pression.
</Text>
</Card>
</ScrollView>
);
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 (
<ScrollView contentContainerStyle={s.page}>
<Header
eyebrow="7 DERNIERS JOURS"
title="Mes progrès"
right={
<Pill tone="coral">
{rawDelta <= 0 ? "Baisse" : "Hausse"}{" "}
{Math.abs(rawDelta).toFixed(1).replace(".", ",")} kg
</Pill>
}
/>
<Text style={s.lead}>Observe les tendances, pas la perfection.</Text>
<Card>
<View style={s.sectionHead}>
<View style={{ flex: 1 }}>
<Text style={s.metricLabel}>Tendance de poids</Text>
<Text style={s.bigNumber}>
{recent.at(-1)!.weight} <Text style={s.bigUnit}>kg</Text>
</Text>
</View>
<Pill>Données personnelles</Pill>
</View>
<View style={s.chart}>
{recent.map((e, i) => (
<View key={`${e.date}-${i}`} style={s.barCol}>
<Text style={s.barValue}>{e.weight}</Text>
<View
style={[
s.bar,
{ height: 58 + ((max - e.weight) / (max - min || 1)) * 55 },
]}
/>
<Text style={s.barLabel}>{e.date}</Text>
</View>
))}
</View>
</Card>
<View style={s.grid}>
<Card style={s.stat}>
<IconTile icon={Droplets} />
<Text style={s.bigNumber}>
{(avg("water") / 1000).toFixed(1).replace(".", ",")} L
</Text>
<Text style={s.body}>moyenne deau</Text>
</Card>
<Card style={s.stat}>
<IconTile icon={Footprints} />
<Text style={s.bigNumber}>
{avg("steps").toLocaleString("fr-FR")}
</Text>
<Text style={s.body}>pas en moyenne</Text>
</Card>
</View>
<Card style={s.insight}>
<Text style={s.tipTag}>À RETENIR</Text>
<Text style={s.tipTitle}>Une semaine régulière 🌱</Text>
<Text style={s.body}>
Tu as bougé au moins 6 000 pas chaque jour. Cette régularité compte
davantage quune journée parfaite.
</Text>
</Card>
</ScrollView>
);
}
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 (
<ScrollView
contentContainerStyle={s.page}
keyboardShouldPersistTaps="handled"
>
<Header
eyebrow="COACHING PERSONNALISÉ"
title="Peg, ton coach IA"
right={<Pill>{journey}</Pill>}
/>
<PegChat journey={journey} />
<Text style={s.h2}>Pour aller plus loin</Text>
{lessons.map(([Icon, title, duration]: [LucideIcon, string, string]) => (
<Pressable
key={title}
accessibilityRole="button"
accessibilityLabel={`${title}, ${duration}`}
style={({ pressed }) => [s.card, s.lesson, pressed && s.pressed]}
>
<IconTile icon={Icon} />
<View style={s.flexCopy}>
<Text style={s.tipTitle}>{title}</Text>
<Text style={s.body}>{duration} · contenu éducatif</Text>
</View>
<ChevronRight size={22} color={C.muted} />
</Pressable>
))}
<Card>
<IconTile icon={Apple} />
<Text style={s.tipTag}>CONNEXIONS SANTÉ</Text>
<Text style={s.tipTitle}>Apple Santé & Health Connect</Text>
<Text style={s.body}>
Disponible prochainement après activation des autorisations natives.
</Text>
</Card>
</ScrollView>
);
}
function PegChat({ journey }: { journey: string }) {
const apiBase = (
process.env.EXPO_PUBLIC_API_URL || "https://api.metabloom.vicode.agency"
).replace(/\/$/, "");
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: "welcome",
role: "assistant",
content: `Bonjour, je suis Peg. Je peux taider à trouver de petites actions adaptées à ton parcours ${journey}, sans jugement. De quoi aimerais-tu parler aujourdhui ?`,
},
]);
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 aujourdhui",
];
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 durgence, contacte un professionnel ou les services durgence.",
},
]);
} finally {
setLoading(false);
}
};
return (
<View style={s.pegCard}>
<View style={s.pegHeader}>
<View style={s.pegAvatar}>
<Bot size={24} color={C.white} />
</View>
<View style={s.flexCopy}>
<Text style={s.pegName}>Peg</Text>
<Text style={s.pegStatus}>Coach bien-être · conseils éducatifs</Text>
</View>
<ShieldCheck size={21} color={C.green} />
</View>
<View style={s.chatList}>
{messages.map((message) => (
<View
key={message.id}
style={[s.messageRow, message.role === "user" && s.messageRowUser]}
>
<View
style={[
s.messageAvatar,
message.role === "user" && s.messageAvatarUser,
]}
>
{message.role === "assistant" ? (
<Bot size={16} color={C.green} />
) : (
<UserRound size={16} color="#7659a6" />
)}
</View>
<View style={[s.bubble, message.role === "user" && s.bubbleUser]}>
<Text
style={[
s.bubbleText,
message.role === "user" && s.bubbleTextUser,
]}
>
{message.content}
</Text>
</View>
</View>
))}
{loading && (
<View style={s.typing}>
<ActivityIndicator color={C.green} />
<Text style={s.pegStatus}>Peg réfléchit</Text>
</View>
)}
</View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={s.suggestions}
>
{suggestions.map((item) => (
<Pressable
key={item}
accessibilityRole="button"
onPress={() => send(item)}
style={({ pressed }) => [s.suggestion, pressed && s.pressed]}
>
<Text style={s.suggestionText}>{item}</Text>
</Pressable>
))}
</ScrollView>
<View style={s.composer}>
<TextInput
accessibilityLabel="Message pour Peg"
placeholder="Écris à Peg…"
placeholderTextColor="#7b8880"
value={input}
onChangeText={setInput}
multiline
maxLength={500}
style={s.chatInput}
/>
<Pressable
accessibilityRole="button"
accessibilityLabel="Envoyer à Peg"
disabled={!input.trim() || loading}
onPress={() => send()}
style={({ pressed }) => [
s.sendButton,
(!input.trim() || loading) && s.sendDisabled,
pressed && s.pressed,
]}
>
<Send size={20} color={C.white} />
</Pressable>
</View>
<Text style={s.pegDisclaimer}>
Peg peut se tromper. Elle ne remplace pas un professionnel de santé et
ne donne aucun diagnostic ni conseil de traitement.
</Text>
</View>
);
}
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 (
<ScrollView contentContainerStyle={s.page}>
<Header eyebrow="MON ESPACE" title="Profil & sécurité" />
<Card style={s.profile}>
<View style={s.avatarLarge}>
<Text style={s.avatarLargeText}>
{profile.name[0]?.toUpperCase()}
</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={s.tipTitle}>{profile.name}</Text>
<Text style={s.body}>
{profile.goal} · {profile.journey}
</Text>
</View>
</Card>
<Text style={s.h2}>Préférences</Text>
<Card style={s.setting}>
<IconTile icon={Activity} />
<View style={{ flex: 1 }}>
<Text style={s.metricLabel}>Unité glycémie</Text>
<Text style={s.body}>{profile.glucoseUnit}</Text>
</View>
<Pressable
accessibilityRole="button"
accessibilityLabel="Changer lunité de glycémie"
onPress={() =>
setProfile({
...profile,
glucoseUnit: profile.glucoseUnit === "mg/dL" ? "mmol/L" : "mg/dL",
})
}
style={({ pressed }) => [s.smallButton, pressed && s.pressed]}
>
<Text style={s.smallButtonText}>Changer</Text>
</Pressable>
</Card>
<Card style={s.setting}>
<IconTile icon={Bell} />
<View style={{ flex: 1 }}>
<Text style={s.metricLabel}>Rappels doux</Text>
<Text style={s.body}>
{profile.reminders ? "Activés à 19 h" : "Désactivés"}
</Text>
</View>
<Switch
accessibilityLabel="Activer les rappels"
value={profile.reminders}
onValueChange={(reminders) => setProfile({ ...profile, reminders })}
trackColor={{ true: C.green }}
/>
</Card>
<Pressable
accessibilityRole="button"
accessibilityLabel="Exporter mes données CSV"
onPress={exportData}
style={({ pressed }) => [s.secondaryCard, pressed && s.pressed]}
>
<Download size={20} color={C.green} />
<Text style={s.secondaryCardText}>Exporter mes données CSV</Text>
</Pressable>
<Card style={s.safety}>
<IconTile icon={ShieldCheck} />
<Text style={s.tipTag}>SÉCURITÉ & CONFIDENTIALITÉ</Text>
<Text style={s.tipTitle}>Tes données restent sous ton contrôle.</Text>
<Text style={s.body}>
Tu peux les exporter ou les supprimer à tout moment. La
synchronisation en ligne nest pas activée dans cette version.
</Text>
</Card>
<Pressable
onPress={() =>
Alert.alert(
"Effacer les données locales ?",
"Cette action supprime les données de cet appareil et réinitialise lapplication.",
[
{ text: "Annuler" },
{ text: "Effacer", style: "destructive", onPress: reset },
],
)
}
style={s.danger}
>
<Text style={s.dangerText}>Effacer les données de cet appareil</Text>
</Pressable>
<Text style={s.disclaimer}>
En cas de malaise ou durgence, contactez immédiatement les services
durgence. MetaBloom ne fournit pas de conseil thérapeutique.
</Text>
</ScrollView>
);
}
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 },
});