feat(staples): add CategorySection component with eyebrow heading and chip row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 20:07:51 +02:00
parent 7bdc049461
commit 376dc03646
2 changed files with 81 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import StapleChip from './StapleChip.svelte';
type Ingredient = { id: string; name: string; isStaple: boolean };
let { name, ingredients, onToggle }: {
name: string;
ingredients: Ingredient[];
onToggle: (id: string, value: boolean) => void;
} = $props();
</script>
<div>
<p class="text-[10px] font-medium tracking-[.08em] uppercase text-[var(--color-text-muted)] mb-[8px]">
{name}
</p>
<div class="flex flex-wrap gap-[6px]">
{#each ingredients as ingredient (ingredient.id)}
<StapleChip
name={ingredient.name}
selected={ingredient.isStaple}
onToggle={(value) => onToggle(ingredient.id, value)}
/>
{/each}
</div>
</div>

View File

@@ -0,0 +1,55 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import CategorySection from './CategorySection.svelte';
const mockIngredients = [
{ id: '1', name: 'Olivenöl', isStaple: true },
{ id: '2', name: 'Butter', isStaple: false },
{ id: '3', name: 'Kokosöl', isStaple: false }
];
describe('CategorySection', () => {
it('renders the category name as a heading', () => {
render(CategorySection, {
props: { name: 'Öle & Fette', ingredients: mockIngredients, onToggle: vi.fn() }
});
expect(screen.getByText('Öle & Fette')).toBeInTheDocument();
});
it('renders a chip for each ingredient', () => {
render(CategorySection, {
props: { name: 'Öle & Fette', ingredients: mockIngredients, onToggle: vi.fn() }
});
expect(screen.getByRole('button', { name: 'Olivenöl' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Butter' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Kokosöl' })).toBeInTheDocument();
});
it('reflects isStaple state on each chip', () => {
render(CategorySection, {
props: { name: 'Öle & Fette', ingredients: mockIngredients, onToggle: vi.fn() }
});
expect(screen.getByRole('button', { name: 'Olivenöl' })).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByRole('button', { name: 'Butter' })).toHaveAttribute('aria-pressed', 'false');
});
it('calls onToggle with ingredient id and new value when chip is clicked', async () => {
const { userEvent } = await import('@testing-library/user-event');
const user = userEvent.setup();
const onToggle = vi.fn();
render(CategorySection, {
props: { name: 'Öle & Fette', ingredients: mockIngredients, onToggle }
});
await user.click(screen.getByRole('button', { name: 'Butter' }));
expect(onToggle).toHaveBeenCalledWith('2', true);
});
it('renders an empty category without crashing', () => {
render(CategorySection, {
props: { name: 'Leer', ingredients: [], onToggle: vi.fn() }
});
expect(screen.getByText('Leer')).toBeInTheDocument();
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
});