From f1889ff20c2e97b44fdcea633151385d1a9cdc9c Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 16 Apr 2026 23:34:29 +0200 Subject: [PATCH] feat(#248): add TagDeleteGuard component and brand-warning CSS token Co-Authored-By: Claude Sonnet 4.6 --- .../admin/tags/[id]/TagDeleteGuard.svelte | 100 ++++++++++++++++++ .../tags/[id]/TagDeleteGuard.svelte.spec.ts | 54 ++++++++++ frontend/src/routes/layout.css | 4 + 3 files changed, 158 insertions(+) create mode 100644 frontend/src/routes/admin/tags/[id]/TagDeleteGuard.svelte create mode 100644 frontend/src/routes/admin/tags/[id]/TagDeleteGuard.svelte.spec.ts diff --git a/frontend/src/routes/admin/tags/[id]/TagDeleteGuard.svelte b/frontend/src/routes/admin/tags/[id]/TagDeleteGuard.svelte new file mode 100644 index 00000000..9238e769 --- /dev/null +++ b/frontend/src/routes/admin/tags/[id]/TagDeleteGuard.svelte @@ -0,0 +1,100 @@ + + +
+

{m.btn_delete()}

+ + +

+ {m.admin_tag_delete_impact({ docs: tag.documentCount, descendants: descendantCount })} +

+ + +
+ + + +
+ + +
+ + +
+
diff --git a/frontend/src/routes/admin/tags/[id]/TagDeleteGuard.svelte.spec.ts b/frontend/src/routes/admin/tags/[id]/TagDeleteGuard.svelte.spec.ts new file mode 100644 index 00000000..3f8565b0 --- /dev/null +++ b/frontend/src/routes/admin/tags/[id]/TagDeleteGuard.svelte.spec.ts @@ -0,0 +1,54 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import TagDeleteGuard from './TagDeleteGuard.svelte'; + +vi.mock('$app/forms', () => ({ enhance: () => () => {} })); + +afterEach(cleanup); + +const tag = { id: 't1', name: 'Familie', documentCount: 3 }; +const allTags = [ + { id: 't1', name: 'Familie', documentCount: 3 }, + { id: 't2', name: 'Eltern', parentId: 't1', documentCount: 1 }, + { id: 't3', name: 'Kinder', parentId: 't1', documentCount: 0 } +]; + +describe('TagDeleteGuard', () => { + it('renders two radio options (single and subtree)', async () => { + render(TagDeleteGuard, { tag, allTags }); + const radios = document.querySelectorAll('input[type="radio"]'); + expect(radios.length).toBe(2); + }); + + it('delete button is disabled initially', async () => { + render(TagDeleteGuard, { tag, allTags }); + const btn = document.querySelector('button[type="submit"]'); + expect(btn?.disabled).toBe(true); + }); + + it('delete button is enabled after selecting single radio', async () => { + render(TagDeleteGuard, { tag, allTags }); + await page.getByRole('radio', { name: /Nur dieses/i }).click(); + const btn = document.querySelector('button[type="submit"]'); + expect(btn?.disabled).toBe(false); + }); + + it('delete button is enabled after selecting subtree radio', async () => { + render(TagDeleteGuard, { tag, allTags }); + await page.getByRole('radio', { name: /Gesamten Teilbaum/i }).click(); + const btn = document.querySelector('button[type="submit"]'); + expect(btn?.disabled).toBe(false); + }); + + it('shows descendant count in impact summary', async () => { + render(TagDeleteGuard, { tag, allTags }); + // tag has 2 descendants (t2 and t3) + await expect.element(page.getByText(/2 Untergeordnete/)).toBeInTheDocument(); + }); + + it('shows document count in impact summary', async () => { + render(TagDeleteGuard, { tag, allTags }); + await expect.element(page.getByText(/3 Dokument/)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/routes/layout.css b/frontend/src/routes/layout.css index 39a433e7..bd984cdf 100644 --- a/frontend/src/routes/layout.css +++ b/frontend/src/routes/layout.css @@ -70,6 +70,10 @@ --color-danger: var(--c-danger); --color-danger-fg: var(--c-danger-fg); + /* Warning — amber, WCAG AA on white */ + --color-warning: #b45309; + --color-warning-fg: #ffffff; + /* Static brand tokens (not themed) */ --color-brand-navy: var(--palette-navy); --color-brand-mint: var(--palette-mint);