BodyMetricsCard
Reworks meal-tracker's ProfilTab body-stats card (gender/weight/height key-value rows + an animated body-fat-% reveal with a category badge) into a yemek component. The original was GSAP-driven (count-up tween, background flash, badge spring, two pulse rings) — this keeps the choreography but in pure CSS keyframes, since yemek has no GSAP dependency and doesn't need one.
Playground
Körpermaße
GeschlechtMännlich
Gewicht82 kg
Größe181 cm
Körperfett14.2%Definiert
Click "Neu würfeln" — the % pops in, the row flashes, and the badge springs in with two expanding pulse rings, same choreography as the original GSAP version.
vue
<script setup>
import { BodyMetricsCard } from 'yemek'
const saved = ref(false)
async function saveBodyMetrics() {
await supabase.auth.updateUser({ data: { /* ... */ } })
saved.value = true
setTimeout(() => { saved.value = false }, 2000)
}
</script>
<template>
<BodyMetricsCard
title="Körpermaße"
:rows="[
{ label: 'Geschlecht', value: gender === 'male' ? 'Männlich' : 'Weiblich' },
{ label: 'Gewicht', value: `${storedWeight} kg` },
{ label: 'Größe', value: `${heightCm} cm` }
]"
body-fat-label="Körperfett"
:body-fat-percent="bodyFatPercent ?? undefined"
:body-fat-category-label="bodyFatCategory ? t(`profil.bfCategory.${bodyFatCategory}`) : undefined"
:body-fat-status="bodyFatStatus"
incomplete-label="Miss Bauch-, Hals- und Hüftumfang für eine Körperfett-Schätzung."
edit-label="Bearbeiten"
saved-label="Gespeichert"
:saved="saved"
@edit="openBodyDialog"
/>
</template>Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | Card heading. |
rows | { label: string; value: string }[] | — | Already-formatted key/value rows (gender, weight, height, ...). Generic — not limited to exactly those three. |
bodyFatLabel | string | undefined | Label for the body-fat row. Row only renders when bodyFatPercent is set. |
bodyFatPercent | number | undefined | Body-fat percentage. Omit entirely (not 0/null) to show incompleteLabel instead. |
bodyFatCategoryLabel | string | undefined | Already-localized category text for the badge (e.g. "Definiert"). |
bodyFatStatus | 'good' | 'warn' | 'danger' | 'good' | Generic 3-tier status the consumer maps its own classification down to (e.g. meal-tracker's 4-category defined/athletic → good, slightlyElevated → warn, elevated → danger) — same pattern as MetricRing's status prop. |
incompleteLabel | string | undefined | Shown instead of the body-fat row when bodyFatPercent is omitted. |
editLabel | string | — | Edit button label. |
savedLabel | string | undefined | Shown (with a checkmark) instead of editLabel while saved is true. |
saved | boolean | false | Swaps the edit button for a checkmark + savedLabel — the consumer flips this after its own save call resolves (no in-flight spinner state here, unlike MealReviewCard's saving). |
Events
| Event | Payload | Fired when |
|---|---|---|
edit | — | The edit button is clicked. |
Behavior notes
- No body-fat classification or unit-conversion logic lives here —
rowsare already-formatted strings, andbodyFatStatusis the consumer's own classification already mapped to a generic 3-tier status. bodyFatStatus="warn"deliberately uses yemek's terracotta highlight token, not the same red as"danger"— "slightly elevated" and "elevated" shouldn't read as the same severity.- The reveal (value pop, row flash, badge spring, two staggered pulse rings) re-triggers any time
bodyFatPercentactually changes value — not on every render, and not on initial mount (a returning user sees their stored value immediately, the animation only plays right after a fresh save). prefers-reduced-motion: reducedisables the flash/spring/pulse animations entirely — the new value/badge still swap in, just without the accompanying motion.