feat(planner): DayMealCard gains onactionsheet prop for full-card mobile tap target

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 10:02:47 +02:00
parent 5b8d336d21
commit dac83c70ea
2 changed files with 95 additions and 39 deletions

View File

@@ -17,15 +17,19 @@
isToday = false, isToday = false,
isSelected = false, isSelected = false,
readonly = false, readonly = false,
onaddrecipe onaddrecipe,
onactionsheet
}: { }: {
slot: Slot; slot: Slot;
isToday?: boolean; isToday?: boolean;
isSelected?: boolean; isSelected?: boolean;
readonly?: boolean; readonly?: boolean;
onaddrecipe?: () => void; onaddrecipe?: () => void;
onactionsheet?: () => void;
} = $props(); } = $props();
let actionSheetMode = $derived(!!onactionsheet && !!slot.recipe);
let metadata = $derived( let metadata = $derived(
[ [
slot.recipe?.cookTimeMin != null ? `${slot.recipe.cookTimeMin} Min` : null, slot.recipe?.cookTimeMin != null ? `${slot.recipe.cookTimeMin} Min` : null,
@@ -44,49 +48,67 @@
); );
</script> </script>
<div {#if actionSheetMode}
data-testid="day-meal-card" <button
data-today={isToday} type="button"
data-selected={isSelected} data-testid="day-meal-card"
class="rounded-[var(--radius-lg)] border-2 p-4 transition-colors {borderClass}" data-today={isToday}
> data-selected={isSelected}
{#if slot.recipe} onclick={onactionsheet}
class="w-full text-left rounded-[var(--radius-lg)] border-2 p-4 transition-colors {borderClass}"
>
<h3 class="font-[var(--font-display)] text-[20px] font-[300] leading-tight text-[var(--color-text)]"> <h3 class="font-[var(--font-display)] text-[20px] font-[300] leading-tight text-[var(--color-text)]">
{slot.recipe.name} {slot.recipe!.name}
</h3> </h3>
{#if metadata} {#if metadata}
<p class="mt-1 font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)]">{metadata}</p> <p class="mt-1 font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)]">{metadata}</p>
{/if} {/if}
</button>
{:else}
<div
data-testid="day-meal-card"
data-today={isToday}
data-selected={isSelected}
class="rounded-[var(--radius-lg)] border-2 p-4 transition-colors {borderClass}"
>
{#if slot.recipe}
<h3 class="font-[var(--font-display)] text-[20px] font-[300] leading-tight text-[var(--color-text)]">
{slot.recipe.name}
</h3>
{#if metadata}
<p class="mt-1 font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)]">{metadata}</p>
{/if}
{#if !readonly} {#if !readonly}
<div class="mt-3 flex gap-2"> <div class="mt-3 flex gap-2">
<a <a
href="/recipes/{slot.recipe.id}/cook" href="/recipes/{slot.recipe.id}/cook"
class="rounded-[var(--radius-md)] bg-[var(--green-dark)] px-3 py-2 text-[13px] font-medium tracking-[0.04em] font-[var(--font-sans)] text-white" class="rounded-[var(--radius-md)] bg-[var(--green-dark)] px-3 py-2 text-[13px] font-medium tracking-[0.04em] font-[var(--font-sans)] text-white"
>
Jetzt kochen
</a>
{#if onaddrecipe}
<button
type="button"
onclick={onaddrecipe}
class="rounded-[var(--radius-md)] border border-[var(--color-border)] px-3 py-2 text-[13px] font-medium tracking-[0.04em] font-[var(--font-sans)] text-[var(--color-text)]"
> >
Tauschen Jetzt kochen
</button> </a>
{/if} {#if onaddrecipe}
</div> <button
type="button"
onclick={onaddrecipe}
class="rounded-[var(--radius-md)] border border-[var(--color-border)] px-3 py-2 text-[13px] font-medium tracking-[0.04em] font-[var(--font-sans)] text-[var(--color-text)]"
>
Tauschen
</button>
{/if}
</div>
{/if}
{:else}
<p class="font-[var(--font-sans)] text-[14px] text-[var(--color-text-muted)]">Kein Gericht geplant</p>
{#if !readonly && onaddrecipe}
<button
type="button"
onclick={onaddrecipe}
class="mt-2 inline-block rounded-[var(--radius-md)] border border-dashed border-[var(--color-border)] px-3 py-2 text-[13px] font-medium tracking-[0.04em] font-[var(--font-sans)] text-[var(--color-text-muted)]"
>
+ Gericht hinzufügen
</button>
{/if}
{/if} {/if}
{:else} </div>
<p class="font-[var(--font-sans)] text-[14px] text-[var(--color-text-muted)]">Kein Gericht geplant</p> {/if}
{#if !readonly && onaddrecipe}
<button
type="button"
onclick={onaddrecipe}
class="mt-2 inline-block rounded-[var(--radius-md)] border border-dashed border-[var(--color-border)] px-3 py-2 text-[13px] font-medium tracking-[0.04em] font-[var(--font-sans)] text-[var(--color-text-muted)]"
>
+ Gericht hinzufügen
</button>
{/if}
{/if}
</div>

View File

@@ -81,4 +81,38 @@ describe('DayMealCard', () => {
render(DayMealCard, { props: { slot: { id: 's2', slotDate: '2026-03-31', recipe: null }, isToday: false, readonly: false } }); render(DayMealCard, { props: { slot: { id: 's2', slotDate: '2026-03-31', recipe: null }, isToday: false, readonly: false } });
expect(screen.queryByRole('button', { name: /Gericht hinzufügen/i })).toBeNull(); expect(screen.queryByRole('button', { name: /Gericht hinzufügen/i })).toBeNull();
}); });
describe('onactionsheet prop (mobile full-card tap target)', () => {
it('card renders as a button when onactionsheet provided and recipe exists', () => {
render(DayMealCard, { props: { slot, onactionsheet: vi.fn() } });
const card = screen.getByRole('button', { name: /Pasta Bolognese/i });
expect(card).toBeTruthy();
});
it('clicking card calls onactionsheet', async () => {
const onactionsheet = vi.fn();
const user = userEvent.setup();
render(DayMealCard, { props: { slot, onactionsheet } });
await user.click(screen.getByRole('button', { name: /Pasta Bolognese/i }));
expect(onactionsheet).toHaveBeenCalledOnce();
});
it('inline Jetzt kochen and Tauschen buttons are hidden when onactionsheet provided', () => {
render(DayMealCard, { props: { slot, onactionsheet: vi.fn() } });
expect(screen.queryByRole('link', { name: /Jetzt kochen/i })).toBeNull();
expect(screen.queryByRole('button', { name: /Tauschen/i })).toBeNull();
});
it('falls back to normal rendering when onactionsheet not provided', () => {
render(DayMealCard, { props: { slot, readonly: false, onaddrecipe: vi.fn() } });
expect(screen.queryByRole('button', { name: /Pasta Bolognese/i })).toBeNull();
expect(screen.getByRole('link', { name: /Jetzt kochen/i })).toBeTruthy();
});
it('empty slot does not render card as button even when onactionsheet provided', () => {
const emptySlot = { id: 's2', slotDate: '2026-03-31', recipe: null };
render(DayMealCard, { props: { slot: emptySlot, onactionsheet: vi.fn(), onaddrecipe: vi.fn() } });
expect(screen.queryByRole('button', { name: /Pasta Bolognese/i })).toBeNull();
});
});
}); });