feat(onboarding): add ProgressSidebar component with 3-step active/completed/future states

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 19:08:38 +02:00
parent 09333ccc0a
commit b9ef06fd73
2 changed files with 127 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
<script lang="ts">
const { currentStep }: { currentStep: number } = $props();
const steps = [
{
number: 1,
label: 'Haushalt benennen',
subtitle: 'Deiner Familie einen Namen geben'
},
{
number: 2,
label: 'Vorräte einrichten',
subtitle: 'Was ihr immer zu Hause habt'
},
{
number: 3,
label: 'Mitglieder einladen',
subtitle: 'Haushalt teilen'
}
];
function circleClass(n: number): string {
if (n === currentStep) return 'bg-[var(--green)] text-white';
if (n < currentStep) return 'bg-[var(--green-tint)] text-[var(--green-dark)]';
return 'bg-[var(--color-subtle)] text-[var(--color-text-muted)]';
}
function labelClass(n: number): string {
if (n === currentStep) return 'text-[13px] font-medium text-[var(--color-text)]';
return 'text-[13px] text-[var(--color-text-muted)]';
}
</script>
<nav>
<!-- Logo row -->
<div class="flex items-center gap-[8px] mb-[40px]">
<div
class="w-[28px] h-[28px] rounded-[6px] bg-[var(--green)] flex items-center justify-center text-[14px]"
>
🥗
</div>
<span class="font-[var(--font-display)] text-[16px] font-medium">Mealplan</span>
</div>
<!-- Steps -->
<div class="flex flex-col gap-[24px]">
{#each steps as step (step.number)}
<div
class="flex gap-[12px] items-start"
data-testid="step-{step.number}"
data-state={step.number < currentStep
? 'completed'
: step.number === currentStep
? 'current'
: 'future'}
aria-current={step.number === currentStep ? 'step' : undefined}
>
<div
class="w-[28px] h-[28px] rounded-full flex items-center justify-center text-[12px] font-medium flex-shrink-0 {circleClass(step.number)}"
aria-label="Schritt {step.number}"
>
{step.number < currentStep ? '✓' : step.number}
</div>
<div>
<div class={labelClass(step.number)}>{step.label}</div>
<div class="text-[11px] text-[var(--color-text-muted)] mt-[2px]">{step.subtitle}</div>
</div>
</div>
{/each}
</div>
</nav>

View File

@@ -0,0 +1,56 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import ProgressSidebar from './ProgressSidebar.svelte';
describe('ProgressSidebar', () => {
it('renders the app logo and name', () => {
render(ProgressSidebar, { props: { currentStep: 1 } });
expect(screen.getByText('Mealplan')).toBeInTheDocument();
});
it('renders all 3 step labels', () => {
render(ProgressSidebar, { props: { currentStep: 1 } });
expect(screen.getByText('Haushalt benennen')).toBeInTheDocument();
expect(screen.getByText('Vorräte einrichten')).toBeInTheDocument();
expect(screen.getByText('Mitglieder einladen')).toBeInTheDocument();
});
it('step 1 active: renders green circle for step 1', () => {
render(ProgressSidebar, { props: { currentStep: 1 } });
const step1 = screen.getByTestId('step-1');
expect(step1).toHaveAttribute('aria-current', 'step');
});
it('step 1 active: steps 2 and 3 are not current', () => {
render(ProgressSidebar, { props: { currentStep: 1 } });
expect(screen.getByTestId('step-2')).not.toHaveAttribute('aria-current');
expect(screen.getByTestId('step-3')).not.toHaveAttribute('aria-current');
});
it('step 2 active: step 1 is completed (checkmark), step 2 is current, step 3 is future', () => {
render(ProgressSidebar, { props: { currentStep: 2 } });
expect(screen.getByTestId('step-1')).toHaveAttribute('data-state', 'completed');
expect(screen.getByTestId('step-2')).toHaveAttribute('aria-current', 'step');
expect(screen.getByTestId('step-3')).not.toHaveAttribute('aria-current');
});
it('step 1 completed has accessible label', () => {
render(ProgressSidebar, { props: { currentStep: 2 } });
const step1 = screen.getByTestId('step-1');
expect(step1).toHaveAttribute('data-state', 'completed');
expect(screen.getByLabelText(/schritt 1/i)).toBeInTheDocument();
});
it('each step has an accessible aria-label', () => {
render(ProgressSidebar, { props: { currentStep: 1 } });
expect(screen.getByLabelText(/schritt 1/i)).toBeInTheDocument();
expect(screen.getByLabelText(/schritt 2/i)).toBeInTheDocument();
expect(screen.getByLabelText(/schritt 3/i)).toBeInTheDocument();
});
it('future steps do not have aria-current', () => {
render(ProgressSidebar, { props: { currentStep: 1 } });
expect(screen.getByTestId('step-2')).not.toHaveAttribute('aria-current');
expect(screen.getByTestId('step-3')).not.toHaveAttribute('aria-current');
});
});