Files
kochwas/src/lib/components/RecipeView.svelte
hsiegeln dc04f5b032
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m21s
feat(recipe): Schrift im Tablet/Desktop-Layout vergrößert
Auf dem 10"-Tablet war die Schrift in Zutaten und Zubereitung zu klein.
Im 2-Spalten-Layout (>=820px) bumpen wir jetzt:
- Zutaten-Zeilen und Step-Text auf 1.2rem (vorher 1rem)
- qty-Spalte breiter (6rem statt 5rem)
- Portionen-Zahl größer
- Step-Badge auf 2.4rem + 1.1rem Font

Mobile bleibt unverändert — Lesedistanz ist dort anders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 21:25:17 +02:00

399 lines
9.0 KiB
Svelte
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.
<script lang="ts">
import { scaleIngredients } from '$lib/recipes/scaler';
import type { Recipe } from '$lib/types';
type Props = {
recipe: Recipe;
showActions?: import('svelte').Snippet;
banner?: import('svelte').Snippet;
titleSlot?: import('svelte').Snippet;
};
let { recipe, showActions, banner, titleSlot }: Props = $props();
const defaultServings = $derived(recipe.servings_default ?? 4);
let servingsOverride = $state<number | null>(null);
const servings = $derived(servingsOverride ?? defaultServings);
const factor = $derived(servings / defaultServings);
const scaled = $derived(scaleIngredients(recipe.ingredients, factor));
let tab = $state<'ing' | 'prep'>('ing');
// In preview, image_path is still an external URL; after save, it's a local
// filename served under /images/. Detect absolute URLs and pass through.
const imageSrc = $derived(
recipe.image_path === null
? null
: /^https?:\/\//i.test(recipe.image_path)
? recipe.image_path
: `/images/${recipe.image_path}`
);
function decr() {
if (servings > 1) servingsOverride = servings - 1;
}
function incr() {
if (servings < 100) servingsOverride = servings + 1;
}
function formatQty(q: number | null): string {
if (q === null) return '';
if (Number.isInteger(q)) return String(q);
return q.toLocaleString('de-DE', { maximumFractionDigits: 2 });
}
function timeSummary(): string {
const parts: string[] = [];
if (recipe.prep_time_min) parts.push(`Vorb. ${recipe.prep_time_min} min`);
if (recipe.cook_time_min) parts.push(`Kochen ${recipe.cook_time_min} min`);
if (!recipe.prep_time_min && !recipe.cook_time_min && recipe.total_time_min)
parts.push(`Gesamt ${recipe.total_time_min} min`);
return parts.join(' · ');
}
</script>
{#if banner}
{@render banner()}
{/if}
<article class="recipe">
<header class="hdr">
{#if imageSrc}
<img src={imageSrc} alt="" class="cover" loading="eager" referrerpolicy="no-referrer" />
{/if}
<div class="hdr-body">
{#if titleSlot}
{@render titleSlot()}
{:else}
<h1>{recipe.title}</h1>
{/if}
{#if recipe.description}
<p class="desc">{recipe.description}</p>
{/if}
<div class="meta">
{#if recipe.category}<span class="pill">{recipe.category}</span>{/if}
{#if recipe.cuisine}<span class="pill">{recipe.cuisine}</span>{/if}
{#if recipe.tags}
{#each recipe.tags.slice(0, 6) as t (t)}
<span class="tag">#{t}</span>
{/each}
{/if}
</div>
{#if timeSummary()}
<p class="times">{timeSummary()}</p>
{/if}
{#if recipe.source_url}
<p class="src">
Quelle: <a href={recipe.source_url} target="_blank" rel="noopener">{recipe.source_domain}</a>
</p>
{/if}
</div>
</header>
{#if showActions}
<div class="actions">
{@render showActions()}
</div>
{/if}
<div class="tabs" role="tablist">
<button
role="tab"
aria-selected={tab === 'ing'}
class="tab"
class:active={tab === 'ing'}
onclick={() => (tab = 'ing')}
>
Zutaten
</button>
<button
role="tab"
aria-selected={tab === 'prep'}
class="tab"
class:active={tab === 'prep'}
onclick={() => (tab = 'prep')}
>
Zubereitung
</button>
</div>
<div class="panes">
<section
class="ingredients"
role="tabpanel"
class:hidden-mobile={tab !== 'ing'}
>
<div class="servings">
<button class="srv-btn" aria-label="Weniger" onclick={decr}></button>
<div class="srv-value">
<strong>{servings}</strong>
<span>{recipe.servings_unit ?? 'Portionen'}</span>
</div>
<button class="srv-btn" aria-label="Mehr" onclick={incr}>+</button>
</div>
<ul class="ing-list">
{#each scaled as ing, i (i)}
<li>
{#if ing.quantity !== null || ing.unit}
<span class="qty">
{formatQty(ing.quantity)}
{#if ing.unit}
{' '}{ing.unit}
{/if}
</span>
{/if}
<span class="name">
{ing.name}
{#if ing.note}<span class="note"> ({ing.note})</span>{/if}
</span>
</li>
{/each}
</ul>
</section>
<section
class="steps"
role="tabpanel"
class:hidden-mobile={tab !== 'prep'}
>
<ol>
{#each recipe.steps as step (step.position)}
<li>{step.text}</li>
{/each}
</ol>
</section>
</div>
</article>
<style>
.recipe {
padding-bottom: 2rem;
}
.hdr {
margin: 0 -1rem 1rem;
}
.cover {
display: block;
width: 100%;
aspect-ratio: 16 / 10;
/* Nie mehr als 30% der Bildschirmhöhe — auf schmalen Screens würde das
Bild sonst alles Wichtige wegdrücken, auf breiten Desktops wäre es
unverhältnismäßig groß. */
max-height: 30vh;
object-fit: cover;
background: #eef3ef;
}
.hdr-body {
padding: 1rem 1rem 0.25rem;
}
h1 {
font-size: clamp(1.5rem, 5.5vw, 2rem);
line-height: 1.15;
margin: 0 0 0.4rem;
}
.desc {
color: #555;
margin: 0 0 0.5rem;
line-height: 1.4;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-bottom: 0.5rem;
}
.pill {
padding: 0.15rem 0.55rem;
background: #eaf4ed;
border-radius: 999px;
font-size: 0.8rem;
color: #2b6a3d;
}
.tag {
font-size: 0.8rem;
color: #888;
}
.times {
margin: 0 0 0.25rem;
color: #666;
font-size: 0.9rem;
}
.src {
margin: 0;
font-size: 0.85rem;
color: #888;
}
.actions {
margin: 0 0 1rem;
}
.tabs {
display: flex;
gap: 0;
border-bottom: 1px solid #e4eae7;
margin-bottom: 1rem;
position: sticky;
top: 57px;
background: #f8faf8;
z-index: 5;
}
.tab {
flex: 1;
background: none;
border: 0;
padding: 0.9rem 0;
font-size: 1rem;
cursor: pointer;
color: #666;
border-bottom: 3px solid transparent;
min-height: 48px;
}
.tab.active {
color: #2b6a3d;
border-bottom-color: #2b6a3d;
font-weight: 600;
}
.servings {
display: flex;
align-items: center;
gap: 1rem;
justify-content: center;
background: white;
border: 1px solid #e4eae7;
border-radius: 12px;
padding: 0.6rem 1rem;
margin-bottom: 1rem;
}
.srv-btn {
width: 44px;
height: 44px;
border: 1px solid #cfd9d1;
background: white;
border-radius: 10px;
font-size: 1.4rem;
cursor: pointer;
}
.srv-value {
display: flex;
flex-direction: column;
align-items: center;
min-width: 80px;
}
.srv-value strong {
font-size: 1.25rem;
}
.srv-value span {
color: #888;
font-size: 0.85rem;
}
.ing-list {
list-style: none;
padding: 0;
margin: 0;
}
.ing-list li {
display: flex;
gap: 0.75rem;
padding: 0.7rem 0.25rem;
border-bottom: 1px solid #edf1ee;
font-size: 1rem;
line-height: 1.4;
}
.qty {
min-width: 5rem;
font-weight: 600;
color: #2b6a3d;
}
.note {
color: #888;
font-size: 0.9em;
}
.steps ol {
padding-left: 0;
list-style: none;
counter-reset: step;
}
.steps li {
counter-increment: step;
position: relative;
padding: 0.8rem 0 0.8rem 3rem;
border-bottom: 1px solid #edf1ee;
line-height: 1.5;
}
.steps li::before {
content: counter(step);
position: absolute;
left: 0;
top: 0.8rem;
width: 2.1rem;
height: 2.1rem;
background: #2b6a3d;
color: white;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 700;
font-size: 0.95rem;
}
.panes {
display: block;
}
.hidden-mobile {
display: none;
}
/* Querformat-Tablets und Desktop: Zutaten + Zubereitung nebeneinander,
Tabs ausgeblendet. Zutaten sticky, damit sie beim Scrollen der
Zubereitung oben bleiben.
Schriftgrößen hier bewusst größer — das Rezept wird auf einem 10"-
Tablet beim Kochen aus ~50 cm Abstand gelesen. */
@media (min-width: 820px) {
.tabs {
display: none;
}
.panes {
display: grid;
grid-template-columns: minmax(260px, 1fr) 1.6fr;
gap: 2rem;
align-items: start;
}
.hidden-mobile {
display: block;
}
.ingredients {
position: sticky;
top: 1rem;
max-height: calc(100vh - 2rem);
overflow-y: auto;
}
.ing-list li {
font-size: 1.2rem;
line-height: 1.5;
padding: 0.85rem 0.25rem;
}
.qty {
min-width: 6rem;
}
.srv-value strong {
font-size: 1.5rem;
}
.srv-value span {
font-size: 1rem;
}
.steps li {
font-size: 1.2rem;
line-height: 1.55;
padding: 1rem 0 1rem 3.4rem;
}
.steps li::before {
width: 2.4rem;
height: 2.4rem;
font-size: 1.1rem;
top: 1rem;
}
}
</style>