diff --git a/CLAUDE.md b/CLAUDE.md index d313269..8b37a6f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,6 +51,12 @@ After completing the code, ask the user if they want a playground link. Only cal - **NEVER** write `@media (prefers-color-scheme: dark)` or `:global(:root[data-theme="dark"])` override blocks — semantic variables handle both themes automatically - **NEVER** use `var(--font-default-dark)` or `var(--accent-dark)` — these are legacy +### Primary interactive elements +- Background: `var(--color-primary)` (nord10 light / nord8 dark) +- Hover: `var(--color-primary-hover)` +- Active: `var(--color-primary-active)` +- Text on primary bg: `var(--color-text-on-primary)` + ### Accent colors (OK to use directly, they work in both themes) - `var(--blue)`, `var(--red)`, `var(--green)`, `var(--orange)` — named accent colors - `var(--nord10)`, `var(--nord11)`, `var(--nord12)`, `var(--nord14)` — OK for hover states of accent-colored buttons only diff --git a/package.json b/package.json index 08679dc..560b46a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.16.0", + "version": "1.17.0", "private": true, "type": "module", "scripts": { diff --git a/src/lib/components/fitness/FoodSearch.svelte b/src/lib/components/fitness/FoodSearch.svelte index a4e54bf..fcb5c51 100644 --- a/src/lib/components/fitness/FoodSearch.svelte +++ b/src/lib/components/fitness/FoodSearch.svelte @@ -12,6 +12,7 @@ * showDetailLinks?: boolean, * autofocus?: boolean, * confirmLabel?: string, + * initialResults?: any[], * }} */ let { @@ -21,6 +22,7 @@ showDetailLinks = true, autofocus = false, confirmLabel = undefined, + initialResults = undefined, } = $props(); const lang = $derived(detectFitnessLang($page.url.pathname)); @@ -28,11 +30,27 @@ const isEn = $derived(lang === 'en'); const btnLabel = $derived(confirmLabel ?? t('log_food', lang)); + // SVG ring constants + const RADIUS = 28; + const ARC_DEGREES = 300; + const ARC_LENGTH = (ARC_DEGREES / 360) * 2 * Math.PI * RADIUS; + const ARC_ROTATE = 120; + function strokeOffset(percent) { + return ARC_LENGTH - (Math.min(percent, 100) / 100) * ARC_LENGTH; + } + // --- Search state --- let query = $state(''); - let results = $state([]); + let results = $state(initialResults ?? []); let loading = $state(false); let timeout = $state(null); + const isPrefilledMode = $derived(initialResults != null); + let filterQuery = $state(''); + const displayResults = $derived( + isPrefilledMode && filterQuery + ? results.filter(r => r.name.toLowerCase().includes(filterQuery.toLowerCase())) + : results + ); // --- Selection state --- let selected = $state(null); @@ -92,6 +110,37 @@ return qty; }); + /** Scaled nutrient values for the preview */ + const previewNutrients = $derived.by(() => { + if (!selected?.per100g || !previewGrams) return null; + const s = previewGrams / 100; + const n = selected.per100g; + return { + calories: Math.round((n.calories ?? 0) * s), + protein: (n.protein ?? 0) * s, + fat: (n.fat ?? 0) * s, + carbs: (n.carbs ?? 0) * s, + saturatedFat: (n.saturatedFat ?? 0) * s, + sugars: (n.sugars ?? 0) * s, + fiber: (n.fiber ?? 0) * s, + }; + }); + + const macroPercent = $derived.by(() => { + if (!selected?.per100g) return { protein: 0, fat: 0, carbs: 0 }; + const n = selected.per100g; + const proteinCal = (n.protein ?? 0) * 4; + const fatCal = (n.fat ?? 0) * 9; + const carbsCal = (n.carbs ?? 0) * 4; + const total = proteinCal + fatCal + carbsCal; + if (total === 0) return { protein: 0, fat: 0, carbs: 0 }; + return { + protein: Math.round(proteinCal / total * 100), + fat: Math.round(fatCal / total * 100), + carbs: 100 - Math.round(proteinCal / total * 100) - Math.round(fatCal / total * 100), + }; + }); + function confirm() { if (!selected) return; const grams = resolveGrams(); @@ -354,36 +403,47 @@ {/if} {:else if !selected} -
- - - {#if query} - + {#if isPrefilledMode} + {#if results.length > 3} + {/if} - {#if browser} - - {/if} -
+ {:else} +
+ + + {#if query} + + {/if} + {#if browser} + + {/if} +
+ {/if} {#if scanError}

{scanError}

{/if} {#if loading}

{t('loading', lang)}

{/if} - {#if results.length > 0} + {#if displayResults.length > 0}
- {#each results as item} + {#each displayResults as item}
{#if showFavorites} {/if} {:else} - +
@@ -425,6 +485,8 @@ {selected.brands} {/if}
+ +
g {/if}
- {#if previewGrams > 0} -
- {#if portionIdx >= 0} - {previewGrams}g - {/if} - {Math.round((selected.per100g?.calories ?? 0) * previewGrams / 100)} kcal - {fmt((selected.per100g?.protein ?? 0) * previewGrams / 100)}g P - {fmt((selected.per100g?.fat ?? 0) * previewGrams / 100)}g F - {fmt((selected.per100g?.carbs ?? 0) * previewGrams / 100)}g C + {#if portionIdx >= 0 && previewGrams > 0} + = {previewGrams}g + {/if} + + {#if previewNutrients} + +
+ {previewNutrients.calories} + kcal +
+ + +
+ {#each [ + { pct: macroPercent.protein, label: isEn ? 'Protein' : 'Eiweiß', cls: 'fs-ring-protein', grams: previewNutrients.protein }, + { pct: macroPercent.fat, label: isEn ? 'Fat' : 'Fett', cls: 'fs-ring-fat', grams: previewNutrients.fat }, + { pct: macroPercent.carbs, label: isEn ? 'Carbs' : 'Kohlenh.', cls: 'fs-ring-carbs', grams: previewNutrients.carbs }, + ] as macro (macro.cls)} +
+ + + + {macro.pct}% + + {macro.label} + {fmt(macro.grams)}g +
+ {/each} +
+ + +
+
+ {isEn ? 'Protein' : 'Eiweiß'} + {fmt(previewNutrients.protein)} g +
+
+ {isEn ? 'Fat' : 'Fett'} + {fmt(previewNutrients.fat)} g +
+
+ {isEn ? 'Saturated Fat' : 'Ges. Fettsäuren'} + {fmt(previewNutrients.saturatedFat)} g +
+
+ {isEn ? 'Carbohydrates' : 'Kohlenhydrate'} + {fmt(previewNutrients.carbs)} g +
+
+ {isEn ? 'Sugars' : 'Zucker'} + {fmt(previewNutrients.sugars)} g +
+
+ {isEn ? 'Fiber' : 'Ballaststoffe'} + {fmt(previewNutrients.fiber)} g +
{/if} +
@@ -488,6 +603,23 @@ transition: border-color 0.15s; min-width: 0; } + .fs-filter-input { + display: block; + width: 100%; + padding: 0.55rem 0.65rem; + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: 8px; + color: var(--color-text-primary); + font-size: 0.9rem; + box-sizing: border-box; + transition: border-color 0.15s; + margin-bottom: 0.25rem; + } + .fs-filter-input:focus { + outline: none; + border-color: var(--color-primary); + } .fs-search-input:focus { outline: none; border-color: var(--color-primary); @@ -786,24 +918,97 @@ color: var(--color-text-secondary); } - /* ── Preview ── */ - .fs-preview { - display: flex; - gap: 0.5rem; - font-size: 0.78rem; - padding: 0.5rem 0.6rem; - background: var(--color-bg-tertiary); - border-radius: 8px; - font-variant-numeric: tabular-nums; - } - .fs-preview-grams { + /* ── Detail view ── */ + .fs-detail-hint { + font-size: 0.75rem; color: var(--color-text-tertiary); } - .fs-preview-cal { font-weight: 700; color: var(--color-text-primary); } - .fs-preview-cal small { font-weight: 500; color: var(--color-text-secondary); } - .fs-preview-p { color: var(--nord14); font-weight: 600; } - .fs-preview-f { color: var(--nord12); font-weight: 600; } - .fs-preview-c { color: var(--nord9); font-weight: 600; } + .fs-detail-cal { + text-align: center; + margin: 0.25rem 0 0.25rem; + } + .fs-detail-cal-num { + font-size: 2.2rem; + font-weight: 800; + color: var(--color-text-primary); + line-height: 1; + } + .fs-detail-cal-unit { + font-size: 1rem; + font-weight: 600; + color: var(--color-text-secondary); + margin-left: 0.2rem; + } + .fs-detail-macros { + display: flex; + justify-content: space-around; + margin: 0.25rem 0 0.5rem; + } + .fs-detail-macro { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.1rem; + flex: 1; + } + .fs-detail-macro-label { + font-size: 0.78rem; + font-weight: 600; + color: var(--color-text-primary); + text-align: center; + } + .fs-detail-macro-val { + font-size: 0.72rem; + color: var(--color-text-tertiary); + } + .fs-ring-bg { + fill: none; + stroke: var(--color-border); + stroke-width: 5; + stroke-linecap: round; + } + .fs-ring-fill { + fill: none; + stroke-width: 5; + stroke-linecap: round; + transition: stroke-dashoffset 0.4s ease; + } + .fs-ring-text { + font-size: 14px; + font-weight: 700; + fill: currentColor; + text-anchor: middle; + dominant-baseline: central; + } + .fs-ring-protein { stroke: var(--nord14); } + .fs-ring-fat { stroke: var(--nord12); } + .fs-ring-carbs { stroke: var(--nord9); } + .fs-detail-rows { + background: var(--color-surface); + border-radius: 10px; + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border); + } + .fs-detail-row { + display: flex; + justify-content: space-between; + padding: 0.3rem 0; + border-bottom: 1px solid var(--color-border); + font-size: 0.85rem; + color: var(--color-text-primary); + } + .fs-detail-row:last-child { + border-bottom: none; + } + .fs-detail-row.sub span:first-child { + padding-left: 0.75rem; + color: var(--color-text-tertiary); + font-size: 0.8rem; + } + .fs-detail-row span:last-child { + color: var(--color-text-secondary); + font-variant-numeric: tabular-nums; + } /* ── Buttons ── */ .fs-actions { @@ -814,12 +1019,13 @@ .fs-btn-cancel { padding: 0.5rem 1.1rem; background: var(--color-bg-tertiary); - color: var(--color-text-primary); + color: var(--color-text-secondary); border: 1px solid var(--color-border); border-radius: 8px; cursor: pointer; font-size: 0.82rem; font-weight: 500; + flex: 1; transition: background 0.15s; } .fs-btn-cancel:hover { @@ -827,17 +1033,18 @@ } .fs-btn-confirm { padding: 0.5rem 1.1rem; - background: var(--nord8); - color: white; + background: var(--color-primary); + color: var(--color-text-on-primary); border: none; border-radius: 8px; cursor: pointer; font-size: 0.82rem; font-weight: 700; - transition: background 0.15s, transform 0.1s; + flex: 2; + transition: opacity 0.15s, transform 0.1s; } .fs-btn-confirm:hover { - background: var(--nord10); + opacity: 0.9; } .fs-btn-confirm:active { transform: scale(0.97); diff --git a/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte b/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte index 0bedddc..c449c50 100644 --- a/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte +++ b/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte @@ -1,7 +1,7 @@