diff --git a/frontend/src/lib/planner/RecipePicker.svelte b/frontend/src/lib/planner/RecipePicker.svelte
new file mode 100644
index 0000000..f629ee0
--- /dev/null
+++ b/frontend/src/lib/planner/RecipePicker.svelte
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+ Rezept wählen
+
+
+ {dateLabel}
+
+
+
+
+
+
+
+
+
+ {#if suggestions.length > 0}
+
+ Empfohlen · Beste Abwechslung
+
+
+ {#each suggestions as suggestion (suggestion.recipe.id)}
+ {@const delta = suggestion.simulatedScore - currentVarietyScore}
+ {@const meta = recipeMetadata(suggestion.recipe)}
+
+
+
+ {suggestion.recipe.name}
+
+ {#if meta}
+
+ {meta}
+
+ {/if}
+ {#if delta > 0}
+
+ ↑ +{delta.toFixed(0)} Punkte
+
+ {:else}
+
+ ⚠ Variationskonflikt
+
+ {/if}
+
+
+
+ {/each}
+ {/if}
+
+
+
+ Alle Rezepte
+
+
+ {#if filteredRecipes.length === 0}
+
+ Keine Treffer
+
+ {:else}
+ {#each filteredRecipes as recipe (recipe.id)}
+ {@const meta = recipeMetadata(recipe)}
+
+
+
+ {recipe.name}
+
+ {#if meta}
+
+ {meta}
+
+ {/if}
+
+
+
+ {/each}
+ {/if}
+
diff --git a/frontend/src/lib/planner/RecipePicker.test.ts b/frontend/src/lib/planner/RecipePicker.test.ts
new file mode 100644
index 0000000..4ef320b
--- /dev/null
+++ b/frontend/src/lib/planner/RecipePicker.test.ts
@@ -0,0 +1,101 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen } from '@testing-library/svelte';
+import userEvent from '@testing-library/user-event';
+import RecipePicker from './RecipePicker.svelte';
+
+const suggestions = [
+ { recipe: { id: 's1', name: 'Lachsfilet', effort: 'easy', cookTimeMin: 25 }, simulatedScore: 9.5 },
+ { recipe: { id: 's2', name: 'Hähnchen-Curry', effort: 'easy', cookTimeMin: 35 }, simulatedScore: 6.0 }
+];
+
+const allRecipes = [
+ { id: 'r1', name: 'Beef Bourguignon', effort: 'hard', cookTimeMin: 150 },
+ { id: 'r2', name: 'Spaghetti Carbonara', effort: 'easy', cookTimeMin: 20 },
+ { id: 'r3', name: 'Tomatensuppe', effort: 'easy', cookTimeMin: 30 }
+];
+
+const baseProps = {
+ planId: 'plan-1',
+ date: '2026-04-05',
+ dateLabel: 'Samstag, 5. April',
+ currentVarietyScore: 7.5,
+ suggestions,
+ allRecipes,
+ onpick: vi.fn()
+};
+
+describe('RecipePicker', () => {
+ it('shows date label in header', () => {
+ render(RecipePicker, { props: baseProps });
+ expect(screen.getByText('Samstag, 5. April')).toBeTruthy();
+ });
+
+ it('shows Empfohlen section', () => {
+ render(RecipePicker, { props: baseProps });
+ expect(screen.getByText(/Empfohlen/i)).toBeTruthy();
+ });
+
+ it('shows all suggestion recipe names', () => {
+ render(RecipePicker, { props: baseProps });
+ expect(screen.getByText('Lachsfilet')).toBeTruthy();
+ expect(screen.getByText('Hähnchen-Curry')).toBeTruthy();
+ });
+
+ it('shows green badge for suggestions with positive delta', () => {
+ render(RecipePicker, { props: baseProps });
+ // Lachsfilet: simulatedScore 9.5 - currentVarietyScore 7.5 = +2 → green badge
+ const badge = screen.getByTestId('badge-s1');
+ expect(badge.getAttribute('data-type')).toBe('good');
+ });
+
+ it('shows yellow badge for suggestions with zero or negative delta', () => {
+ render(RecipePicker, { props: baseProps });
+ // Hähnchen-Curry: 6.0 - 7.5 = -1.5 → yellow badge
+ const badge = screen.getByTestId('badge-s2');
+ expect(badge.getAttribute('data-type')).toBe('warning');
+ });
+
+ it('shows Alle Rezepte section', () => {
+ render(RecipePicker, { props: baseProps });
+ expect(screen.getByText(/Alle Rezepte/i)).toBeTruthy();
+ });
+
+ it('shows all recipe names in Alle Rezepte', () => {
+ render(RecipePicker, { props: baseProps });
+ expect(screen.getByText('Beef Bourguignon')).toBeTruthy();
+ expect(screen.getByText('Spaghetti Carbonara')).toBeTruthy();
+ expect(screen.getByText('Tomatensuppe')).toBeTruthy();
+ });
+
+ it('filters recipes by search query', async () => {
+ render(RecipePicker, { props: baseProps });
+ const input = screen.getByRole('searchbox');
+ await userEvent.type(input, 'Spaghetti');
+ expect(screen.queryByText('Beef Bourguignon')).toBeNull();
+ expect(screen.getByText('Spaghetti Carbonara')).toBeTruthy();
+ });
+
+ it('calls onpick with recipeId and name when Wählen clicked for suggestion', async () => {
+ const onpick = vi.fn();
+ render(RecipePicker, { props: { ...baseProps, onpick } });
+ const buttons = screen.getAllByRole('button', { name: /Wählen/i });
+ await userEvent.click(buttons[0]);
+ expect(onpick).toHaveBeenCalledWith('s1', 'Lachsfilet');
+ });
+
+ it('calls onpick when Wählen clicked for all-recipes item', async () => {
+ const onpick = vi.fn();
+ render(RecipePicker, { props: { ...baseProps, onpick } });
+ const buttons = screen.getAllByRole('button', { name: /Wählen/i });
+ // First 2 are suggestions, rest are allRecipes
+ await userEvent.click(buttons[2]);
+ expect(onpick).toHaveBeenCalledWith('r1', 'Beef Bourguignon');
+ });
+
+ it('shows empty state when search has no results', async () => {
+ render(RecipePicker, { props: baseProps });
+ const input = screen.getByRole('searchbox');
+ await userEvent.type(input, 'xyznotfound');
+ expect(screen.getByText(/Keine Treffer/i)).toBeTruthy();
+ });
+});
diff --git a/frontend/src/routes/(app)/planner/+server.ts b/frontend/src/routes/(app)/planner/+server.ts
new file mode 100644
index 0000000..910efc5
--- /dev/null
+++ b/frontend/src/routes/(app)/planner/+server.ts
@@ -0,0 +1,24 @@
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { apiClient } from '$lib/server/api';
+
+// GET /planner?planId=&date= — returns suggestions JSON for C4 recipe picker
+export const GET: RequestHandler = async ({ fetch, url }) => {
+ const planId = url.searchParams.get('planId');
+ const date = url.searchParams.get('date');
+
+ if (!planId || !date) {
+ return json({ suggestions: [] });
+ }
+
+ const api = apiClient(fetch);
+ const { data } = await api.GET('/v1/week-plans/{id}/suggestions', {
+ params: { path: { id: planId }, query: { slotDate: date } }
+ });
+
+ const suggestions = (data?.suggestions ?? []).sort(
+ (a: any, b: any) => (b.simulatedScore ?? 0) - (a.simulatedScore ?? 0)
+ );
+
+ return json({ suggestions });
+};