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:
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 & 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 & 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>
|
||||
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user