feat(richtlinien): add /hilfe/transkription page with RichtlinienRuleCard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
30
frontend/src/lib/components/RichtlinienRuleCard.svelte
Normal file
30
frontend/src/lib/components/RichtlinienRuleCard.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
type Props = {
|
||||
icon: string;
|
||||
title: string;
|
||||
body: string;
|
||||
beispielOutput?: string;
|
||||
beispielLabel?: string;
|
||||
};
|
||||
|
||||
let { icon, title, body, beispielOutput, beispielLabel = 'Beispiel' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="border-brand-sand break-inside-avoid rounded-sm border bg-white p-5 shadow-sm">
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<span aria-hidden="true" class="text-xl">{icon}</span>
|
||||
<h3 class="font-serif text-base font-bold text-ink">{title}</h3>
|
||||
</div>
|
||||
<p class="font-serif text-sm leading-relaxed text-ink-2">{body}</p>
|
||||
|
||||
{#if beispielOutput !== undefined}
|
||||
<div class="border-brand-sand mt-4 rounded-sm border bg-[#FAF8F1] px-4 py-3">
|
||||
<p class="font-sans text-xs font-semibold tracking-wider text-ink-3 uppercase">
|
||||
{beispielLabel}
|
||||
</p>
|
||||
<p class="mt-1 font-sans text-sm text-ink">
|
||||
→ <code class="font-mono">{beispielOutput}</code>
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import RichtlinienRuleCard from './RichtlinienRuleCard.svelte';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const defaultProps = {
|
||||
icon: '✍',
|
||||
title: 'Unleserliche Wörter',
|
||||
body: 'Schreiben Sie [unleserlich].',
|
||||
beispielOutput: '[unleserlich]'
|
||||
};
|
||||
|
||||
describe('RichtlinienRuleCard', () => {
|
||||
it('renders an h3 with the title', async () => {
|
||||
render(RichtlinienRuleCard, { props: defaultProps });
|
||||
await expect
|
||||
.element(page.getByRole('heading', { level: 3 }))
|
||||
.toHaveTextContent('Unleserliche Wörter');
|
||||
});
|
||||
|
||||
it('renders the body text', async () => {
|
||||
render(RichtlinienRuleCard, { props: defaultProps });
|
||||
await expect.element(page.getByText('Schreiben Sie [unleserlich].')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders icon in a span with aria-hidden="true"', async () => {
|
||||
render(RichtlinienRuleCard, { props: defaultProps });
|
||||
const iconSpan = document.querySelector('span[aria-hidden="true"]');
|
||||
expect(iconSpan).not.toBeNull();
|
||||
expect(iconSpan!.textContent).toContain('✍');
|
||||
});
|
||||
|
||||
it('renders beispielOutput in monospace with → arrow', async () => {
|
||||
render(RichtlinienRuleCard, { props: defaultProps });
|
||||
const mono = document.querySelector('code, [class*="font-mono"]');
|
||||
expect(mono).not.toBeNull();
|
||||
expect(mono!.textContent).toContain('[unleserlich]');
|
||||
await expect.element(page.getByText(/→/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render beispiel section when beispielOutput is absent', async () => {
|
||||
render(RichtlinienRuleCard, {
|
||||
props: { icon: '✍', title: 'Test', body: 'Body' }
|
||||
});
|
||||
expect(document.querySelector('code, [class*="font-mono"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
124
frontend/src/routes/hilfe/transkription/+page.svelte
Normal file
124
frontend/src/routes/hilfe/transkription/+page.svelte
Normal file
@@ -0,0 +1,124 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import RichtlinienRuleCard from '$lib/components/RichtlinienRuleCard.svelte';
|
||||
|
||||
const rules = [
|
||||
{
|
||||
icon: '❓',
|
||||
title: m.richtlinien_rule_unleserlich_title(),
|
||||
body: m.richtlinien_rule_unleserlich_body(),
|
||||
beispielOutput: '[unleserlich]'
|
||||
},
|
||||
{
|
||||
icon: '✗',
|
||||
title: m.richtlinien_rule_durchgestrichen_title(),
|
||||
body: m.richtlinien_rule_durchgestrichen_body(),
|
||||
beispielOutput: '[durchgestrichen: der Text]'
|
||||
},
|
||||
{
|
||||
icon: 'ſ',
|
||||
title: m.richtlinien_rule_langes_s_title(),
|
||||
body: m.richtlinien_rule_langes_s_body(),
|
||||
beispielOutput: 's'
|
||||
},
|
||||
{
|
||||
icon: '?',
|
||||
title: m.richtlinien_rule_name_title(),
|
||||
body: m.richtlinien_rule_name_body(),
|
||||
beispielOutput: '[Müller?]'
|
||||
},
|
||||
{
|
||||
icon: '💬',
|
||||
title: m.richtlinien_rule_dialekt_title(),
|
||||
body: m.richtlinien_rule_dialekt_body()
|
||||
}
|
||||
];
|
||||
|
||||
const klaerungChips = [
|
||||
m.richtlinien_klaer_abkuerzungen(),
|
||||
m.richtlinien_klaer_datumsformate(),
|
||||
m.richtlinien_klaer_umbrueche(),
|
||||
m.richtlinien_klaer_caps()
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.richtlinien_title()} — Familienarchiv</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-2xl px-4 py-10 font-serif">
|
||||
<!-- Title -->
|
||||
<h1 class="mb-4 text-3xl font-bold text-ink">{m.richtlinien_title()}</h1>
|
||||
|
||||
<!-- Intro -->
|
||||
<p class="mb-8 text-base leading-relaxed text-ink-2">{m.richtlinien_intro()}</p>
|
||||
|
||||
<!-- Wikipedia info card -->
|
||||
<div class="border-brand-sand mb-10 rounded-sm border bg-white p-5 shadow-sm">
|
||||
<p class="mb-3 font-sans text-sm text-ink-2">{m.richtlinien_wiki_text()}</p>
|
||||
<a
|
||||
href="https://de.wikipedia.org/wiki/Kurrent"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
referrerpolicy="no-referrer"
|
||||
class="inline-flex items-center gap-1 font-sans text-sm font-medium text-ink underline decoration-brand-mint decoration-[1.5px] underline-offset-[3px]"
|
||||
>
|
||||
{m.richtlinien_wiki_link()}
|
||||
<span class="new-tab ml-1 text-[11px] text-ink-3">({m.common_opens_new_tab()})</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Rules section -->
|
||||
<h2 class="mb-5 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||
{m.richtlinien_rules_label()}
|
||||
</h2>
|
||||
<div class="mb-10 flex flex-col gap-4">
|
||||
{#each rules as rule (rule.title)}
|
||||
<RichtlinienRuleCard
|
||||
icon={rule.icon}
|
||||
title={rule.title}
|
||||
body={rule.body}
|
||||
beispielOutput={rule.beispielOutput}
|
||||
beispielLabel={m.richtlinien_beispiel_label()}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Noch in Klärung -->
|
||||
<h2 class="mb-3 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||
{m.richtlinien_klaerung_label()}
|
||||
</h2>
|
||||
<p class="mb-4 font-serif text-sm leading-relaxed text-ink-2">
|
||||
{m.richtlinien_klaerung_intro()}
|
||||
</p>
|
||||
<div class="mb-10 flex flex-wrap gap-2">
|
||||
{#each klaerungChips as chip (chip)}
|
||||
<span
|
||||
class="border-brand-sand rounded-full border bg-white px-3 py-1 font-sans text-xs text-ink-2"
|
||||
>{chip}</span
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Closing card -->
|
||||
<div class="border-brand-sand rounded-sm border bg-white p-6 shadow-sm">
|
||||
<h2 class="mb-2 font-serif text-lg font-bold text-ink">{m.richtlinien_closing_title()}</h2>
|
||||
<p class="font-serif text-sm leading-relaxed text-ink-2">{m.richtlinien_closing_body()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@media print {
|
||||
:global(.app-nav) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.new-tab {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 1.5cm;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1
frontend/src/routes/hilfe/transkription/+page.ts
Normal file
1
frontend/src/routes/hilfe/transkription/+page.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const prerender = true;
|
||||
72
frontend/src/routes/hilfe/transkription/page.svelte.spec.ts
Normal file
72
frontend/src/routes/hilfe/transkription/page.svelte.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import Page from './+page.svelte';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe('Richtlinien page — structure', () => {
|
||||
it('renders h1 with richtlinien title', async () => {
|
||||
render(Page);
|
||||
await expect
|
||||
.element(page.getByRole('heading', { level: 1 }))
|
||||
.toHaveTextContent('Transkriptions-Richtlinien');
|
||||
});
|
||||
|
||||
it('renders intro paragraph', async () => {
|
||||
render(Page);
|
||||
await expect.element(page.getByText(/Damit alle Briefe einheitlich/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Wikipedia external link with security attributes and new-tab annotation', async () => {
|
||||
render(Page);
|
||||
const wikiLink = page.getByRole('link', { name: /Wikipedia/ });
|
||||
await expect.element(wikiLink).toBeInTheDocument();
|
||||
await expect.element(wikiLink).toHaveAttribute('target', '_blank');
|
||||
await expect.element(wikiLink).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
await expect.element(wikiLink).toHaveAttribute('referrerpolicy', 'no-referrer');
|
||||
// visible annotation (not sr-only)
|
||||
const link = document.querySelector('a[href*="wikipedia"]') as HTMLAnchorElement;
|
||||
expect(link.textContent).toContain('öffnet in neuem Tab');
|
||||
});
|
||||
|
||||
it('renders Regeln h2 section', async () => {
|
||||
render(Page);
|
||||
await expect
|
||||
.element(page.getByRole('heading', { level: 2, name: /Regeln für die Transkription/ }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Noch in Klärung h2 section', async () => {
|
||||
render(Page);
|
||||
await expect
|
||||
.element(page.getByRole('heading', { level: 2, name: /Noch in Klärung/ }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders closing invitation card', async () => {
|
||||
render(Page);
|
||||
await expect.element(page.getByText(/Fehlt eine Regel/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Richtlinien page — rule cards', () => {
|
||||
it('renders five rule card titles', async () => {
|
||||
render(Page);
|
||||
await expect.element(page.getByText('Nicht lesbare Wörter')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Durchgestrichene Wörter')).toBeInTheDocument();
|
||||
await expect.element(page.getByText(/Das lange s/)).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Unsichere Namen')).toBeInTheDocument();
|
||||
await expect.element(page.getByText(/Dialekt/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Richtlinien page — Noch in Klärung chips', () => {
|
||||
it('renders four clarification chips', async () => {
|
||||
render(Page);
|
||||
await expect.element(page.getByText('Abkürzungen')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Datumsformate')).toBeInTheDocument();
|
||||
await expect.element(page.getByText(/Zeilenumbrüche/)).toBeInTheDocument();
|
||||
await expect.element(page.getByText(/Groß-\/Kleinschreibung/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user