diff --git a/frontend/src/lib/onboarding/StaplesManager.svelte b/frontend/src/lib/onboarding/StaplesManager.svelte
new file mode 100644
index 0000000..3e133ad
--- /dev/null
+++ b/frontend/src/lib/onboarding/StaplesManager.svelte
@@ -0,0 +1,80 @@
+
+
+
+ {#if errorMessage}
+
{errorMessage}
+ {/if}
+
+
+ {#each categories as category (category.id)}
+ ({
+ ...ing,
+ isStaple: stapleState[ing.id] ?? ing.isStaple
+ }))}
+ onToggle={handleToggle}
+ />
+ {/each}
+
+
diff --git a/frontend/src/lib/onboarding/StaplesManager.test.ts b/frontend/src/lib/onboarding/StaplesManager.test.ts
new file mode 100644
index 0000000..2dfee3a
--- /dev/null
+++ b/frontend/src/lib/onboarding/StaplesManager.test.ts
@@ -0,0 +1,102 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { render, screen } from '@testing-library/svelte';
+import { userEvent } from '@testing-library/user-event';
+import StaplesManager from './StaplesManager.svelte';
+
+const mockCategories = [
+ {
+ id: 'cat-1',
+ name: 'Öle & Fette',
+ ingredients: [
+ { id: 'ing-1', name: 'Olivenöl', isStaple: true },
+ { id: 'ing-2', name: 'Butter', isStaple: false }
+ ]
+ },
+ {
+ id: 'cat-2',
+ name: 'Gewürze',
+ ingredients: [
+ { id: 'ing-3', name: 'Salz', isStaple: true },
+ { id: 'ing-4', name: 'Pfeffer', isStaple: true }
+ ]
+ }
+];
+
+describe('StaplesManager', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }));
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ vi.unstubAllGlobals();
+ });
+
+ it('renders all categories', () => {
+ render(StaplesManager, { props: { categories: mockCategories, context: 'onboarding' } });
+ expect(screen.getByText('Öle & Fette')).toBeInTheDocument();
+ expect(screen.getByText('Gewürze')).toBeInTheDocument();
+ });
+
+ it('renders all chips with correct initial aria-pressed state', () => {
+ render(StaplesManager, { props: { categories: mockCategories, context: 'onboarding' } });
+ expect(screen.getByRole('button', { name: 'Olivenöl' })).toHaveAttribute('aria-pressed', 'true');
+ expect(screen.getByRole('button', { name: 'Butter' })).toHaveAttribute('aria-pressed', 'false');
+ expect(screen.getByRole('button', { name: 'Salz' })).toHaveAttribute('aria-pressed', 'true');
+ });
+
+ it('clicking a chip immediately updates aria-pressed (optimistic)', async () => {
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+ render(StaplesManager, { props: { categories: mockCategories, context: 'onboarding' } });
+
+ const butter = screen.getByRole('button', { name: 'Butter' });
+ expect(butter).toHaveAttribute('aria-pressed', 'false');
+ await user.click(butter);
+ expect(butter).toHaveAttribute('aria-pressed', 'true');
+ });
+
+ it('rapid clicks on same chip result in exactly one fetch call after debounce', async () => {
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+ render(StaplesManager, { props: { categories: mockCategories, context: 'onboarding' } });
+
+ const butter = screen.getByRole('button', { name: 'Butter' });
+ await user.click(butter);
+ await user.click(butter);
+ await user.click(butter);
+
+ expect(fetch).not.toHaveBeenCalled();
+ vi.advanceTimersByTime(300);
+ await vi.runAllTimersAsync();
+
+ expect(fetch).toHaveBeenCalledTimes(1);
+ });
+
+ it('reverts chip and shows error when PATCH fails', async () => {
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }));
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+ render(StaplesManager, { props: { categories: mockCategories, context: 'onboarding' } });
+
+ const butter = screen.getByRole('button', { name: 'Butter' });
+ await user.click(butter);
+ expect(butter).toHaveAttribute('aria-pressed', 'true');
+
+ vi.advanceTimersByTime(300);
+ await vi.runAllTimersAsync();
+
+ expect(butter).toHaveAttribute('aria-pressed', 'false');
+ expect(screen.getByText(/konnte nicht gespeichert werden/i)).toBeInTheDocument();
+ });
+
+ it('uses 2-column grid class in onboarding context', () => {
+ render(StaplesManager, { props: { categories: mockCategories, context: 'onboarding' } });
+ const grid = screen.getByTestId('category-grid');
+ expect(grid.className).toContain('md:grid-cols-2');
+ });
+
+ it('uses 3-column grid class in settings context', () => {
+ render(StaplesManager, { props: { categories: mockCategories, context: 'settings' } });
+ const grid = screen.getByTestId('category-grid');
+ expect(grid.className).toContain('md:grid-cols-3');
+ });
+});