feat(planner): desktop redesign — flip tiles, full-width grid, no right panel #54
77
frontend/src/lib/planner/RecipePickerDrawer.svelte
Normal file
77
frontend/src/lib/planner/RecipePickerDrawer.svelte
Normal file
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import type { Recipe, Suggestion } from '$lib/planner/types';
|
||||
import RecipePicker from './RecipePicker.svelte';
|
||||
|
||||
let {
|
||||
open,
|
||||
slotDate,
|
||||
planId,
|
||||
suggestions,
|
||||
allRecipes,
|
||||
isLoading,
|
||||
onpick,
|
||||
onclose,
|
||||
excludeRecipeId,
|
||||
replacingRecipe
|
||||
}: {
|
||||
open: boolean;
|
||||
slotDate: string;
|
||||
planId: string;
|
||||
suggestions: Suggestion[];
|
||||
allRecipes: Recipe[];
|
||||
isLoading: boolean;
|
||||
onpick: (recipeId: string, recipeName: string) => void;
|
||||
onclose: () => void;
|
||||
excludeRecipeId?: string;
|
||||
replacingRecipe?: { name: string; meta?: string };
|
||||
} = $props();
|
||||
|
||||
let drawerTransform = $derived(open ? 'translateX(0)' : 'translateX(100%)');
|
||||
let backdropVisibility = $derived(open ? 'visible' : 'hidden');
|
||||
let backdropOpacity = $derived(open ? '1' : '0');
|
||||
</script>
|
||||
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
data-testid="drawer-backdrop"
|
||||
aria-hidden="true"
|
||||
onclick={onclose}
|
||||
style="position: fixed; inset: 0; background: rgba(0,0,0,0.3); z-index: 40; visibility: {backdropVisibility}; opacity: {backdropOpacity}; transition: opacity 0.2s, visibility 0.2s;"
|
||||
></div>
|
||||
|
||||
<!-- Drawer panel -->
|
||||
<div
|
||||
data-testid="recipe-picker-drawer"
|
||||
aria-hidden={!open}
|
||||
style="position: fixed; right: 0; top: 0; height: 100%; width: min(480px, 90vw); background: var(--color-page); border-left: 1px solid var(--color-border); z-index: 50; transform: {drawerTransform}; transition: transform 0.25s ease; display: flex; flex-direction: column;"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--color-border); flex-shrink: 0;">
|
||||
<p style="margin: 0; font-family: var(--font-display); font-size: 15px; font-weight: 500; color: var(--color-text);">
|
||||
Rezept wählen
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Schließen"
|
||||
onclick={onclose}
|
||||
style="background: none; border: none; cursor: pointer; font-size: 20px; line-height: 1; color: var(--color-text-muted); padding: 4px 8px;"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- RecipePicker content -->
|
||||
<div style="overflow-y: auto; flex: 1;">
|
||||
<RecipePicker
|
||||
{planId}
|
||||
date={slotDate}
|
||||
dateLabel={slotDate}
|
||||
{suggestions}
|
||||
{allRecipes}
|
||||
{isLoading}
|
||||
{onpick}
|
||||
{excludeRecipeId}
|
||||
{replacingRecipe}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
80
frontend/src/lib/planner/RecipePickerDrawer.test.ts
Normal file
80
frontend/src/lib/planner/RecipePickerDrawer.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
import RecipePickerDrawer from './RecipePickerDrawer.svelte';
|
||||
|
||||
const baseProps = {
|
||||
open: true,
|
||||
slotDate: '2026-04-14',
|
||||
planId: 'plan-1',
|
||||
suggestions: [],
|
||||
allRecipes: [
|
||||
{ id: 'r1', name: 'Pasta Bolognese', cookTimeMin: 45, effort: 'mittel' },
|
||||
{ id: 'r2', name: 'Lachs', cookTimeMin: 20, effort: 'einfach' }
|
||||
],
|
||||
isLoading: false,
|
||||
onpick: vi.fn(),
|
||||
onclose: vi.fn()
|
||||
};
|
||||
|
||||
describe('RecipePickerDrawer', () => {
|
||||
describe('visibility', () => {
|
||||
it('renders drawer content when open=true', () => {
|
||||
render(RecipePickerDrawer, { props: baseProps });
|
||||
expect(screen.getByTestId('recipe-picker-drawer')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('drawer is not visible when open=false', () => {
|
||||
render(RecipePickerDrawer, { props: { ...baseProps, open: false } });
|
||||
const drawer = screen.getByTestId('recipe-picker-drawer');
|
||||
// Drawer exists in DOM but should be off-screen / aria-hidden
|
||||
expect(drawer.getAttribute('aria-hidden')).toBe('true');
|
||||
});
|
||||
|
||||
it('renders recipe list inside drawer', () => {
|
||||
render(RecipePickerDrawer, { props: baseProps });
|
||||
expect(screen.getByText('Pasta Bolognese')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('backdrop', () => {
|
||||
it('renders backdrop when open', () => {
|
||||
render(RecipePickerDrawer, { props: baseProps });
|
||||
expect(screen.getByTestId('drawer-backdrop')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('calls onclose when backdrop is clicked', async () => {
|
||||
const onclose = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(RecipePickerDrawer, { props: { ...baseProps, onclose } });
|
||||
await user.click(screen.getByTestId('drawer-backdrop'));
|
||||
expect(onclose).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe('close button', () => {
|
||||
it('renders a close button inside the drawer', () => {
|
||||
render(RecipePickerDrawer, { props: baseProps });
|
||||
expect(screen.getByRole('button', { name: /schließen|close/i })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('calls onclose when close button clicked', async () => {
|
||||
const onclose = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(RecipePickerDrawer, { props: { ...baseProps, onclose } });
|
||||
await user.click(screen.getByRole('button', { name: /schließen|close/i }));
|
||||
expect(onclose).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe('recipe picking', () => {
|
||||
it('calls onpick when a recipe is selected', async () => {
|
||||
const onpick = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(RecipePickerDrawer, { props: { ...baseProps, onpick } });
|
||||
const pickButtons = screen.getAllByRole('button', { name: /Wählen/i });
|
||||
await user.click(pickButtons[0]);
|
||||
expect(onpick).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user