pflegepilot_v3: einheitliche Button-Stile statt Text-Links, Mobile-Fixes

Neue geteilte Button-Stile (lib/theme.ts: buttonStyles) ersetzen das
verbreitete Muster "unterstrichener Text" bei Aktionen wie Bearbeiten,
Loeschen, PDF und allen "+"-Funktionen - wirkte optisch nach Entwicklung
und war auf Touch-Geraeten schwer treffsicher. Angewendet auf Patientenakte,
Budget-Tracker, Dashboard, Admin-Dashboard, Changelog-Admin und Aufgaben.

Zusaetzlich zwei Mobile-Responsivitaets-Fixes auf der Patientenakte: das
Dokumenttyp-Auswahlfeld bekam eine explizite volle Breite, und der native
Datei-Upload-Button (der unter Tailwind-Preflight seine Browser-Chrome
verliert und dadurch wie reiner Fliesstext wirkte) wird jetzt ueber
Tailwinds file:-Variante gezielt wie ein echter Button gestylt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Sonnet 5 committed 2026-07-30 21:42:34 +02:00
1 parent dd47c14c5a
commit 467e05ccd5
6 files changed
+84 -26

No files matched your search

@@ -16,7 +16,7 @@ import {
import { ResponsiveTable, type Column } from "@/components/ResponsiveTable";
import { BegruendungModal } from "@/components/BegruendungModal";
import { Collapsible } from "@/components/Collapsible";
import { getAccent } from "@/lib/theme";
import { getAccent, buttonStyles } from "@/lib/theme";
type Aktion = { typ: "sperren" | "entsperren" | "loeschen" | "abo"; nutzer: NutzerAdmin };
@@ -114,17 +114,17 @@ export default function AdminPage() {
n.id === eigeneId ? (
<span className="text-xs text-slate-500">eigenes Konto</span>
) : (
<div className="flex flex-wrap gap-2 text-xs">
<div className="flex flex-wrap gap-2">
{n.aktiv ? (
<button onClick={() => setAktion({ typ: "sperren", nutzer: n })} className="text-red-600 underline">
<button onClick={() => setAktion({ typ: "sperren", nutzer: n })} className={buttonStyles.smallDanger}>
Sperren
</button>
) : (
<button onClick={() => setAktion({ typ: "entsperren", nutzer: n })} className="text-emerald-600 underline">
<button onClick={() => setAktion({ typ: "entsperren", nutzer: n })} className={buttonStyles.smallSuccess}>
Entsperren
</button>
)}
<button onClick={() => setAktion({ typ: "loeschen", nutzer: n })} className="text-red-600 underline">
<button onClick={() => setAktion({ typ: "loeschen", nutzer: n })} className={buttonStyles.smallDanger}>
Löschen
</button>
<button
@@ -132,11 +132,11 @@ export default function AdminPage() {
setMonate(1);
setAktion({ typ: "abo", nutzer: n });
}}
className="text-slate-600 underline"
className={buttonStyles.small}
>
Freischalten
</button>
<button onClick={() => rechnungErstellen(n)} className="text-slate-600 underline">
<button onClick={() => rechnungErstellen(n)} className={buttonStyles.small}>
Rechnung
</button>
</div>
@@ -151,13 +151,21 @@ export default function AdminPage() {
<section className="mb-6 rounded-2xl border border-slate-200 bg-white p-5">
<p className="mb-3 text-sm font-semibold text-slate-700">Neues Konto anlegen</p>
<div className="flex flex-wrap gap-2">
{/* autoComplete bewusst "off"/"new-password" statt username/
current-password: dies legt ein NEUES, fremdes Konto an, iOS
Safari soll hier nicht die eigenen gespeicherten Zugangsdaten
des Admins vorschlagen. */}
<input
name="neu_benutzername"
autoComplete="off"
placeholder="Benutzername"
value={neuBenutzername}
onChange={(e) => setNeuBenutzername(e.target.value)}
className="rounded-lg border border-slate-300 px-3 py-2 text-sm"
/>
<input
name="neu_passwort"
autoComplete="new-password"
placeholder="Passwort (min. 6 Zeichen)"
type="password"
value={neuPasswort}
@@ -165,12 +173,16 @@ export default function AdminPage() {
className="rounded-lg border border-slate-300 px-3 py-2 text-sm"
/>
<input
name="neu_name"
autoComplete="off"
placeholder="Name"
value={neuName}
onChange={(e) => setNeuName(e.target.value)}
className="rounded-lg border border-slate-300 px-3 py-2 text-sm"
/>
<input
name="neu_email"
autoComplete="off"
placeholder="E-Mail (optional)"
type="email"
value={neuEmail}
@@ -216,6 +228,8 @@ export default function AdminPage() {
<div>
<label className="mb-1 block text-xs font-medium text-slate-500">Patienten-ID</label>
<input
name="dsgvo_patient_id"
autoComplete="off"
type="number"
value={dsgvoPid ?? ""}
onChange={(e) => setDsgvoPid(e.target.value ? Number(e.target.value) : null)}
@@ -227,6 +241,8 @@ export default function AdminPage() {
Rechtsgrundlage / Aktenzeichen
</label>
<input
name="dsgvo_grund"
autoComplete="off"
value={dsgvoGrund}
onChange={(e) => setDsgvoGrund(e.target.value)}
className="w-64 rounded-lg border border-slate-300 px-3 py-2 text-sm"
@@ -295,18 +311,18 @@ export default function AdminPage() {
{(b.groesse_bytes / 1024 / 1024).toFixed(1)} MB
</span>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<a
href={backupUrl(b.dateiname)}
target="_blank"
rel="noreferrer"
className="text-xs text-slate-600 underline"
className={buttonStyles.small}
>
Herunterladen
</a>
<button
onClick={() => backupLoeschen.mutate(b.dateiname)}
className="text-xs text-red-600 underline"
className={buttonStyles.smallDanger}
>
Löschen
</button>
@@ -341,7 +357,7 @@ export default function AdminPage() {
setRestoreDatei(e.target.files?.[0] ?? null);
setRestoreErfolg(null);
}}
className="text-sm"
className={buttonStyles.fileInput}
/>
</div>
<div className="mb-3">
@@ -6,7 +6,7 @@ import { googleCalendarUrl, icsUrl, useAufgaben, useAufgabenMutationen } from "@
import { ResponsiveTable, type Column } from "@/components/ResponsiveTable";
import type { Aufgabe } from "@/lib/aufgaben";
import { AufgabeModal } from "@/components/AufgabeModal";
import { getAccent } from "@/lib/theme";
import { getAccent, buttonStyles } from "@/lib/theme";
const PRIORITAET_BADGE: Record<string, string> = {
hoch: getAccent("rose").badge,
@@ -75,7 +75,7 @@ export default function AufgabenPage() {
key: "aktion",
label: "",
render: (a) => (
<button onClick={() => erledigen.mutate(a.id)} className="text-xs text-emerald-600 underline">
<button onClick={() => erledigen.mutate(a.id)} className={buttonStyles.smallSuccess}>
Erledigt
</button>
),
@@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import { useAuth } from "@/lib/auth";
import { useChangelog, useChangelogMutationen, type ChangelogEintrag } from "@/lib/changelog";
import { accentForKey, getAccent } from "@/lib/theme";
import { accentForKey, getAccent, buttonStyles } from "@/lib/theme";
export default function ChangelogPage() {
const { meQuery } = useAuth();
@@ -50,7 +50,7 @@ export default function ChangelogPage() {
{istAdmin && (
<button
onClick={() => bearbeitenStarten("neu")}
className="rounded-lg bg-slate-800 px-3 py-1.5 text-sm font-medium text-white"
className={buttonStyles.add}
>
+ Neuer Eintrag
</button>
@@ -70,11 +70,11 @@ export default function ChangelogPage() {
<p className="mb-1 text-sm font-semibold text-slate-800">{e.titel}</p>
</div>
{istAdmin && (
<div className="flex gap-2 text-xs">
<button onClick={() => bearbeitenStarten(e)} className="text-slate-600 underline">
<div className="flex shrink-0 gap-2">
<button onClick={() => bearbeitenStarten(e)} className={buttonStyles.small}>
Bearbeiten
</button>
<button onClick={() => loeschen.mutate(e.id)} className="text-red-600 underline">
<button onClick={() => loeschen.mutate(e.id)} className={buttonStyles.smallDanger}>
Löschen
</button>
</div>
@@ -7,7 +7,7 @@ import { useDashboard, type DashboardPatientKachel } from "@/lib/patienten";
import { useAntraegeFuerPatient, pdfUrl } from "@/lib/briefe";
import { useAufgaben, type Aufgabe } from "@/lib/aufgaben";
import { BentoGrid, BentoTile } from "@/components/BentoGrid";
import { getAccent } from "@/lib/theme";
import { getAccent, buttonStyles } from "@/lib/theme";
const ANTRAG_TYP_LABEL: Record<string, string> = {
erstantrag: "Erstantrag",
@@ -16,6 +16,9 @@ const ANTRAG_TYP_LABEL: Record<string, string> = {
adressaenderung: "Adressänderung",
vollmacht: "Vollmacht",
patientenverfuegung: "Patientenverfügung",
eingliederung: "Eingliederungshilfe",
grundsicherung: "Grundsicherung",
blindenhilfe: "Blindenhilfe",
};
const ANTRAG_STATUS_LABEL: Record<string, string> = {
@@ -97,7 +100,7 @@ function PatientKachel({ patient, aufgaben }: { patient: DashboardPatientKachel;
target="_blank"
rel="noreferrer"
onClick={(e) => e.stopPropagation()}
className="shrink-0 text-slate-500 underline"
className={`shrink-0 ${buttonStyles.small}`}
>
PDF
</a>
@@ -158,8 +161,8 @@ export default function DashboardPage() {
<p className="mt-1 text-3xl font-semibold text-slate-800">{data.offene_aufgaben}</p>
</BentoTile>
<BentoTile accent="rose">
<p className="text-xs font-medium text-slate-500">Schnellzugriff</p>
<Link href="/patienten/neu" className="mt-1 block text-sm font-medium text-slate-700 underline">
<p className="mb-1.5 text-xs font-medium text-slate-500">Schnellzugriff</p>
<Link href="/patienten/neu" className={`w-full ${buttonStyles.add}`}>
+ Neuer Patient
</Link>
</BentoTile>
@@ -7,6 +7,7 @@ import {
useLeistungMutationen,
type Leistung,
} from "@/lib/leistungen";
import { buttonStyles } from "@/lib/theme";
export function BudgetTracker({ leistung }: { leistung: Leistung }) {
const [offen, setOffen] = useState(false);
@@ -26,7 +27,7 @@ export function BudgetTracker({ leistung }: { leistung: Leistung }) {
<button
type="button"
onClick={() => setOffen(true)}
className="self-start text-xs text-slate-500 underline"
className={`self-start ${buttonStyles.small}`}
>
Budget &amp; Ausgaben
</button>
@@ -44,7 +45,7 @@ export function BudgetTracker({ leistung }: { leistung: Leistung }) {
<div className="rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs">
<div className="mb-2 flex items-center justify-between">
<span className="font-medium text-slate-600">Budget &amp; Ausgaben {aktuellesJahr}</span>
<button type="button" onClick={() => setOffen(false)} className="text-slate-400 underline">
<button type="button" onClick={() => setOffen(false)} className={buttonStyles.small}>
Zuklappen
</button>
</div>
@@ -88,7 +89,7 @@ export function BudgetTracker({ leistung }: { leistung: Leistung }) {
<button
type="button"
onClick={() => ausgabeLoeschen.mutate(a.id)}
className="shrink-0 text-red-600 underline"
className={`shrink-0 ${buttonStyles.smallDanger}`}
>
Löschen
</button>
@@ -133,7 +134,7 @@ export function BudgetTracker({ leistung }: { leistung: Leistung }) {
{ onSuccess: () => setNeueAusgabe((f) => ({ ...f, betrag: "", beschreibung: "" })) }
)
}
className="rounded bg-slate-800 px-2 py-1 text-white disabled:opacity-50"
className={buttonStyles.smallPrimary}
>
+ Erfassen
</button>
+38
View File
@@ -109,3 +109,41 @@ export function accentForKey(key: string): AccentName {
}
return ACCENT_NAMES[hash % ACCENT_NAMES.length];
}
/**
* Wiederverwendbare Button-Stile fuer Aktions-Ausloeser. Ersetzt das
* verbreitete "nur unterstrichener Text" Muster (z. B. "Loeschen"/
* "Bearbeiten"/"PDF" als reiner Link) - wirkte optisch nach Entwicklung statt
* fertigem Produkt und war auf Touch-Geraeten schwer treffsicher zu tippen.
* Vier Farb-Varianten (neutral/primary/danger/success), je in normaler und
* kleiner (fuer dichte Listen/Tabellenzeilen) Groesse, plus eine Pill-Form
* fuer "+"-Aktionen ("+ Widerspruch", "+ eigene Leistung erfassen", ...).
* Alle Groessen liegen bei py-1.5+ fuer ausreichend grosse Tippflaechen.
*/
export const buttonStyles = {
secondary:
"inline-flex items-center justify-center gap-1 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50",
primary:
"inline-flex items-center justify-center gap-1 rounded-lg bg-slate-800 px-3 py-2 text-sm font-medium text-white transition hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-50",
danger:
"inline-flex items-center justify-center gap-1 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm font-medium text-red-700 transition hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-50",
success:
"inline-flex items-center justify-center gap-1 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm font-medium text-emerald-700 transition hover:bg-emerald-100 disabled:cursor-not-allowed disabled:opacity-50",
small:
"inline-flex items-center justify-center gap-1 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 text-xs font-medium text-slate-600 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50",
smallDanger:
"inline-flex items-center justify-center gap-1 rounded-lg border border-red-200 bg-red-50 px-2.5 py-1.5 text-xs font-medium text-red-700 transition hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-50",
smallSuccess:
"inline-flex items-center justify-center gap-1 rounded-lg border border-emerald-200 bg-emerald-50 px-2.5 py-1.5 text-xs font-medium text-emerald-700 transition hover:bg-emerald-100 disabled:cursor-not-allowed disabled:opacity-50",
smallPrimary:
"inline-flex items-center justify-center gap-1 rounded-lg bg-slate-800 px-2.5 py-1.5 text-xs font-medium text-white transition hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-50",
add:
"inline-flex items-center justify-center gap-1 rounded-full border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 transition hover:border-slate-400 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50",
/** Nativer <input type="file"> verliert unter Tailwind-Preflight seine
* Browser-Standard-Chrome und wirkt dadurch wie reiner Fliesstext statt
* eines Buttons - das ::file-selector-button-Pseudoelement wird ueber
* Tailwinds file:-Variante gezielt nachgestylt, damit es wie die uebrigen
* primary-Buttons der App aussieht. */
fileInput:
"block w-full text-sm text-slate-600 file:mr-3 file:cursor-pointer file:rounded-lg file:border-0 file:bg-slate-800 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white file:transition hover:file:bg-slate-700 file:disabled:cursor-not-allowed file:disabled:opacity-50",
} as const;