feat: show macro/calorie overflow with red reverse-fill indicators

When intake exceeds goals, macro bars show a red segment growing from
the right edge backwards and the calorie ring draws a red arc backwards
from the 100% mark, clearly visualizing the overrun amount.
This commit is contained in:
2026-04-08 10:28:36 +02:00
parent cb35a5c3dc
commit a74bd15a57

View File

@@ -444,7 +444,9 @@
// Net calorie balance: goal + burned - eaten
const calorieBalance = $derived(goalCalories ? (goalCalories + (exerciseKcal || 0) - dayTotals.calories) : 0);
const calorieProgress = $derived(goalCalories ? Math.min(dayTotals.calories / (goalCalories + (exerciseKcal || 0)) * 100, 100) : 0);
const calorieProgressRaw = $derived(goalCalories ? dayTotals.calories / (goalCalories + (exerciseKcal || 0)) * 100 : 0);
const calorieProgress = $derived(Math.min(calorieProgressRaw, 100));
const calorieOverflow = $derived(Math.max(calorieProgressRaw - 100, 0));
// DRI for micros
const dri = $derived(getDRI(goalSex));
@@ -459,6 +461,11 @@
return ARC_LENGTH - (Math.min(percent, 100) / 100) * ARC_LENGTH;
}
/** Stroke offset for overflow arc drawn from the end backwards */
function overflowOffset(overflowPct) {
return ARC_LENGTH - (Math.min(overflowPct, 100) / 100) * ARC_LENGTH;
}
// --- Inline add food ---
let addingMeal = $state(null);
@@ -746,6 +753,12 @@
stroke-dasharray="{ARC_LENGTH} {2 * Math.PI * RADIUS}"
stroke-dashoffset={strokeOffset(calorieProgress)}
transform="rotate({ARC_ROTATE} 35 35)" />
{#if calorieOverflow > 0}
<circle class="ring-fill ring-overflow" cx="35" cy="35" r={RADIUS}
stroke-dasharray="{ARC_LENGTH} {2 * Math.PI * RADIUS}"
stroke-dashoffset={overflowOffset(calorieOverflow)}
transform="translate(70, 0) scale(-1, 1) rotate({ARC_ROTATE} 35 35)" />
{/if}
<text class="ring-text-main" x="35" y="30">{fmtCal(Math.abs(calorieBalance))}</text>
<text class="ring-text-sub" x="35" y="42">{calorieBalance >= 0 ? (isEn ? 'KCAL LEFT' : 'KCAL ÜBRIG') : (isEn ? 'KCAL OVER' : 'KCAL ÜBER')}</text>
</svg>
@@ -783,11 +796,15 @@
] as macro}
{@const pct = macro.goal ? macro.value / macro.goal * 100 : 0}
{@const over = pct > 100}
{@const overPct = Math.min(pct - 100, 100)}
{@const remaining = macro.goal ? macro.goal - macro.value : 0}
<div class="macro-bar-item">
<span class="macro-bar-label">{macro.label}</span>
<div class="macro-bar-track">
<div class="macro-bar-fill" class:over style="width: {Math.min(pct, 100)}%; {over ? '' : `background: ${macro.color}`}"></div>
<div class="macro-bar-fill" style="width: {Math.min(pct, 100)}%; background: {macro.color}"></div>
{#if over}
<div class="macro-bar-overflow" style="width: {overPct}%"></div>
{/if}
</div>
{#if macro.goal}
<span class="macro-bar-info" class:over>
@@ -1473,14 +1490,21 @@
border: 1px solid var(--color-border);
border-radius: 3px;
overflow: hidden;
position: relative;
}
.macro-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.macro-bar-fill.over {
background: var(--nord11) !important;
.macro-bar-overflow {
position: absolute;
top: 0;
right: 0;
height: 100%;
background: var(--nord11);
border-radius: 0 3px 3px 0;
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.macro-bar-info {
font-size: 0.68rem;
@@ -1506,6 +1530,10 @@
transition: stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.ring-calories { stroke: var(--nord8); }
.ring-overflow {
stroke: var(--nord11);
transition: stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.ring-text-main {
font-size: 14px;
font-weight: 800;