Responsiv machen und Barrierefreiheit nach WCAG 2.1 AA herstellen
All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 1m33s
All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 1m33s
Rechtsrahmen: § 13 Abs. 3 LBGG SH i. V. m. §§ 3 und 4 BITV 2.0. Eine Erklaerung "nicht barrierefrei" ersetzt die Herstellungspflicht nicht, sie dokumentiert den Stand nur voruebergehend. Farben, alle Werte gegen die WCAG-Formel nachgerechnet: - Sekundaertext 4.40:1 auf muted-Flaechen -> 4.55:1 - Rahmen von Eingabefeldern 1.24:1 -> 3:1 (WCAG 1.4.11); dekorative Trenner bleiben unveraendert - Badge-Farben scheiterten auf der eigenen 10-Prozent-Flaeche (brand 4.20, success 4.18, destructive 3.88) -> jeweils ueber 4.5 - Warnfarbe im Admin 3.11:1 -> amber-700 mit 4.90:1 Fokus war faktisch unsichtbar: Der Ring wurde durchgaengig mit halber Deckkraft gezeichnet (ring-ring/50), effektiv rund 1.5:1. Jetzt volle Deckkraft in Markenfarbe, 12 Dateien betroffen. Weiter: Sprungmarke zum Inhalt auf allen Seiten, Suchfelder beschriftet, Druck-Schaltflaechen hatten als reine Icon-Buttons gar keinen zugaenglichen Namen, Liste als echte Liste ausgezeichnet, doppelter Link je Zeile entfernt, Trefferzahl als Live-Region. Responsiv: Aussenabstaende und Kopfzeilen ab 320 Pixel, Listenzeilen brechen um. Neu nach § 4 BITV 2.0: Seiten fuer Leichte Sprache und Gebaerdensprache, im Admin pflegbar. Leer gelassen und damit noch nicht online - die Inhalte kommen vom Amt. In der Erklaerung als bekannte Luecke benannt. Geprueft mit axe-core (WCAG 2.1 A und AA) ueber alle oeffentlichen Seiten und den Adminbereich, hell und dunkel, dazu Tastaturbedienung, 200 Prozent Vergroesserung und 320 Pixel Breite: keine Verstoesse, kein waagerechter Ueberlauf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c951a4123a
commit
3633e8a4cf
@ -217,7 +217,7 @@ export default function AdminDashboard() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<header className="sticky top-0 z-40 border-b bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<header className="sticky top-0 z-40 border-b bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||||
<div className="mx-auto flex h-14 max-w-7xl items-center justify-between gap-4 px-6">
|
<div className="mx-auto flex h-14 max-w-7xl items-center justify-between gap-2 px-4 sm:gap-4 sm:px-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Brand />
|
<Brand />
|
||||||
<Badge variant="secondary">Admin</Badge>
|
<Badge variant="secondary">Admin</Badge>
|
||||||
@ -314,7 +314,7 @@ export default function AdminDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="mx-auto max-w-7xl px-6 py-8">
|
<main id="inhalt" tabIndex={-1} className="mx-auto max-w-7xl px-4 py-6 sm:px-6 sm:py-8">
|
||||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
@ -326,7 +326,7 @@ export default function AdminDashboard() {
|
|||||||
{incompleteCount > 0 && (
|
{incompleteCount > 0 && (
|
||||||
<>
|
<>
|
||||||
{" · "}
|
{" · "}
|
||||||
<span className="text-amber-600 dark:text-amber-500">
|
<span className="text-amber-700 dark:text-amber-400">
|
||||||
{incompleteCount} unvollständig
|
{incompleteCount} unvollständig
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
@ -334,10 +334,19 @@ export default function AdminDashboard() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex w-full items-center gap-2 sm:w-auto">
|
||||||
<div className="relative flex-1 sm:w-64 sm:flex-none">
|
<div className="min-w-0 flex-1 sm:w-64 sm:flex-none">
|
||||||
<Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
<label htmlFor="admin-suche" className="sr-only">
|
||||||
|
Erklärungen nach Titel oder Verantwortlichem durchsuchen
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Search
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground"
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
id="admin-suche"
|
||||||
|
type="search"
|
||||||
placeholder="Suchen…"
|
placeholder="Suchen…"
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
@ -350,14 +359,15 @@ export default function AdminDashboard() {
|
|||||||
onClick={() => setSearchTerm("")}
|
onClick={() => setSearchTerm("")}
|
||||||
className="absolute top-1/2 right-0.5 -translate-y-1/2 text-muted-foreground"
|
className="absolute top-1/2 right-0.5 -translate-y-1/2 text-muted-foreground"
|
||||||
>
|
>
|
||||||
<X />
|
<X aria-hidden="true" />
|
||||||
<span className="sr-only">Suche zurücksetzen</span>
|
<span className="sr-only">Suche zurücksetzen</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleAddNew}>
|
<Button onClick={handleAddNew} className="shrink-0">
|
||||||
<Plus />
|
<Plus aria-hidden="true" />
|
||||||
Neu
|
Neu
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -448,7 +458,7 @@ export default function AdminDashboard() {
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="cursor-help border-amber-500/40 text-amber-600 dark:text-amber-500"
|
className="cursor-help border-amber-500/40 text-amber-700 dark:text-amber-400"
|
||||||
>
|
>
|
||||||
<TriangleAlert />
|
<TriangleAlert />
|
||||||
{missingByPolicy.get(policy.id)!.length}
|
{missingByPolicy.get(policy.id)!.length}
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export default function SignInPage() {
|
|||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main className="flex flex-1 items-center justify-center px-6 pb-20">
|
<main id="inhalt" tabIndex={-1} className="flex flex-1 items-center justify-center px-4 pb-20 sm:px-6">
|
||||||
<div className="w-full max-w-sm space-y-6">
|
<div className="w-full max-w-sm space-y-6">
|
||||||
<div className="flex flex-col items-center gap-3 text-center">
|
<div className="flex flex-col items-center gap-3 text-center">
|
||||||
<div className="flex size-11 items-center justify-center rounded-xl bg-brand text-brand-foreground">
|
<div className="flex size-11 items-center justify-center rounded-xl bg-brand text-brand-foreground">
|
||||||
|
|||||||
@ -129,7 +129,7 @@ export default async function PolicyPage({
|
|||||||
<div className="flex min-h-screen flex-col bg-background">
|
<div className="flex min-h-screen flex-col bg-background">
|
||||||
<PublicHeader subtitle="Datenschutz" />
|
<PublicHeader subtitle="Datenschutz" />
|
||||||
|
|
||||||
<main className="mx-auto w-full max-w-3xl flex-1 px-6 py-8">
|
<main id="inhalt" tabIndex={-1} className="mx-auto w-full max-w-3xl flex-1 px-4 py-6 sm:px-6 sm:py-8">
|
||||||
<Button variant="ghost" size="sm" asChild className="-ml-2 mb-4 text-muted-foreground">
|
<Button variant="ghost" size="sm" asChild className="-ml-2 mb-4 text-muted-foreground">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<ArrowLeft />
|
<ArrowLeft />
|
||||||
|
|||||||
10
app/gebaerdensprache/page.tsx
Normal file
10
app/gebaerdensprache/page.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { LegalPage, legalPageMetadata } from "@/components/legal-page"
|
||||||
|
|
||||||
|
// Inhalt kommt aus der Datenbank und wird im Admin gepflegt.
|
||||||
|
export const dynamic = "force-dynamic"
|
||||||
|
|
||||||
|
export const metadata = legalPageMetadata("signLanguage")
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <LegalPage page="signLanguage" />
|
||||||
|
}
|
||||||
@ -17,16 +17,19 @@
|
|||||||
--secondary: oklch(0.968 0.002 286);
|
--secondary: oklch(0.968 0.002 286);
|
||||||
--secondary-foreground: oklch(0.21 0.006 286);
|
--secondary-foreground: oklch(0.21 0.006 286);
|
||||||
--muted: oklch(0.968 0.002 286);
|
--muted: oklch(0.968 0.002 286);
|
||||||
--muted-foreground: oklch(0.552 0.014 286);
|
/* 4.55:1 auch auf muted-Flächen – vorher 0.552 und damit knapp darunter */
|
||||||
|
--muted-foreground: oklch(0.545 0.014 286);
|
||||||
--accent: oklch(0.968 0.002 286);
|
--accent: oklch(0.968 0.002 286);
|
||||||
--accent-foreground: oklch(0.21 0.006 286);
|
--accent-foreground: oklch(0.21 0.006 286);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.520 0.245 27.325); /* 4.5:1 auch auf 10%-Fläche im Badge */
|
||||||
--destructive-foreground: oklch(0.985 0 0);
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
--success: oklch(0.596 0.145 163);
|
--success: oklch(0.495 0.145 163); /* 4.5:1 auch auf 10%-Fläche im Badge */
|
||||||
--border: oklch(0.918 0.004 286);
|
--border: oklch(0.918 0.004 286);
|
||||||
--input: oklch(0.918 0.004 286);
|
/* Rahmen von Eingabefeldern: 3:1 nach WCAG 1.4.11, sonst ist das Feld
|
||||||
--ring: oklch(0.6 0.012 286);
|
nicht als solches erkennbar. Dekorative Trenner nutzen --border. */
|
||||||
--brand: oklch(0.55 0.155 258);
|
--input: oklch(0.620 0.004 286);
|
||||||
|
--ring: oklch(0.53 0.155 258); /* Fokus in Markenfarbe, 5.2:1 */
|
||||||
|
--brand: oklch(0.53 0.155 258); /* 4.5:1 auch auf 10%-Fläche im Badge */
|
||||||
--brand-foreground: oklch(0.985 0 0);
|
--brand-foreground: oklch(0.985 0 0);
|
||||||
--chart-1: oklch(0.55 0.155 258);
|
--chart-1: oklch(0.55 0.155 258);
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
@ -58,12 +61,12 @@
|
|||||||
--muted-foreground: oklch(0.712 0.013 286);
|
--muted-foreground: oklch(0.712 0.013 286);
|
||||||
--accent: oklch(0.256 0.006 286);
|
--accent: oklch(0.256 0.006 286);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.637 0.237 25.331);
|
--destructive: oklch(0.670 0.237 25.331); /* 4.5:1 auf 20%-Fläche im Badge */
|
||||||
--destructive-foreground: oklch(0.985 0 0);
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
--success: oklch(0.696 0.17 162.48);
|
--success: oklch(0.696 0.17 162.48);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(0.500 0.006 286); /* deckend statt transparent, 3:1 */
|
||||||
--ring: oklch(0.55 0.012 286);
|
--ring: oklch(0.68 0.14 258); /* Fokus in Markenfarbe, 6.9:1 */
|
||||||
--brand: oklch(0.68 0.14 258);
|
--brand: oklch(0.68 0.14 258);
|
||||||
--brand-foreground: oklch(0.14 0.004 286);
|
--brand-foreground: oklch(0.14 0.004 286);
|
||||||
--chart-1: oklch(0.68 0.14 258);
|
--chart-1: oklch(0.68 0.14 258);
|
||||||
@ -148,7 +151,7 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import localFont from "next/font/local"
|
|||||||
import { AppConfigProvider } from "@/components/providers/app-config-provider"
|
import { AppConfigProvider } from "@/components/providers/app-config-provider"
|
||||||
import { AuthProvider } from "@/components/providers/session-provider"
|
import { AuthProvider } from "@/components/providers/session-provider"
|
||||||
import { appName } from "@/lib/app-config"
|
import { appName } from "@/lib/app-config"
|
||||||
|
import { SkipLink } from "@/components/skip-link"
|
||||||
import { Toaster } from "@/components/ui/sonner"
|
import { Toaster } from "@/components/ui/sonner"
|
||||||
import "./globals.css"
|
import "./globals.css"
|
||||||
|
|
||||||
@ -56,6 +57,7 @@ export default function RootLayout({
|
|||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
|
<SkipLink />
|
||||||
<AppConfigProvider appName={appName()}>
|
<AppConfigProvider appName={appName()}>
|
||||||
<AuthProvider>{children}</AuthProvider>
|
<AuthProvider>{children}</AuthProvider>
|
||||||
</AppConfigProvider>
|
</AppConfigProvider>
|
||||||
|
|||||||
10
app/leichte-sprache/page.tsx
Normal file
10
app/leichte-sprache/page.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { LegalPage, legalPageMetadata } from "@/components/legal-page"
|
||||||
|
|
||||||
|
// Inhalt kommt aus der Datenbank und wird im Admin gepflegt.
|
||||||
|
export const dynamic = "force-dynamic"
|
||||||
|
|
||||||
|
export const metadata = legalPageMetadata("easyLanguage")
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <LegalPage page="easyLanguage" />
|
||||||
|
}
|
||||||
@ -70,7 +70,7 @@ export function DefaultableField({
|
|||||||
invalid={invalid}
|
invalid={invalid}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<Badge variant="outline" className="text-amber-600 dark:text-amber-500">
|
<Badge variant="outline" className="text-amber-700 dark:text-amber-400">
|
||||||
Weicht vom Standard ab
|
Weicht vom Standard ab
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export async function LegalPage({ page }: { page: SitePageKey }) {
|
|||||||
<div className="flex min-h-screen flex-col bg-background">
|
<div className="flex min-h-screen flex-col bg-background">
|
||||||
<PublicHeader subtitle={meta.title} />
|
<PublicHeader subtitle={meta.title} />
|
||||||
|
|
||||||
<main className="mx-auto w-full max-w-3xl flex-1 px-6 py-8">
|
<main id="inhalt" tabIndex={-1} className="mx-auto w-full max-w-3xl flex-1 px-4 py-6 sm:px-6 sm:py-8">
|
||||||
<h1 className="mb-6 text-2xl font-semibold tracking-tight">
|
<h1 className="mb-6 text-2xl font-semibold tracking-tight">
|
||||||
{meta.title}
|
{meta.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export function PolicyList({ initial }: { initial: PrivacyPolicy[] }) {
|
|||||||
}, [policies, searchTerm])
|
}, [policies, searchTerm])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto w-full max-w-5xl flex-1 px-6 py-8">
|
<main id="inhalt" tabIndex={-1} className="mx-auto w-full max-w-5xl flex-1 px-4 py-6 sm:px-6 sm:py-8">
|
||||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
@ -41,9 +41,18 @@ export function PolicyList({ initial }: { initial: PrivacyPolicy[] }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative w-full sm:w-72">
|
<div className="w-full sm:w-72">
|
||||||
<Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
<label htmlFor="suche" className="sr-only">
|
||||||
|
Datenschutzerklärungen durchsuchen
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Search
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground"
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
id="suche"
|
||||||
|
type="search"
|
||||||
placeholder="Suchen…"
|
placeholder="Suchen…"
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
@ -56,12 +65,13 @@ export function PolicyList({ initial }: { initial: PrivacyPolicy[] }) {
|
|||||||
onClick={() => setSearchTerm("")}
|
onClick={() => setSearchTerm("")}
|
||||||
className="absolute top-1/2 right-0.5 -translate-y-1/2 text-muted-foreground"
|
className="absolute top-1/2 right-0.5 -translate-y-1/2 text-muted-foreground"
|
||||||
>
|
>
|
||||||
<X />
|
<X aria-hidden="true" />
|
||||||
<span className="sr-only">Suche zurücksetzen</span>
|
<span className="sr-only">Suche zurücksetzen</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
|
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
|
||||||
@ -102,60 +112,58 @@ export function PolicyList({ initial }: { initial: PrivacyPolicy[] }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y overflow-hidden rounded-lg border bg-card">
|
<ul className="divide-y overflow-hidden rounded-lg border bg-card">
|
||||||
{filteredPolicies.map((policy) => (
|
{filteredPolicies.map((policy) => (
|
||||||
<div
|
<li
|
||||||
key={policy.id}
|
key={policy.id}
|
||||||
className="group flex items-center gap-3 px-3 transition-colors hover:bg-muted/40"
|
className="group flex flex-wrap items-center gap-x-3 gap-y-1 px-3 transition-colors hover:bg-muted/40 sm:flex-nowrap"
|
||||||
>
|
>
|
||||||
{/* Echter Link: teilbar, in neuem Tab zu öffnen, ohne JavaScript nutzbar */}
|
{/* Echter Link: teilbar, in neuem Tab zu öffnen, ohne JavaScript
|
||||||
|
nutzbar. Der Pfeil gehört mit hinein, statt daneben ein
|
||||||
|
zweiter Link auf dasselbe Ziel zu stehen. */}
|
||||||
<Link
|
<Link
|
||||||
href={`/erklaerung/${policy.slug}`}
|
href={`/erklaerung/${policy.slug}`}
|
||||||
className="flex min-w-0 flex-1 items-center gap-3 rounded-md py-4 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
className="flex min-w-0 flex-1 items-center gap-3 rounded-md py-4 outline-none focus-visible:ring-[3px] focus-visible:ring-ring"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-brand/10 text-brand"
|
||||||
>
|
>
|
||||||
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-brand/10 text-brand">
|
|
||||||
<FileText className="size-4.5" />
|
<FileText className="size-4.5" />
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block truncate font-medium">
|
<span className="block font-medium">{policy.title}</span>
|
||||||
{policy.title}
|
|
||||||
</span>
|
|
||||||
<span className="mt-0.5 block truncate text-sm text-muted-foreground">
|
<span className="mt-0.5 block truncate text-sm text-muted-foreground">
|
||||||
{policy.responsible.replace(/[*_#`]/g, "").split("\n")[0]}
|
{policy.responsible.replace(/[*_#`]/g, "").split("\n")[0]}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
<ChevronRight
|
||||||
|
aria-hidden="true"
|
||||||
|
className="hidden size-4 shrink-0 text-muted-foreground group-hover:text-foreground sm:block"
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-2 pr-1">
|
<div className="flex shrink-0 items-center gap-2 pb-3 sm:pb-0">
|
||||||
<span className="hidden text-sm text-muted-foreground sm:inline">
|
<span className="text-sm text-muted-foreground">
|
||||||
|
<span className="sr-only">Zuletzt geändert: </span>
|
||||||
{formatDate(policy.updatedAt)}
|
{formatDate(policy.updatedAt)}
|
||||||
</span>
|
</span>
|
||||||
<Separator orientation="vertical" className="hidden h-5 sm:block" />
|
<Separator orientation="vertical" className="h-5" />
|
||||||
<PrintPolicy policy={policy} size="icon-sm" variant="ghost" />
|
<PrintPolicy policy={policy} size="icon-sm" variant="ghost" />
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
asChild
|
|
||||||
className="text-muted-foreground group-hover:text-foreground"
|
|
||||||
>
|
|
||||||
<Link href={`/erklaerung/${policy.slug}`}>
|
|
||||||
<ChevronRight />
|
|
||||||
<span className="sr-only">{policy.title} öffnen</span>
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && !error && filteredPolicies.length > 0 && (
|
{!isLoading && !error && filteredPolicies.length > 0 && (
|
||||||
<div className="mt-4 flex items-center justify-between text-sm text-muted-foreground">
|
<div className="mt-4 flex items-center justify-between gap-3 text-sm text-muted-foreground">
|
||||||
<span>
|
{/* Beim Filtern hört eine Vorlesesoftware sonst nicht, was passiert */}
|
||||||
|
<p role="status" aria-live="polite">
|
||||||
{filteredPolicies.length}{" "}
|
{filteredPolicies.length}{" "}
|
||||||
{filteredPolicies.length === 1 ? "Erklärung" : "Erklärungen"}
|
{filteredPolicies.length === 1 ? "Erklärung" : "Erklärungen"}
|
||||||
{searchTerm && ` von ${policies.length}`}
|
{searchTerm && ` von ${policies.length}`}
|
||||||
</span>
|
</p>
|
||||||
<Badge variant="outline">DSGVO-konform</Badge>
|
<Badge variant="outline">DSGVO-konform</Badge>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -39,8 +39,15 @@ export function PrintPolicy({
|
|||||||
|
|
||||||
const button = (
|
const button = (
|
||||||
<Button variant={variant} size={size} onClick={handlePrint} className={className}>
|
<Button variant={variant} size={size} onClick={handlePrint} className={className}>
|
||||||
<Printer />
|
<Printer aria-hidden="true" />
|
||||||
{!isIconOnly && label}
|
{isIconOnly ? (
|
||||||
|
// Ein Tooltip ist kein zugänglicher Name – ohne diesen Text wäre die
|
||||||
|
// Schaltfläche für Screenreader namenlos. Der Titel steht mit drin,
|
||||||
|
// weil in der Liste mehrere gleich aussehende Schaltflächen stehen.
|
||||||
|
<span className="sr-only">{`${label}: ${policy.title}`}</span>
|
||||||
|
) : (
|
||||||
|
label
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import { ThemeToggle } from "@/components/theme-toggle"
|
|||||||
export function PublicHeader({ subtitle }: { subtitle?: string }) {
|
export function PublicHeader({ subtitle }: { subtitle?: string }) {
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-40 border-b bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<header className="sticky top-0 z-40 border-b bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||||
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between gap-4 px-6">
|
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between gap-2 px-4 sm:gap-4 sm:px-6">
|
||||||
<Link href="/" className="rounded-md outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50">
|
<Link href="/" className="rounded-md outline-none focus-visible:ring-[3px] focus-visible:ring-ring">
|
||||||
<Brand subtitle={subtitle} />
|
<Brand subtitle={subtitle} />
|
||||||
<span className="sr-only">Zur Übersicht</span>
|
<span className="sr-only">Zur Übersicht</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export async function SiteFooter() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="mt-12 border-t">
|
<footer className="mt-12 border-t">
|
||||||
<div className="mx-auto flex max-w-5xl flex-col gap-3 px-6 py-6 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
|
<div className="mx-auto flex max-w-5xl flex-col gap-3 px-4 py-6 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between sm:px-6">
|
||||||
<span>
|
<span>
|
||||||
© {new Date().getFullYear()} {appName()}
|
© {new Date().getFullYear()} {appName()}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -102,6 +102,56 @@ Name, E-Mail, Telefon
|
|||||||
Wenn wir auf Ihre Meldung nicht zufriedenstellend reagieren, können Sie die
|
Wenn wir auf Ihre Meldung nicht zufriedenstellend reagieren, können Sie die
|
||||||
Schlichtungsstelle nach § 16 BGG anrufen:
|
Schlichtungsstelle nach § 16 BGG anrufen:
|
||||||
Anschrift und Kontakt der zuständigen Schlichtungsstelle`,
|
Anschrift und Kontakt der zuständigen Schlichtungsstelle`,
|
||||||
|
|
||||||
|
easyLanguage: `## Willkommen
|
||||||
|
|
||||||
|
Das hier ist die Internet-Seite vom Amt Musterstadt.
|
||||||
|
Hier steht: Was macht das Amt mit Ihren Daten?
|
||||||
|
|
||||||
|
## Was finden Sie auf dieser Seite?
|
||||||
|
|
||||||
|
Sie finden hier eine Liste.
|
||||||
|
In der Liste stehen viele Themen.
|
||||||
|
Zum Beispiel: Bau-Antrag. Oder: Wohn-Geld.
|
||||||
|
Zu jedem Thema gibt es einen Text.
|
||||||
|
In dem Text steht: Welche Daten braucht das Amt?
|
||||||
|
Und: Wie lange speichert das Amt die Daten?
|
||||||
|
|
||||||
|
## Wie finden Sie ein Thema?
|
||||||
|
|
||||||
|
Oben auf der Seite ist ein Such-Feld.
|
||||||
|
Dort können Sie ein Wort eintippen.
|
||||||
|
Dann zeigt die Seite nur noch passende Themen.
|
||||||
|
Sie können auch auf ein Thema in der Liste klicken.
|
||||||
|
|
||||||
|
## Barriere-Freiheit
|
||||||
|
|
||||||
|
Barriere-frei heißt: Alle Menschen können die Seite benutzen.
|
||||||
|
Wir arbeiten daran, dass die Seite barriere-frei wird.
|
||||||
|
Manche Sachen sind noch nicht barriere-frei.
|
||||||
|
Sie können uns das melden.
|
||||||
|
Schreiben Sie an: *[E-Mail-Adresse eintragen]*
|
||||||
|
|
||||||
|
*Dieser Text muss vor der Veröffentlichung von einer Prüf-Gruppe
|
||||||
|
gegengelesen werden. Das ist bei Leichter Sprache üblich.*`,
|
||||||
|
|
||||||
|
signLanguage: `## Informationen in Deutscher Gebärdensprache
|
||||||
|
|
||||||
|
Hier entsteht ein Video in Deutscher Gebärdensprache.
|
||||||
|
|
||||||
|
Das Video erklärt:
|
||||||
|
- die wesentlichen Inhalte dieser Website
|
||||||
|
- wie die Navigation funktioniert
|
||||||
|
- die wesentlichen Inhalte der Erklärung zur Barrierefreiheit
|
||||||
|
|
||||||
|
*[Video einbinden, sobald es vorliegt. Erforderlich nach § 4 BITV 2.0.
|
||||||
|
Ein Video braucht eine Produktion mit gehörlosen Dolmetscherinnen oder
|
||||||
|
Dolmetschern – Untertitel allein genügen nicht.]*
|
||||||
|
|
||||||
|
### Kontakt
|
||||||
|
|
||||||
|
Fragen zur Barrierefreiheit richten Sie bitte an:
|
||||||
|
*[Kontaktdaten eintragen]*`,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SitePagesFormProps {
|
interface SitePagesFormProps {
|
||||||
@ -160,9 +210,10 @@ export function SitePagesForm({ open, onOpenChange }: SitePagesFormProps) {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Rechtliche Seiten</DialogTitle>
|
<DialogTitle>Rechtliche Seiten</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Impressum, Datenschutzerklärung und Erklärung zur Barrierefreiheit
|
Die eigenständigen Seiten des Portals: drei rechtliche Pflichtseiten
|
||||||
des Portals selbst. Sie erscheinen in der Fußzeile der öffentlichen
|
sowie Leichte Sprache und Gebärdensprache nach § 4 BITV 2.0. Sie
|
||||||
Seiten – ein leer gelassener Text nimmt die Seite vom Netz.
|
erscheinen in der Fußzeile der öffentlichen Seiten – ein leer
|
||||||
|
gelassener Text nimmt die betreffende Seite vom Netz.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@ -172,12 +223,10 @@ export function SitePagesForm({ open, onOpenChange }: SitePagesFormProps) {
|
|||||||
onValueChange={(value) => setTab(value as SitePageKey)}
|
onValueChange={(value) => setTab(value as SitePageKey)}
|
||||||
className="min-h-0 flex-1 gap-3"
|
className="min-h-0 flex-1 gap-3"
|
||||||
>
|
>
|
||||||
<TabsList>
|
<TabsList className="h-auto flex-wrap">
|
||||||
{SITE_PAGES.map((key) => (
|
{SITE_PAGES.map((key) => (
|
||||||
<TabsTrigger key={key} value={key}>
|
<TabsTrigger key={key} value={key}>
|
||||||
{key === "accessibility"
|
{SITE_PAGE_META[key].short}
|
||||||
? "Barrierefreiheit"
|
|
||||||
: SITE_PAGE_META[key].title}
|
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|||||||
17
components/skip-link.tsx
Normal file
17
components/skip-link.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Sprungmarke zum Hauptinhalt (WCAG 2.4.1, Ebene A).
|
||||||
|
*
|
||||||
|
* Unsichtbar, bis sie den Fokus bekommt – dann erscheint sie als erstes
|
||||||
|
* bedienbares Element oben links. Wer mit der Tastatur navigiert, überspringt
|
||||||
|
* damit Kopfzeile und Navigation, statt sie auf jeder Seite durchzutabben.
|
||||||
|
*/
|
||||||
|
export function SkipLink() {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href="#inhalt"
|
||||||
|
className="sr-only rounded-md bg-background px-4 py-2 text-sm font-medium text-foreground shadow-lg outline-none ring-2 ring-ring focus:not-sr-only focus:absolute focus:top-3 focus:left-3 focus:z-50"
|
||||||
|
>
|
||||||
|
Zum Inhalt springen
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -35,7 +35,7 @@ function AccordionTrigger({
|
|||||||
<AccordionPrimitive.Trigger
|
<AccordionPrimitive.Trigger
|
||||||
data-slot="accordion-trigger"
|
data-slot="accordion-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
"flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-md border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [&>svg]:pointer-events-none [&>svg]:size-3",
|
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-md border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@ -5,14 +5,14 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none disabled:pointer-events-none disabled:opacity-50 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none disabled:pointer-events-none disabled:opacity-50 focus-visible:border-ring focus-visible:ring-ring focus-visible:ring-[3px] aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive",
|
||||||
outline:
|
outline:
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
secondary:
|
secondary:
|
||||||
|
|||||||
@ -11,7 +11,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none md:text-sm",
|
"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none md:text-sm",
|
||||||
"file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground",
|
"file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground",
|
||||||
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30",
|
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"focus-visible:border-ring focus-visible:ring-ring focus-visible:ring-[3px]",
|
||||||
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
||||||
className
|
className
|
||||||
|
|||||||
@ -18,7 +18,7 @@ function ScrollArea({
|
|||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.Viewport
|
<ScrollAreaPrimitive.Viewport
|
||||||
data-slot="scroll-area-viewport"
|
data-slot="scroll-area-viewport"
|
||||||
className="size-full rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
className="size-full rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
|||||||
@ -32,7 +32,7 @@ function SelectTrigger({
|
|||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30 dark:hover:bg-input/50",
|
"flex h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30 dark:hover:bg-input/50",
|
||||||
"data-[placeholder]:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
"data-[placeholder]:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring",
|
||||||
"disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
"disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className
|
||||||
|
|||||||
@ -14,7 +14,7 @@ function Switch({
|
|||||||
data-slot="switch"
|
data-slot="switch"
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer inline-flex h-[1.15rem] w-8 shrink-0 cursor-pointer items-center rounded-full border border-transparent shadow-xs transition-all outline-none",
|
"peer inline-flex h-[1.15rem] w-8 shrink-0 cursor-pointer items-center rounded-full border border-transparent shadow-xs transition-all outline-none",
|
||||||
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50",
|
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
"data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
|
"data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -43,7 +43,7 @@ function TabsTrigger({
|
|||||||
data-slot="tabs-trigger"
|
data-slot="tabs-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-[calc(100%-1px)] flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none",
|
"inline-flex h-[calc(100%-1px)] flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none",
|
||||||
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50",
|
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||||
"data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30",
|
"data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30",
|
||||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className
|
||||||
|
|||||||
@ -9,7 +9,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none md:text-sm",
|
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none md:text-sm",
|
||||||
"placeholder:text-muted-foreground dark:bg-input/30",
|
"placeholder:text-muted-foreground dark:bg-input/30",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"focus-visible:border-ring focus-visible:ring-ring focus-visible:ring-[3px]",
|
||||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
||||||
className
|
className
|
||||||
|
|||||||
@ -1,32 +1,59 @@
|
|||||||
/** Die drei rechtlichen Seiten des Portals. */
|
/**
|
||||||
export const SITE_PAGES = ["imprint", "privacy", "accessibility"] as const
|
* Die eigenständigen Seiten des Portals.
|
||||||
|
*
|
||||||
|
* Drei rechtliche Pflichtseiten sowie zwei Seiten, die § 4 BITV 2.0 von
|
||||||
|
* öffentlichen Stellen verlangt: Erläuterungen in Leichter Sprache und in
|
||||||
|
* Deutscher Gebärdensprache.
|
||||||
|
*/
|
||||||
|
export const SITE_PAGES = [
|
||||||
|
"imprint",
|
||||||
|
"privacy",
|
||||||
|
"accessibility",
|
||||||
|
"easyLanguage",
|
||||||
|
"signLanguage",
|
||||||
|
] as const
|
||||||
|
|
||||||
/** Bezeichner einer rechtlichen Seite. */
|
/** Bezeichner einer Seite. */
|
||||||
export type SitePageKey = (typeof SITE_PAGES)[number]
|
export type SitePageKey = (typeof SITE_PAGES)[number]
|
||||||
|
|
||||||
/** Überschrift und URL je Seite – für Fußzeile, Admin und die Seiten selbst. */
|
/** Überschrift und URL je Seite – für Fußzeile, Admin und die Seiten selbst. */
|
||||||
export const SITE_PAGE_META: Record<
|
export const SITE_PAGE_META: Record<
|
||||||
SitePageKey,
|
SitePageKey,
|
||||||
{ title: string; path: string; hint: string }
|
{ title: string; short: string; path: string; hint: string }
|
||||||
> = {
|
> = {
|
||||||
imprint: {
|
imprint: {
|
||||||
title: "Impressum",
|
title: "Impressum",
|
||||||
|
short: "Impressum",
|
||||||
path: "/impressum",
|
path: "/impressum",
|
||||||
hint: "Anbieterkennzeichnung nach § 5 DDG",
|
hint: "Anbieterkennzeichnung nach § 5 DDG",
|
||||||
},
|
},
|
||||||
privacy: {
|
privacy: {
|
||||||
title: "Datenschutzerklärung",
|
title: "Datenschutzerklärung",
|
||||||
|
short: "Datenschutz",
|
||||||
path: "/datenschutz",
|
path: "/datenschutz",
|
||||||
hint: "Datenverarbeitung durch dieses Portal selbst",
|
hint: "Datenverarbeitung durch dieses Portal selbst",
|
||||||
},
|
},
|
||||||
accessibility: {
|
accessibility: {
|
||||||
title: "Erklärung zur Barrierefreiheit",
|
title: "Erklärung zur Barrierefreiheit",
|
||||||
|
short: "Barrierefreiheit",
|
||||||
path: "/barrierefreiheit",
|
path: "/barrierefreiheit",
|
||||||
hint: "Pflichtangabe öffentlicher Stellen nach BITV 2.0",
|
hint: "Pflichtangabe öffentlicher Stellen nach § 7 BITV 2.0, jährlich zu überprüfen",
|
||||||
|
},
|
||||||
|
easyLanguage: {
|
||||||
|
title: "Leichte Sprache",
|
||||||
|
short: "Leichte Sprache",
|
||||||
|
path: "/leichte-sprache",
|
||||||
|
hint: "Pflichtangabe nach § 4 BITV 2.0: wesentliche Inhalte, Navigation und Barrierefreiheitserklärung in Leichter Sprache",
|
||||||
|
},
|
||||||
|
signLanguage: {
|
||||||
|
title: "Gebärdensprache",
|
||||||
|
short: "Gebärdensprache",
|
||||||
|
path: "/gebaerdensprache",
|
||||||
|
hint: "Pflichtangabe nach § 4 BITV 2.0: dieselben Inhalte in Deutscher Gebärdensprache, üblicherweise als Video",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Inhalte der rechtlichen Seiten, wie sie API und Admin austauschen. */
|
/** Inhalte der Seiten, wie sie API und Admin austauschen. */
|
||||||
export type SitePagesContent = Record<SitePageKey, string>
|
export type SitePagesContent = Record<SitePageKey, string>
|
||||||
|
|
||||||
/** Leere Ausgangsinhalte – solange im Admin nichts gepflegt wurde. */
|
/** Leere Ausgangsinhalte – solange im Admin nichts gepflegt wurde. */
|
||||||
@ -34,4 +61,6 @@ export const EMPTY_SITE_PAGES: SitePagesContent = {
|
|||||||
imprint: "",
|
imprint: "",
|
||||||
privacy: "",
|
privacy: "",
|
||||||
accessibility: "",
|
accessibility: "",
|
||||||
|
easyLanguage: "",
|
||||||
|
signLanguage: "",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
-- Zwei weitere Seiten, die § 4 BITV 2.0 für öffentliche Stellen verlangt:
|
||||||
|
-- Erläuterungen in Leichter Sprache und in Deutscher Gebärdensprache zu den
|
||||||
|
-- wesentlichen Inhalten, zur Navigation und zur Erklärung zur Barrierefreiheit.
|
||||||
|
--
|
||||||
|
-- Wie die übrigen Seiten: leerer Text bedeutet, die Seite wird nicht
|
||||||
|
-- ausgeliefert und nicht in der Fußzeile verlinkt.
|
||||||
|
ALTER TABLE "SitePages" ADD COLUMN "easyLanguage" TEXT NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE "SitePages" ADD COLUMN "signLanguage" TEXT NOT NULL DEFAULT '';
|
||||||
@ -40,7 +40,9 @@ model SitePages {
|
|||||||
id Int @id @default(1) // Singleton – immer 1
|
id Int @id @default(1) // Singleton – immer 1
|
||||||
imprint String @default("") // Impressum (§ 5 DDG)
|
imprint String @default("") // Impressum (§ 5 DDG)
|
||||||
privacy String @default("") // Datenschutzerklärung für dieses Portal
|
privacy String @default("") // Datenschutzerklärung für dieses Portal
|
||||||
accessibility String @default("") // Erklärung zur Barrierefreiheit (BITV 2.0)
|
accessibility String @default("") // Erklärung zur Barrierefreiheit (§ 7 BITV 2.0)
|
||||||
|
easyLanguage String @default("") // Erläuterungen in Leichter Sprache (§ 4 BITV 2.0)
|
||||||
|
signLanguage String @default("") // Erläuterungen in Deutscher Gebärdensprache (§ 4 BITV 2.0)
|
||||||
updatedAt DateTime @updatedAt // Automatisch bei Update gesetzt
|
updatedAt DateTime @updatedAt // Automatisch bei Update gesetzt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user