diff --git a/frontend/src/lib/planner/DesktopDayTile.svelte b/frontend/src/lib/planner/DesktopDayTile.svelte
new file mode 100644
index 0000000..2476f96
--- /dev/null
+++ b/frontend/src/lib/planner/DesktopDayTile.svelte
@@ -0,0 +1,201 @@
+
+
+{#if slot.recipe}
+
+
+
+
+ {slot.recipe.name}
+
+
+
+
+
+
+ {#if slot.recipe.cookTimeMin}
+
{slot.recipe.cookTimeMin} min
+ {/if}
+
+ {#if slot.recipe.effort}
+
{slot.recipe.effort}
+ {/if}
+
+
+
+ {#if isPlanner}
+
+ {/if}
+
+ {#if isPlanner && slot.id}
+
+ {/if}
+
+
+
+{:else}
+
+{/if}
+
+
diff --git a/frontend/src/lib/planner/DesktopDayTile.test.ts b/frontend/src/lib/planner/DesktopDayTile.test.ts
new file mode 100644
index 0000000..89603a2
--- /dev/null
+++ b/frontend/src/lib/planner/DesktopDayTile.test.ts
@@ -0,0 +1,172 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen } from '@testing-library/svelte';
+import { userEvent } from '@testing-library/user-event';
+import DesktopDayTile from './DesktopDayTile.svelte';
+
+const filledSlot = {
+ id: 's1',
+ slotDate: '2026-04-14',
+ recipe: {
+ id: 'r1',
+ name: 'Pasta Bolognese',
+ cookTimeMin: 45,
+ effort: 'mittel',
+ heroImageUrl: null,
+ tags: [{ id: 't1', name: 'Rind', tagType: 'protein' }]
+ }
+};
+
+const emptySlot = { id: null, slotDate: '2026-04-14', recipe: null };
+
+describe('DesktopDayTile — filled slot', () => {
+ describe('front face', () => {
+ it('renders recipe name on front face', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: null, isPlanner: true, slotMap: {}, suggestions: [] } });
+ expect(screen.getByText('Pasta Bolognese')).toBeTruthy();
+ });
+
+ it('has data-testid="day-meal-card" on the scene element', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: null, isPlanner: true, slotMap: {}, suggestions: [] } });
+ expect(screen.getByTestId('day-meal-card')).toBeTruthy();
+ });
+
+ it('applies today ring when isToday', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: true, activeSlotId: null, isPlanner: true, slotMap: {}, suggestions: [] } });
+ const scene = screen.getByTestId('day-meal-card');
+ expect(scene.getAttribute('data-today')).toBe('true');
+ });
+
+ it('applies selected ring when activeSlotId matches slot id', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: 's1', isPlanner: true, slotMap: {}, suggestions: [] } });
+ const scene = screen.getByTestId('day-meal-card');
+ expect(scene.getAttribute('data-flipped')).toBe('true');
+ });
+
+ it('dims tile when another slot is active', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: 'other-slot', isPlanner: true, slotMap: {}, suggestions: [] } });
+ const scene = screen.getByTestId('day-meal-card');
+ expect(scene.getAttribute('data-dimmed')).toBe('true');
+ });
+
+ it('is not dimmed when no slot is active', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: null, isPlanner: true, slotMap: {}, suggestions: [] } });
+ const scene = screen.getByTestId('day-meal-card');
+ expect(scene.getAttribute('data-dimmed')).toBe('false');
+ });
+ });
+
+ describe('flip interaction', () => {
+ it('calls onflip with slot id when scene is clicked', async () => {
+ const onflip = vi.fn();
+ const user = userEvent.setup();
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: null, isPlanner: true, slotMap: {}, suggestions: [], onflip } });
+ await user.click(screen.getByTestId('day-meal-card'));
+ expect(onflip).toHaveBeenCalledWith('s1');
+ });
+
+ it('calls onflip when Enter key is pressed on scene', async () => {
+ const onflip = vi.fn();
+ const user = userEvent.setup();
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: null, isPlanner: true, slotMap: {}, suggestions: [], onflip } });
+ screen.getByTestId('day-meal-card').focus();
+ await user.keyboard('{Enter}');
+ expect(onflip).toHaveBeenCalledWith('s1');
+ });
+
+ it('calls onflip when Space key is pressed on scene', async () => {
+ const onflip = vi.fn();
+ const user = userEvent.setup();
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: null, isPlanner: true, slotMap: {}, suggestions: [], onflip } });
+ screen.getByTestId('day-meal-card').focus();
+ await user.keyboard(' ');
+ expect(onflip).toHaveBeenCalledWith('s1');
+ });
+ });
+
+ describe('back face (flipped state)', () => {
+ it('shows recipe name on back face when flipped', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: 's1', isPlanner: true, slotMap: {}, suggestions: [] } });
+ // Back face should also show recipe name
+ const names = screen.getAllByText('Pasta Bolognese');
+ expect(names.length).toBeGreaterThanOrEqual(1);
+ });
+
+ it('shows Koch-Modus link on back face when flipped', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: 's1', isPlanner: true, slotMap: {}, suggestions: [] } });
+ expect(screen.getByRole('link', { name: /Koch-Modus/i })).toBeTruthy();
+ });
+
+ it('shows Rezept ansehen link on back face when flipped', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: 's1', isPlanner: true, slotMap: {}, suggestions: [] } });
+ expect(screen.getByRole('link', { name: /Rezept ansehen/i })).toBeTruthy();
+ });
+
+ it('shows close button on back face', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: 's1', isPlanner: true, slotMap: {}, suggestions: [] } });
+ expect(screen.getByRole('button', { name: /Schließen/i })).toBeTruthy();
+ });
+
+ it('calls onclose when close button clicked', async () => {
+ const onclose = vi.fn();
+ const user = userEvent.setup();
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: 's1', isPlanner: true, slotMap: {}, suggestions: [], onclose } });
+ await user.click(screen.getByRole('button', { name: /Schließen/i }));
+ expect(onclose).toHaveBeenCalledOnce();
+ });
+
+ it('shows Gericht tauschen button for planner on back face', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: 's1', isPlanner: true, slotMap: {}, suggestions: [] } });
+ expect(screen.getByRole('button', { name: /Gericht tauschen/i })).toBeTruthy();
+ });
+
+ it('hides Gericht tauschen for non-planner', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: 's1', isPlanner: false, slotMap: {}, suggestions: [] } });
+ expect(screen.queryByRole('button', { name: /Gericht tauschen/i })).toBeNull();
+ });
+
+ it('calls onswap when Gericht tauschen clicked', async () => {
+ const onswap = vi.fn();
+ const user = userEvent.setup();
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: 's1', isPlanner: true, slotMap: {}, suggestions: [], onswap } });
+ await user.click(screen.getByRole('button', { name: /Gericht tauschen/i }));
+ expect(onswap).toHaveBeenCalledOnce();
+ });
+
+ it('shows Entfernen button for planner when slot has id', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: 's1', isPlanner: true, slotMap: {}, suggestions: [] } });
+ expect(screen.getByRole('button', { name: /Entfernen/i })).toBeTruthy();
+ });
+
+ it('calls onremove when Entfernen clicked', async () => {
+ const onremove = vi.fn();
+ const user = userEvent.setup();
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: 's1', isPlanner: true, slotMap: {}, suggestions: [], onremove } });
+ await user.click(screen.getByRole('button', { name: /Entfernen/i }));
+ expect(onremove).toHaveBeenCalledOnce();
+ });
+
+ it('aria-expanded is true when flipped', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: 's1', isPlanner: true, slotMap: {}, suggestions: [] } });
+ const scene = screen.getByTestId('day-meal-card');
+ expect(scene.getAttribute('aria-expanded')).toBe('true');
+ });
+
+ it('aria-expanded is false when not flipped', () => {
+ render(DesktopDayTile, { props: { slot: filledSlot, isToday: false, activeSlotId: null, isPlanner: true, slotMap: {}, suggestions: [] } });
+ const scene = screen.getByTestId('day-meal-card');
+ expect(scene.getAttribute('aria-expanded')).toBe('false');
+ });
+ });
+});
+
+describe('DesktopDayTile — empty slot', () => {
+ it('renders EmptyDayTile (shows Gericht wählen) for empty slot', () => {
+ render(DesktopDayTile, { props: { slot: emptySlot, isToday: false, activeSlotId: null, isPlanner: true, slotMap: {}, suggestions: [] } });
+ expect(screen.getByRole('button', { name: /Gericht wählen/i })).toBeTruthy();
+ });
+
+ it('does not render Koch-Modus for empty slot', () => {
+ render(DesktopDayTile, { props: { slot: emptySlot, isToday: false, activeSlotId: null, isPlanner: true, slotMap: {}, suggestions: [] } });
+ expect(screen.queryByRole('link', { name: /Koch-Modus/i })).toBeNull();
+ });
+});