From 3ce60c21de7b87e2ef9097b3347405de6f3c3a50 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Wed, 8 Apr 2026 13:15:47 +0200 Subject: [PATCH] feat: inline custom meals, calorie ring overflow animation, theme fixes Add custom meals tab to inline food add section with search/meals toggle. Animate calorie ring overflow (red) after primary fill completes, with separate glow elements so red overflow glows red independently. Apply same delayed overflow animation to macro progress bars. Replace hardcoded nord8 with --color-primary throughout nutrition page (today badge, ring, tabs, buttons). Add custom clear button to FoodSearch, hide number input spinners globally. --- package.json | 2 +- src/app.css | 10 ++ src/lib/components/fitness/FoodSearch.svelte | 25 ++- .../[measure=fitnessMeasure]/+page.svelte | 43 ++++- .../[nutrition=fitnessNutrition]/+page.svelte | 161 ++++++++++++++++-- 5 files changed, 216 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 62bbbbf..4a05e5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.11.0", + "version": "1.11.1", "private": true, "type": "module", "scripts": { diff --git a/src/app.css b/src/app.css index 793b3a0..9df09e5 100644 --- a/src/app.css +++ b/src/app.css @@ -272,6 +272,16 @@ font-family: Helvetica, Arial, "Noto Sans", sans-serif; } +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} +input[type="number"] { + -moz-appearance: textfield; + appearance: textfield; +} + body { margin: 0; padding: 0; diff --git a/src/lib/components/fitness/FoodSearch.svelte b/src/lib/components/fitness/FoodSearch.svelte index bc9d3ba..a4e54bf 100644 --- a/src/lib/components/fitness/FoodSearch.svelte +++ b/src/lib/components/fitness/FoodSearch.svelte @@ -357,13 +357,18 @@
+ {#if query} + + {/if} {#if browser} + +
@@ -293,8 +297,33 @@ } .profile-fields { display: flex; - gap: 0.75rem; + gap: 0.5rem; align-items: flex-end; + justify-content: flex-end; + flex-wrap: wrap; + } + .sex-pills { + display: flex; + gap: 0.4rem; + } + .sex-pill { + display: flex; + align-items: center; + gap: 0.3rem; + padding: 0.35rem 0.65rem; + border: 1px solid var(--color-border); + border-radius: 20px; + background: var(--color-bg-tertiary); + color: var(--color-text-secondary); + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + transition: all 150ms; + } + .sex-pill.active { + background: var(--color-primary); + border-color: var(--color-primary); + color: var(--color-text-on-primary); } .profile-save-btn { padding: 0.4rem 0.75rem; @@ -315,11 +344,11 @@ } .form-group { - flex: 1; display: flex; flex-direction: column; gap: 0.2rem; margin-bottom: 0.4rem; + flex-shrink: 0; } .form-group label { font-size: 0.7rem; diff --git a/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte b/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte index 9b09fa5..6672568 100644 --- a/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte +++ b/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte @@ -468,6 +468,7 @@ // --- Inline add food --- let addingMeal = $state(null); + let inlineTab = $state('search'); // 'search' | 'meals' // --- FAB modal (route-based via ?add param) --- const showFabModal = $derived($page.url.searchParams.has('add')); @@ -588,12 +589,57 @@ function startAdd(meal) { addingMeal = meal; + inlineTab = 'search'; + loadCustomMeals(); } function cancelAdd() { addingMeal = null; } + async function inlineLogCustomMeal(meal) { + if (!addingMeal) return; + try { + const totals = {}; + const nutrientKeys = ['calories', 'protein', 'fat', 'saturatedFat', 'carbs', 'fiber', 'sugars', + 'calcium', 'iron', 'magnesium', 'phosphorus', 'potassium', 'sodium', 'zinc', + 'vitaminA', 'vitaminC', 'vitaminD', 'vitaminE', 'vitaminK', + 'thiamin', 'riboflavin', 'niacin', 'vitaminB6', 'vitaminB12', 'folate', 'cholesterol', + 'isoleucine', 'leucine', 'lysine', 'methionine', 'phenylalanine', 'threonine', + 'tryptophan', 'valine', 'histidine', 'alanine', 'arginine', 'asparticAcid', + 'cysteine', 'glutamicAcid', 'glycine', 'proline', 'serine', 'tyrosine']; + for (const k of nutrientKeys) totals[k] = 0; + let totalGrams = 0; + for (const ing of meal.ingredients) { + const r = ing.amountGrams / 100; + totalGrams += ing.amountGrams; + for (const k of nutrientKeys) totals[k] += (ing.per100g?.[k] ?? 0) * r; + } + const per100g = {}; + const scale = totalGrams > 0 ? 100 / totalGrams : 0; + for (const k of nutrientKeys) per100g[k] = totals[k] * scale; + + await fetch('/api/fitness/food-log', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + date: currentDate, + mealType: addingMeal, + name: meal.name, + source: 'custom', + sourceId: meal._id, + amountGrams: totalGrams, + per100g, + }) + }); + await goto(`/fitness/${s.nutrition}?date=${currentDate}`, { replaceState: true, noScroll: true }); + cancelAdd(); + toast.success(isEn ? `Logged "${meal.name}"` : `"${meal.name}" eingetragen`); + } catch { + toast.error(isEn ? 'Failed to log meal' : 'Fehler beim Eintragen'); + } + } + async function inlineLogFood(food) { try { const res = await fetch('/api/fitness/food-log', { @@ -776,14 +822,21 @@ - 0} + + {/if} + {#if calorieOverflow > 0} {/if} {fmtCal(Math.abs(calorieBalance))} @@ -1241,7 +1294,41 @@ {#if addingMeal === meal}
- +
+
+ + +
+ +
+ + {#if inlineTab === 'search'} + + {:else} +
+ {#if customMeals.length === 0} +

{t('no_custom_meals', lang)}

+ {/if} + {#each customMeals as cm} +
+
+ {cm.name} + {cm.ingredients.length} {t('ingredients', lang)} · {fmtCal(mealTotalCal(cm))} kcal +
+ +
+ {/each} + + + {isEn ? 'Manage meals' : 'Mahlzeiten verwalten'} + +
+ {/if}
{:else}