From 781c4ffebbf5e8b265b1fc9b2e0d86de5ee1e0c5 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 22 Apr 2026 10:46:41 +0200 Subject: [PATCH 1/7] feat(nav): add BackButton component calling history.back() Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/components/BackButton.svelte | 25 ++++++++++++++++ .../lib/components/BackButton.svelte.spec.ts | 29 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 frontend/src/lib/components/BackButton.svelte create mode 100644 frontend/src/lib/components/BackButton.svelte.spec.ts diff --git a/frontend/src/lib/components/BackButton.svelte b/frontend/src/lib/components/BackButton.svelte new file mode 100644 index 00000000..4b2edbaf --- /dev/null +++ b/frontend/src/lib/components/BackButton.svelte @@ -0,0 +1,25 @@ + + + diff --git a/frontend/src/lib/components/BackButton.svelte.spec.ts b/frontend/src/lib/components/BackButton.svelte.spec.ts new file mode 100644 index 00000000..ce714912 --- /dev/null +++ b/frontend/src/lib/components/BackButton.svelte.spec.ts @@ -0,0 +1,29 @@ +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import BackButton from './BackButton.svelte'; + +afterEach(cleanup); + +describe('BackButton', () => { + it('renders a button with "Zurück" text', async () => { + render(BackButton); + await expect.element(page.getByRole('button', { name: /zurück/i })).toBeInTheDocument(); + }); + + it('calls history.back() when clicked', async () => { + const backSpy = vi.spyOn(history, 'back').mockImplementation(() => {}); + render(BackButton); + + await page.getByRole('button', { name: /zurück/i }).click(); + + expect(backSpy).toHaveBeenCalledOnce(); + backSpy.mockRestore(); + }); + + it('has min-h-[44px] for touch target compliance', async () => { + render(BackButton); + const btn = document.querySelector('button'); + expect(btn?.className).toContain('min-h-[44px]'); + }); +}); -- 2.49.1 From cc9c47254b05c0f4f9e541a7576a10f20cd69b21 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 22 Apr 2026 10:49:23 +0200 Subject: [PATCH 2/7] refactor(nav): replace static back-link hrefs with BackButton All 7 in-scope back navigation links converted to use history.back(). Admin panel mobile chevron converted inline (icon-only, different visual pattern). Cancel buttons left as static links. Co-Authored-By: Claude Sonnet 4.6 --- .../src/routes/admin/users/[id]/+page.svelte | 10 +++++---- .../routes/documents/[id]/edit/+page.svelte | 14 ++----------- frontend/src/routes/enrich/+page.svelte | 14 ++----------- frontend/src/routes/enrich/[id]/+page.svelte | 14 ++----------- frontend/src/routes/persons/[id]/+page.svelte | 14 ++----------- .../src/routes/persons/[id]/edit/+page.svelte | 21 ++----------------- frontend/src/routes/persons/new/+page.svelte | 21 ++----------------- 7 files changed, 18 insertions(+), 90 deletions(-) diff --git a/frontend/src/routes/admin/users/[id]/+page.svelte b/frontend/src/routes/admin/users/[id]/+page.svelte index 738bc2f6..a9a19568 100644 --- a/frontend/src/routes/admin/users/[id]/+page.svelte +++ b/frontend/src/routes/admin/users/[id]/+page.svelte @@ -23,9 +23,11 @@ $effect(() => {
- history.back()} + aria-label={m.btn_back()} + class="mr-3 inline-flex items-center gap-1 text-xs text-ink-3 outline-none hover:text-ink focus-visible:ring-2 focus-visible:ring-focus-ring md:hidden" > { > - +

{m.admin_user_edit_heading({ username: data.editUser.email })}

diff --git a/frontend/src/routes/documents/[id]/edit/+page.svelte b/frontend/src/routes/documents/[id]/edit/+page.svelte index 936407b4..abf00423 100644 --- a/frontend/src/routes/documents/[id]/edit/+page.svelte +++ b/frontend/src/routes/documents/[id]/edit/+page.svelte @@ -2,6 +2,7 @@ import { enhance } from '$app/forms'; import { m } from '$lib/paraglide/messages.js'; import { getConfirmService } from '$lib/services/confirm.svelte.js'; +import BackButton from '$lib/components/BackButton.svelte'; import DocumentEditLayout from '$lib/components/document/DocumentEditLayout.svelte'; let { data, form } = $props(); @@ -26,18 +27,7 @@ async function handleDelete() { {#snippet topbar()} - - - {m.btn_back_to_document()} - +

{doc.title || doc.originalFilename} diff --git a/frontend/src/routes/enrich/+page.svelte b/frontend/src/routes/enrich/+page.svelte index f23a53e0..b2130465 100644 --- a/frontend/src/routes/enrich/+page.svelte +++ b/frontend/src/routes/enrich/+page.svelte @@ -1,5 +1,6 @@

- - - - - {m.btn_back_to_overview()} - +

{m.persons_new_heading()}

-- 2.49.1 From ae3bc3f24610a3b5bf5134319bc92f16850ad774 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 22 Apr 2026 10:50:35 +0200 Subject: [PATCH 3/7] docs(claude): update back link pattern to use BackButton component Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3dd52a15..d0b67fe4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -311,13 +311,15 @@ Save bar pattern — use **sticky full-bleed** for long forms (edit document), *
``` -Back link pattern: +Back button pattern — use the shared `` component from `$lib/components/BackButton.svelte`: ```svelte - - - Zurück zur Übersicht - + + + ``` +The component calls `history.back()` so the user returns to wherever they came from. Label is always "Zurück" (no contextual suffix — destination is unknown). Touch target ≥ 44px and focus ring are built in. Do not use a static `` for back navigation. Subtle action link (e.g. "new document/person"): ```svelte -- 2.49.1 From 6c99c6a6709a3a22fa04f6d138c6e5b6bcd7d74a Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 22 Apr 2026 11:00:56 +0200 Subject: [PATCH 4/7] test(nav): add E2E tests for BackButton navigation and accessibility Co-Authored-By: Claude Sonnet 4.6 --- frontend/e2e/back-button.spec.ts | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 frontend/e2e/back-button.spec.ts diff --git a/frontend/e2e/back-button.spec.ts b/frontend/e2e/back-button.spec.ts new file mode 100644 index 00000000..4bfa0f66 --- /dev/null +++ b/frontend/e2e/back-button.spec.ts @@ -0,0 +1,58 @@ +import AxeBuilder from '@axe-core/playwright'; +import { test, expect } from '@playwright/test'; + +test.describe('BackButton — navigation', () => { + test('returns to previous page via history when clicked', async ({ page }) => { + // Navigate to persons list, then to a person detail + await page.goto('/persons'); + await page.waitForSelector('[data-hydrated]'); + + const firstPersonLink = page.locator('a[href^="/persons/"]').first(); + const personHref = await firstPersonLink.getAttribute('href'); + await firstPersonLink.click(); + await page.waitForURL(/\/persons\/.+/); + + // Now navigate to the edit page from the detail page + const editLink = page.locator('a[href$="/edit"]').first(); + await editLink.click(); + await page.waitForURL(/\/persons\/.+\/edit/); + + // Click the BackButton — should return to person detail, not /persons list + const backBtn = page.getByRole('button', { name: /zurück/i }); + await expect(backBtn).toBeVisible(); + await backBtn.click(); + + // Should return to the person detail URL (history.back()), not the static /persons + await expect(page).toHaveURL(new RegExp(personHref!.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + }); +}); + +test.describe('BackButton — accessibility', () => { + test('touch target is at least 44px tall on /persons/new', async ({ page }) => { + await page.goto('/persons/new'); + await page.waitForSelector('[data-hydrated]'); + + const backBtn = page.getByRole('button', { name: /zurück/i }); + await expect(backBtn).toBeVisible(); + + const box = await backBtn.boundingBox(); + expect(box).not.toBeNull(); + expect(box!.height).toBeGreaterThanOrEqual(44); + }); + + test('passes axe-core wcag2a/wcag2aa scan on /persons/new', async ({ page }) => { + await page.goto('/persons/new'); + await page.waitForSelector('[data-hydrated]'); + + const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze(); + + if (results.violations.length > 0) { + const summary = results.violations + .map((v) => `[${v.impact}] ${v.id}: ${v.description}`) + .join('\n'); + console.log(`\nAxe violations on /persons/new:\n${summary}`); + } + + expect(results.violations).toEqual([]); + }); +}); -- 2.49.1 From 367dcc66f251f5708302935830fcbe077f307d96 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 22 Apr 2026 11:16:49 +0200 Subject: [PATCH 5/7] refactor(nav): add class prop to BackButton, remove mb-4 from topbar contexts - BackButton now accepts a `class` prop (default 'mb-4') so callers can override spacing; resolves hardcoded margin in flex-row topbar snippets - documents/[id]/edit and enrich/[id] pass class="" to suppress the margin - Replace weak className unit test with class-prop behaviour tests - Add [data-hydrated] comment in E2E spec explaining what emits the attribute Co-Authored-By: Claude Sonnet 4.6 --- frontend/e2e/back-button.spec.ts | 3 +++ frontend/src/lib/components/BackButton.svelte | 3 ++- frontend/src/lib/components/BackButton.svelte.spec.ts | 11 +++++++++-- frontend/src/routes/documents/[id]/edit/+page.svelte | 2 +- frontend/src/routes/enrich/[id]/+page.svelte | 2 +- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/frontend/e2e/back-button.spec.ts b/frontend/e2e/back-button.spec.ts index 4bfa0f66..dcc94bd8 100644 --- a/frontend/e2e/back-button.spec.ts +++ b/frontend/e2e/back-button.spec.ts @@ -1,6 +1,9 @@ import AxeBuilder from '@axe-core/playwright'; import { test, expect } from '@playwright/test'; +// [data-hydrated] is set by +layout.svelte once SvelteKit's client-side hydration is complete. +// Waiting on it ensures the component is interactive before we interact with it. + test.describe('BackButton — navigation', () => { test('returns to previous page via history when clicked', async ({ page }) => { // Navigate to persons list, then to a person detail diff --git a/frontend/src/lib/components/BackButton.svelte b/frontend/src/lib/components/BackButton.svelte index 4b2edbaf..622cd726 100644 --- a/frontend/src/lib/components/BackButton.svelte +++ b/frontend/src/lib/components/BackButton.svelte @@ -1,11 +1,12 @@