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:
@@ -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>
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user