feat(planner): overhaul desktop layout — flip tiles, no right panel

Replaces 3-panel layout with 2-panel (sidebar + full-width grid):
- Remove persistent right panel and toolbar + Gericht hinzufügen button
- grid-cols-7 tiles use DesktopDayTile (CSS 3D card flip)
- RecipePickerDrawer slides in on tile CTA / Gericht tauschen
- Page-owned activeSlotId + drawerOpen/drawerSlotId state
- Single Escape handler: drawer > flip priority
- Extend server load to forward recipe tags from /v1/recipes API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 11:04:26 +02:00
parent 2cebf504f2
commit f97cf49bd0
6 changed files with 136 additions and 237 deletions

View File

@@ -35,6 +35,7 @@
isPlanner,
slotMap,
suggestions,
topSuggestion,
onflip,
onclose,
onswap,
@@ -47,6 +48,7 @@
isPlanner: boolean;
slotMap: Record<string, any>;
suggestions: Suggestion[];
topSuggestion?: Suggestion;
onflip?: (slotId: string) => void;
onclose?: () => void;
onswap?: () => void;
@@ -83,7 +85,7 @@
{#if slot.recipe}
<div
data-testid="day-meal-card"
data-testid="day-meal-card-{slot.slotDate ?? ''}"
role="button"
tabindex="0"
aria-label={slot.recipe?.name ?? 'Gericht'}
@@ -155,7 +157,7 @@
slotId={slot.id ?? ''}
{isPlanner}
{slotMap}
topSuggestion={undefined}
{topSuggestion}
{onaddrecipe}
/>
{/if}

View File

@@ -27,30 +27,30 @@ describe('DesktopDayTile — filled slot', () => {
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();
expect(screen.getByTestId("day-meal-card-2026-04-14")).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');
const scene = screen.getByTestId("day-meal-card-2026-04-14");
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');
const scene = screen.getByTestId("day-meal-card-2026-04-14");
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');
const scene = screen.getByTestId("day-meal-card-2026-04-14");
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');
const scene = screen.getByTestId("day-meal-card-2026-04-14");
expect(scene.getAttribute('data-dimmed')).toBe('false');
});
});
@@ -60,7 +60,7 @@ describe('DesktopDayTile — filled slot', () => {
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'));
await user.click(screen.getByTestId("day-meal-card-2026-04-14"));
expect(onflip).toHaveBeenCalledWith('s1');
});
@@ -68,7 +68,7 @@ describe('DesktopDayTile — filled slot', () => {
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();
screen.getByTestId("day-meal-card-2026-04-14").focus();
await user.keyboard('{Enter}');
expect(onflip).toHaveBeenCalledWith('s1');
});
@@ -77,7 +77,7 @@ describe('DesktopDayTile — filled slot', () => {
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();
screen.getByTestId("day-meal-card-2026-04-14").focus();
await user.keyboard(' ');
expect(onflip).toHaveBeenCalledWith('s1');
});
@@ -147,13 +147,13 @@ describe('DesktopDayTile — filled slot', () => {
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');
const scene = screen.getByTestId("day-meal-card-2026-04-14");
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');
const scene = screen.getByTestId("day-meal-card-2026-04-14");
expect(scene.getAttribute('aria-expanded')).toBe('false');
});
});

View File

@@ -60,18 +60,20 @@
</button>
</div>
<!-- RecipePicker content -->
<!-- RecipePicker content — only mount when open to avoid duplicate text in DOM -->
<div style="overflow-y: auto; flex: 1;">
<RecipePicker
{planId}
date={slotDate}
dateLabel={slotDate}
{suggestions}
{allRecipes}
{isLoading}
{onpick}
{excludeRecipeId}
{replacingRecipe}
/>
{#if open}
<RecipePicker
{planId}
date={slotDate}
dateLabel={slotDate}
{suggestions}
{allRecipes}
{isLoading}
{onpick}
{excludeRecipeId}
{replacingRecipe}
/>
{/if}
</div>
</div>

View File

@@ -1,8 +1,16 @@
export interface TagItem {
id?: string;
name?: string;
tagType?: string;
}
export interface Recipe {
id: string;
name: string;
effort?: string;
cookTimeMin?: number;
heroImageUrl?: string | null;
tags?: TagItem[];
}
export interface Suggestion {