Dark Mode: folgt dem System, mit Umschalter
Drei Zustaende in der Kopfzeile — System, Hell, Dunkel. Drei statt zwei, weil ein reiner Ein/Aus-Schalter nach dem ersten Klick fuer immer eine feste Wahl bedeutet; wer "System" waehlt, bekommt abends dunkel und morgens hell. Umgesetzt ueber `light-dark()` im CSS, nicht ueber `dark:`-Klassen. Die Oberflaeche benutzt an rund 440 Stellen feste Farbklassen; jede davon zu verdoppeln waere eine riesige Fleissarbeit, und jede kuenftige Zeile muesste daran denken. Stattdessen entscheiden die Farbvariablen selbst, welchen Wert sie tragen — gesteuert ueber `color-scheme`. Deshalb genuegt fuer den Umschalter ein Attribut am html-Element, und die Systemwahl funktioniert sogar ohne JavaScript. Das Skript im Kopf setzt nur eine ausdrueckliche Wahl, damit die helle Fassung nicht kurz aufblitzt. Zwei Faelle liessen sich nicht ueber Variablen loesen: `bg-white` muss im Dunkeln eine dunkle Flaeche sein, `text-white` dagegen weiss bleiben — beide teilen sich aber dieselbe Variable. Fuer Flaechen gibt es deshalb jetzt `bg-surface`. Ebenso brauchen Akzent-Links im Dunkeln eine hellere Stufe als Akzent-Knoepfe: dafuer `text-accent-text` und `bg-accent-soft`. Bewusst unveraendert bleiben die QR-Codes selbst — deren Farben legt die Gestaltung fest. Die dunklen Werte sind keine Spiegelung: Reines Schwarz unter weisser Schrift flimmert. Alle Textkontraste sind gemessen und liegen ueber 4,5:1, der niedrigste bei 5,9:1.
This commit is contained in:
@@ -29,6 +29,8 @@ entscheidet die Verwaltung — nicht gebrauchte lassen sich ausblenden.
|
|||||||
- **Typen abschaltbar:** Was ihr nicht braucht, erscheint gar nicht erst.
|
- **Typen abschaltbar:** Was ihr nicht braucht, erscheint gar nicht erst.
|
||||||
- **Eigenes Erscheinungsbild:** Name, Logo, Favicon und Akzentfarbe im
|
- **Eigenes Erscheinungsbild:** Name, Logo, Favicon und Akzentfarbe im
|
||||||
Admin-Bereich einstellbar.
|
Admin-Bereich einstellbar.
|
||||||
|
- **Hell und dunkel:** folgt dem Betriebssystem, mit Umschalter für eine
|
||||||
|
eigene Wahl.
|
||||||
- **Zwei Container, kein Reverse Proxy nötig.** Migrationen laufen beim Start
|
- **Zwei Container, kein Reverse Proxy nötig.** Migrationen laufen beim Start
|
||||||
von selbst; alles Nötige steht in einer `.env`.
|
von selbst; alles Nötige steht in einer `.env`.
|
||||||
|
|
||||||
|
|||||||
+32
-1
@@ -368,7 +368,38 @@ der Stufe 600 steht weiße Schrift. Käme die Helligkeit aus der Eingabe, würde
|
|||||||
ein helles Gelb die Kopfzeile unlesbar machen — so bleibt jeder Farbton
|
ein helles Gelb die Kopfzeile unlesbar machen — so bleibt jeder Farbton
|
||||||
benutzbar.
|
benutzbar.
|
||||||
|
|
||||||
**Auf den Landingpages** gelten Farbe und Favicon, aber weder Logo noch
|
### Hell und dunkel
|
||||||
|
|
||||||
|
Die Anwendung folgt von sich aus der Einstellung des Betriebssystems. In der
|
||||||
|
Kopfzeile steht ein Umschalter mit drei Zuständen:
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| **System** | folgt dem Betriebssystem — abends dunkel, morgens hell |
|
||||||
|
| **Hell** | immer hell |
|
||||||
|
| **Dunkel** | immer dunkel |
|
||||||
|
|
||||||
|
Drei statt zwei, weil ein reiner Ein/Aus-Schalter nach dem ersten Klick für
|
||||||
|
immer eine feste Wahl bedeutet. Die Wahl merkt sich der Browser; ein zweiter
|
||||||
|
Tab zieht automatisch nach.
|
||||||
|
|
||||||
|
**Wie es umgesetzt ist.** Nicht über `dark:`-Klassen — die hätten an rund 440
|
||||||
|
Stellen ergänzt werden müssen, und jede neue Zeile müsste daran denken.
|
||||||
|
Stattdessen entscheidet `light-dark()` im CSS, welchen Wert die Farbvariablen
|
||||||
|
tragen; gesteuert wird das über `color-scheme`. Deshalb genügt für den
|
||||||
|
Umschalter ein Attribut am `<html>`-Element — und die Systemwahl funktioniert
|
||||||
|
sogar **ohne JavaScript**.
|
||||||
|
|
||||||
|
Die dunklen Werte sind keine Spiegelung der hellen: Reines Schwarz unter
|
||||||
|
weißer Schrift flimmert. Die Flächen liegen bei 20–26 % Helligkeit im selben
|
||||||
|
leicht blauen Ton wie die helle Reihe. Alle Textkontraste liegen über der
|
||||||
|
Anforderung von 4,5:1 — gemessen, nicht geschätzt.
|
||||||
|
|
||||||
|
Unverändert bleiben die QR-Codes selbst. Deren Farben legt die Gestaltung
|
||||||
|
fest, und ein Code, der hell gedruckt wird, soll in der Vorschau nicht anders
|
||||||
|
aussehen.
|
||||||
|
|
||||||
|
**Auf den Landingpages** gelten Farbe, Favicon und das Farbthema, aber weder Logo noch
|
||||||
Navigation. Wer einen Aushang scannt, soll die Seite des Inhalts sehen, nicht
|
Navigation. Wer einen Aushang scannt, soll die Seite des Inhalts sehen, nicht
|
||||||
die einer Verwaltungssoftware. Ein Logo je Gruppe auf den Landingpages steht
|
die einer Verwaltungssoftware. Ein Logo je Gruppe auf den Landingpages steht
|
||||||
als eigener Punkt auf der [Roadmap](roadmap.md).
|
als eigener Punkt auf der [Roadmap](roadmap.md).
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ Name, Logo, Favicon und Akzentfarbe im Admin-Bereich einstellbar. Die Farbe
|
|||||||
wirkt über die Farbvariablen, die Tailwind ohnehin benutzt — deshalb ohne
|
wirkt über die Farbvariablen, die Tailwind ohnehin benutzt — deshalb ohne
|
||||||
Umbau der rund 60 eingefärbten Stellen.
|
Umbau der rund 60 eingefärbten Stellen.
|
||||||
|
|
||||||
|
### Dark Mode (16.08.2026)
|
||||||
|
|
||||||
|
Folgt dem Betriebssystem, mit Umschalter für Hell, Dunkel und System.
|
||||||
|
Umgesetzt über `light-dark()` statt über `dark:`-Klassen — deshalb ohne
|
||||||
|
Änderung an den rund 440 Farbklassen und ohne JavaScript für die Systemwahl.
|
||||||
|
|
||||||
### Dokumentation (15.08.2026)
|
### Dokumentation (15.08.2026)
|
||||||
|
|
||||||
Bedienungsanleitung mit Bildern, Authentik-Anleitung, Funktionsübersicht.
|
Bedienungsanleitung mit Bildern, Authentik-Anleitung, Funktionsübersicht.
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ export default async function AccountPage() {
|
|||||||
<span className="font-medium">Die Anmeldung ist ausgeschaltet.</span>{" "}
|
<span className="font-medium">Die Anmeldung ist ausgeschaltet.</span>{" "}
|
||||||
Es gibt deshalb kein Konto und niemanden zum Abmelden — wer die
|
Es gibt deshalb kein Konto und niemanden zum Abmelden — wer die
|
||||||
Anwendung erreicht, darf alles. Zum Einschalten{" "}
|
Anwendung erreicht, darf alles. Zum Einschalten{" "}
|
||||||
<code className="rounded bg-white/70 px-1.5 py-0.5 font-mono text-xs">
|
<code className="rounded bg-surface/70 px-1.5 py-0.5 font-mono text-xs">
|
||||||
AUTH_ENABLED=true
|
AUTH_ENABLED=true
|
||||||
</code>{" "}
|
</code>{" "}
|
||||||
in der <code className="font-mono text-xs">.env</code> setzen und{" "}
|
in der <code className="font-mono text-xs">.env</code> setzen und{" "}
|
||||||
<code className="rounded bg-white/70 px-1.5 py-0.5 font-mono text-xs">
|
<code className="rounded bg-surface/70 px-1.5 py-0.5 font-mono text-xs">
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
</code>{" "}
|
</code>{" "}
|
||||||
ausführen. Beim nächsten Aufruf legst du den ersten Administrator
|
ausführen. Beim nächsten Aufruf legst du den ersten Administrator
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Link from "next/link";
|
|||||||
|
|
||||||
import { LogoutButton } from "@/components/auth-forms";
|
import { LogoutButton } from "@/components/auth-forms";
|
||||||
import { Icon } from "@/components/icons";
|
import { Icon } from "@/components/icons";
|
||||||
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { actorLabel, getActor, isAuthEnabled } from "@/lib/auth/actor";
|
import { actorLabel, getActor, isAuthEnabled } from "@/lib/auth/actor";
|
||||||
import { can } from "@/lib/auth/policy";
|
import { can } from "@/lib/auth/policy";
|
||||||
import { getBranding } from "@/lib/branding";
|
import { getBranding } from "@/lib/branding";
|
||||||
@@ -26,7 +27,7 @@ export default async function AppLayout({ children }: LayoutProps<"/">) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col">
|
||||||
<header className="border-b border-slate-200 bg-white">
|
<header className="border-b border-slate-200 bg-surface">
|
||||||
<div className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-x-4 gap-y-2 px-4 py-3.5 sm:px-6">
|
<div className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-x-4 gap-y-2 px-4 py-3.5 sm:px-6">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
@@ -54,6 +55,11 @@ export default async function AppLayout({ children }: LayoutProps<"/">) {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="flex flex-wrap items-center gap-0.5 text-sm font-medium sm:gap-1">
|
<nav className="flex flex-wrap items-center gap-0.5 text-sm font-medium sm:gap-1">
|
||||||
|
<ThemeToggle />
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mx-1 hidden h-5 w-px bg-slate-200 sm:block"
|
||||||
|
/>
|
||||||
{signedIn ? (
|
{signedIn ? (
|
||||||
<>
|
<>
|
||||||
<Link href="/" className={NAV_LINK}>
|
<Link href="/" className={NAV_LINK}>
|
||||||
@@ -81,7 +87,7 @@ export default async function AppLayout({ children }: LayoutProps<"/">) {
|
|||||||
title={`Konto von ${actor.user.email}`}
|
title={`Konto von ${actor.user.email}`}
|
||||||
className="flex max-w-[13rem] items-center gap-2 rounded-lg px-2.5 py-2 text-slate-700 transition-colors hover:bg-slate-100"
|
className="flex max-w-[13rem] items-center gap-2 rounded-lg px-2.5 py-2 text-slate-700 transition-colors hover:bg-slate-100"
|
||||||
>
|
>
|
||||||
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-indigo-100 text-[0.7rem] font-semibold text-indigo-700 uppercase">
|
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-accent-soft text-[0.7rem] font-semibold text-accent-soft-fg uppercase">
|
||||||
{actorLabel(actor).slice(0, 2)}
|
{actorLabel(actor).slice(0, 2)}
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden truncate sm:inline">
|
<span className="hidden truncate sm:inline">
|
||||||
@@ -121,7 +127,7 @@ export default async function AppLayout({ children }: LayoutProps<"/">) {
|
|||||||
|
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-1">{children}</main>
|
||||||
|
|
||||||
<footer className="border-t border-slate-200 bg-white">
|
<footer className="border-t border-slate-200 bg-surface">
|
||||||
<div className="mx-auto max-w-6xl px-4 py-6 text-sm text-slate-500 sm:px-6">
|
<div className="mx-auto max-w-6xl px-4 py-6 text-sm text-slate-500 sm:px-6">
|
||||||
{branding.appName}
|
{branding.appName}
|
||||||
{branding.footerText ? ` · ${branding.footerText}` : null}
|
{branding.footerText ? ` · ${branding.footerText}` : null}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export default async function FeedbackResultsPage({ params }: PageProps) {
|
|||||||
{kommentare.map((eintrag) => (
|
{kommentare.map((eintrag) => (
|
||||||
<li
|
<li
|
||||||
key={eintrag.id}
|
key={eintrag.id}
|
||||||
className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm"
|
className="rounded-xl border border-slate-200 bg-surface p-4 shadow-sm"
|
||||||
>
|
>
|
||||||
<div className="mb-1.5 flex items-center gap-2 text-sm text-slate-500">
|
<div className="mb-1.5 flex items-center gap-2 text-sm text-slate-500">
|
||||||
<span className="font-medium text-slate-900 tabular-nums">
|
<span className="font-medium text-slate-900 tabular-nums">
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default async function HomePage() {
|
|||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
usable
|
usable
|
||||||
? "flex size-11 shrink-0 items-center justify-center rounded-xl bg-indigo-50 text-indigo-600"
|
? "flex size-11 shrink-0 items-center justify-center rounded-xl bg-accent-soft text-accent-text"
|
||||||
: "flex size-11 shrink-0 items-center justify-center rounded-xl bg-slate-100 text-slate-400"
|
: "flex size-11 shrink-0 items-center justify-center rounded-xl bg-slate-100 text-slate-400"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -96,7 +96,7 @@ export default async function HomePage() {
|
|||||||
{usable ? (
|
{usable ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/erstellen/${qrType.id}`}
|
href={`/erstellen/${qrType.id}`}
|
||||||
className="flex h-full items-start gap-4 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm transition-all hover:border-indigo-300 hover:shadow-md"
|
className="flex h-full items-start gap-4 rounded-2xl border border-slate-200 bg-surface p-4 shadow-sm transition-all hover:border-indigo-300 hover:shadow-md"
|
||||||
>
|
>
|
||||||
{tile}
|
{tile}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ function Tile({
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className="flex items-start gap-4 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition-all hover:border-indigo-300 hover:shadow-md"
|
className="flex items-start gap-4 rounded-2xl border border-slate-200 bg-surface p-5 shadow-sm transition-all hover:border-indigo-300 hover:shadow-md"
|
||||||
>
|
>
|
||||||
<span className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-indigo-50 text-indigo-600">
|
<span className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-accent-soft text-accent-text">
|
||||||
<Icon name={icon} className="size-6" />
|
<Icon name={icon} className="size-6" />
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
@@ -78,11 +78,11 @@ export default async function AdminPage() {
|
|||||||
<Notice tone="info">
|
<Notice tone="info">
|
||||||
<span className="font-medium">Die Anmeldung ist ausgeschaltet.</span>{" "}
|
<span className="font-medium">Die Anmeldung ist ausgeschaltet.</span>{" "}
|
||||||
Jeder, der die Anwendung erreicht, darf alles. Zum Einschalten
|
Jeder, der die Anwendung erreicht, darf alles. Zum Einschalten
|
||||||
<code className="mx-1 rounded bg-white/70 px-1.5 py-0.5 font-mono text-xs">
|
<code className="mx-1 rounded bg-surface/70 px-1.5 py-0.5 font-mono text-xs">
|
||||||
AUTH_ENABLED=true
|
AUTH_ENABLED=true
|
||||||
</code>
|
</code>
|
||||||
in der <code className="font-mono text-xs">.env</code> setzen und
|
in der <code className="font-mono text-xs">.env</code> setzen und
|
||||||
<code className="mx-1 rounded bg-white/70 px-1.5 py-0.5 font-mono text-xs">
|
<code className="mx-1 rounded bg-surface/70 px-1.5 py-0.5 font-mono text-xs">
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
</code>
|
</code>
|
||||||
ausführen. Beim nächsten Aufruf legst du den ersten Administrator
|
ausführen. Beim nächsten Aufruf legst du den ersten Administrator
|
||||||
|
|||||||
+72
-1
@@ -9,10 +9,81 @@
|
|||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- Dark Mode
|
||||||
|
|
||||||
|
Umgesetzt über `light-dark()` statt über `dark:`-Klassen.
|
||||||
|
|
||||||
|
Der Grund: Die Oberfläche benutzt an rund 440 Stellen feste Farbklassen wie
|
||||||
|
`bg-slate-50` oder `text-slate-900`. Jede davon um eine `dark:`-Variante zu
|
||||||
|
ergänzen wäre eine riesige, fehleranfällige Fleißarbeit — und jede künftige
|
||||||
|
Zeile müsste daran denken. Stattdessen werden hier die Farbvariablen
|
||||||
|
umdefiniert, die Tailwind ohnehin benutzt: `bg-slate-50` wird zu
|
||||||
|
`background-color: var(--color-slate-50)`, und was diese Variable enthält,
|
||||||
|
entscheidet sich hier.
|
||||||
|
|
||||||
|
Welche Hälfte von `light-dark()` gilt, steuert `color-scheme` — deshalb
|
||||||
|
genügt für den Umschalter ein Attribut am <html>-Element. Ohne Attribut
|
||||||
|
folgt die Anwendung dem Betriebssystem, ganz ohne JavaScript.
|
||||||
|
|
||||||
|
Die dunklen Werte sind keine bloße Spiegelung: Reines Schwarz unter weißer
|
||||||
|
Schrift flimmert. Die Flächen liegen deshalb bei 16–26 % Helligkeit, mit
|
||||||
|
demselben leicht blauen Ton wie die helle Reihe.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
/* Grautöne. Hell: die Reihe von Tailwind. Dunkel: von dunkel nach hell
|
||||||
|
gedreht, damit `text-slate-900` weiterhin „die kräftigste Schrift“
|
||||||
|
bedeutet und `bg-slate-50` „der ruhigste Grund“. */
|
||||||
|
--color-slate-50: light-dark(oklch(98.4% 0.003 247.858), oklch(16.5% 0.018 265));
|
||||||
|
--color-slate-100: light-dark(oklch(96.8% 0.007 247.896), oklch(21% 0.02 265));
|
||||||
|
--color-slate-200: light-dark(oklch(92.9% 0.013 255.508), oklch(28% 0.022 262));
|
||||||
|
--color-slate-300: light-dark(oklch(86.9% 0.022 252.894), oklch(34% 0.026 260));
|
||||||
|
--color-slate-400: light-dark(oklch(70.4% 0.04 256.788), oklch(58% 0.032 258));
|
||||||
|
--color-slate-500: light-dark(oklch(55.4% 0.046 257.417), oklch(68% 0.03 257));
|
||||||
|
--color-slate-600: light-dark(oklch(44.6% 0.043 257.281), oklch(77% 0.025 257));
|
||||||
|
--color-slate-700: light-dark(oklch(37.2% 0.044 257.287), oklch(85% 0.02 257));
|
||||||
|
--color-slate-800: light-dark(oklch(27.9% 0.041 260.031), oklch(91% 0.014 258));
|
||||||
|
--color-slate-900: light-dark(oklch(20.8% 0.042 265.755), oklch(96.5% 0.008 258));
|
||||||
|
--color-slate-950: light-dark(oklch(12.9% 0.042 264.695), oklch(99% 0.003 258));
|
||||||
|
|
||||||
|
/* Flächen. `bg-white` wäre im Dunkeln falsch, `text-white` dagegen richtig
|
||||||
|
(es steht auf farbigem Grund) — beide teilen sich aber dieselbe Variable.
|
||||||
|
Deshalb tragen Flächen einen eigenen Namen: `bg-surface`. */
|
||||||
|
--color-surface: light-dark(#ffffff, oklch(22.5% 0.02 264));
|
||||||
|
|
||||||
|
/* Akzent auf dunklem Grund. Die Reihe selbst bleibt, wie das
|
||||||
|
Erscheinungsbild sie setzt — hier wird nur eine andere Stufe gewählt:
|
||||||
|
Fließtext braucht im Dunkeln eine hellere, Flächen eine dunklere. */
|
||||||
|
--color-accent-text: light-dark(
|
||||||
|
var(--color-indigo-600),
|
||||||
|
var(--color-indigo-400)
|
||||||
|
);
|
||||||
|
--color-accent-soft: light-dark(
|
||||||
|
var(--color-indigo-50),
|
||||||
|
color-mix(in oklab, var(--color-indigo-500) 22%, oklch(22.5% 0.02 264))
|
||||||
|
);
|
||||||
|
--color-accent-soft-fg: light-dark(
|
||||||
|
var(--color-indigo-700),
|
||||||
|
var(--color-indigo-300)
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Immer dunkel, unabhängig vom Thema: die Fläche hinter einem Dialog und
|
||||||
|
der Grund einer Videovorschau. */
|
||||||
|
--color-scrim: oklch(20.8% 0.042 265.755);
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--color-slate-50);
|
background-color: var(--color-slate-50);
|
||||||
color: var(--color-slate-900);
|
color: var(--color-slate-900);
|
||||||
@@ -22,7 +93,7 @@ body {
|
|||||||
|
|
||||||
/* Sichtbarer, einheitlicher Fokusring für Tastaturbedienung. */
|
/* Sichtbarer, einheitlicher Fokusring für Tastaturbedienung. */
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
outline: 2px solid var(--color-indigo-600);
|
outline: 2px solid var(--color-accent-text);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
import { THEME_INIT_SCRIPT } from "@/components/theme-toggle";
|
||||||
import { accentCss } from "@/lib/branding-colors";
|
import { accentCss } from "@/lib/branding-colors";
|
||||||
import { getBranding } from "@/lib/branding";
|
import { getBranding } from "@/lib/branding";
|
||||||
|
|
||||||
@@ -49,6 +50,12 @@ export default async function RootLayout({ children }: LayoutProps<"/">) {
|
|||||||
return (
|
return (
|
||||||
<html lang="de" className="h-full">
|
<html lang="de" className="h-full">
|
||||||
<body className="flex min-h-full flex-col">
|
<body className="flex min-h-full flex-col">
|
||||||
|
{/*
|
||||||
|
Setzt eine ausdrücklich gewählte Fassung, bevor der Browser zeichnet.
|
||||||
|
Wer der Systemeinstellung folgt, braucht das nicht — das erledigt
|
||||||
|
`light-dark()` im CSS von allein.
|
||||||
|
*/}
|
||||||
|
<script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />
|
||||||
{/*
|
{/*
|
||||||
Überschreibt die Farbvariablen, die Tailwind ohnehin benutzt — damit
|
Überschreibt die Farbvariablen, die Tailwind ohnehin benutzt — damit
|
||||||
wirkt die Akzentfarbe an jeder Stelle der Oberfläche, ohne dass eine
|
wirkt die Akzentfarbe an jeder Stelle der Oberfläche, ohne dass eine
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export function BrandingAdmin({ initial }: { initial: BrandingDto }) {
|
|||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setWert({ ...wert, accentColor: event.target.value.toUpperCase() })
|
setWert({ ...wert, accentColor: event.target.value.toUpperCase() })
|
||||||
}
|
}
|
||||||
className="size-10 cursor-pointer rounded-lg border border-slate-300 bg-white p-1"
|
className="size-10 cursor-pointer rounded-lg border border-slate-300 bg-surface p-1"
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="branding-accent"
|
id="branding-accent"
|
||||||
@@ -259,7 +259,7 @@ export function BrandingAdmin({ initial }: { initial: BrandingDto }) {
|
|||||||
accept={UPLOAD_KINDS.logo.accept}
|
accept={UPLOAD_KINDS.logo.accept}
|
||||||
inputRef={logoInput}
|
inputRef={logoInput}
|
||||||
laeuft={laeuft}
|
laeuft={laeuft}
|
||||||
hintergrund="bg-white"
|
hintergrund="bg-surface"
|
||||||
onWaehlen={(datei) => hochladen(datei, "logo", "logoUploadId")}
|
onWaehlen={(datei) => hochladen(datei, "logo", "logoUploadId")}
|
||||||
onEntfernen={() => setWert({ ...wert, logoUploadId: null })}
|
onEntfernen={() => setWert({ ...wert, logoUploadId: null })}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ export function CodesList() {
|
|||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
aria-label="Ansicht"
|
aria-label="Ansicht"
|
||||||
className="flex shrink-0 rounded-lg border border-slate-300 bg-white p-0.5"
|
className="flex shrink-0 rounded-lg border border-slate-300 bg-surface p-0.5"
|
||||||
>
|
>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
@@ -316,7 +316,7 @@ export function CodesList() {
|
|||||||
className={cx(
|
className={cx(
|
||||||
"rounded-md p-2 transition-colors",
|
"rounded-md p-2 transition-colors",
|
||||||
view === value
|
view === value
|
||||||
? "bg-indigo-50 text-indigo-700"
|
? "bg-accent-soft text-accent-soft-fg"
|
||||||
: "text-slate-500 hover:bg-slate-100 hover:text-slate-700",
|
: "text-slate-500 hover:bg-slate-100 hover:text-slate-700",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -374,7 +374,7 @@ export function CodesList() {
|
|||||||
setQuery("");
|
setQuery("");
|
||||||
setGroupFilter("alle");
|
setGroupFilter("alle");
|
||||||
}}
|
}}
|
||||||
className="font-medium text-indigo-600 underline"
|
className="font-medium text-accent-text underline"
|
||||||
>
|
>
|
||||||
Filter zurücksetzen
|
Filter zurücksetzen
|
||||||
</button>
|
</button>
|
||||||
@@ -384,7 +384,7 @@ export function CodesList() {
|
|||||||
|
|
||||||
{/* ------------------------------------------------------ Listenansicht */}
|
{/* ------------------------------------------------------ Listenansicht */}
|
||||||
{view === "list" && visible.length > 0 ? (
|
{view === "list" && visible.length > 0 ? (
|
||||||
<div className="rounded-2xl border border-slate-200 bg-white shadow-sm">
|
<div className="rounded-2xl border border-slate-200 bg-surface shadow-sm">
|
||||||
{/* Auf schmalen Schirmen blenden wir Spalten von rechts nach links
|
{/* Auf schmalen Schirmen blenden wir Spalten von rechts nach links
|
||||||
aus, statt die Tabelle seitlich scrollen zu lassen. Was wegfällt,
|
aus, statt die Tabelle seitlich scrollen zu lassen. Was wegfällt,
|
||||||
steht beim Namen (Kurz-URL) oder in der Kartenansicht. */}
|
steht beim Namen (Kurz-URL) oder in der Kartenansicht. */}
|
||||||
@@ -466,7 +466,7 @@ export function CodesList() {
|
|||||||
href={code.shortUrl}
|
href={code.shortUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="truncate font-mono text-xs text-indigo-600 hover:underline"
|
className="truncate font-mono text-xs text-accent-text hover:underline"
|
||||||
title={code.shortUrl}
|
title={code.shortUrl}
|
||||||
>
|
>
|
||||||
{code.shortUrl.replace(/^https?:\/\//, "")}
|
{code.shortUrl.replace(/^https?:\/\//, "")}
|
||||||
@@ -617,7 +617,7 @@ export function CodesList() {
|
|||||||
href={code.shortUrl}
|
href={code.shortUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="font-mono text-xs break-all text-indigo-600 hover:underline"
|
className="font-mono text-xs break-all text-accent-text hover:underline"
|
||||||
>
|
>
|
||||||
{code.shortUrl}
|
{code.shortUrl}
|
||||||
</a>
|
</a>
|
||||||
@@ -670,7 +670,7 @@ export function CodesList() {
|
|||||||
{code.type === "feedback" ? (
|
{code.type === "feedback" ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/meine-codes/${code.id}/feedback`}
|
href={`/meine-codes/${code.id}/feedback`}
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
className="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-surface px-3 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
||||||
>
|
>
|
||||||
<Icon name="star" className="size-3.5" />
|
<Icon name="star" className="size-3.5" />
|
||||||
Rückmeldungen
|
Rückmeldungen
|
||||||
@@ -687,14 +687,14 @@ export function CodesList() {
|
|||||||
</Button>
|
</Button>
|
||||||
<a
|
<a
|
||||||
href={`/api/codes/${code.id}/qr?format=png&download=1`}
|
href={`/api/codes/${code.id}/qr?format=png&download=1`}
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
className="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-surface px-3 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
||||||
>
|
>
|
||||||
<Icon name="download" className="size-3.5" />
|
<Icon name="download" className="size-3.5" />
|
||||||
PNG
|
PNG
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={`/api/codes/${code.id}/qr?format=svg&download=1`}
|
href={`/api/codes/${code.id}/qr?format=svg&download=1`}
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
className="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-surface px-3 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
||||||
>
|
>
|
||||||
<Icon name="download" className="size-3.5" />
|
<Icon name="download" className="size-3.5" />
|
||||||
SVG
|
SVG
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function ColorField({
|
|||||||
aria-label={`${label} auswählen`}
|
aria-label={`${label} auswählen`}
|
||||||
value={valid ? value : "#000000"}
|
value={valid ? value : "#000000"}
|
||||||
onChange={(event) => onChange(event.target.value.toUpperCase())}
|
onChange={(event) => onChange(event.target.value.toUpperCase())}
|
||||||
className="size-10 shrink-0 cursor-pointer rounded-lg border border-slate-300 bg-white p-1"
|
className="size-10 shrink-0 cursor-pointer rounded-lg border border-slate-300 bg-surface p-1"
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
id={id}
|
id={id}
|
||||||
@@ -95,7 +95,7 @@ export function DesignPanel({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
patch({ fgColor: preset.fgColor, bgColor: preset.bgColor })
|
patch({ fgColor: preset.fgColor, bgColor: preset.bgColor })
|
||||||
}
|
}
|
||||||
className="flex items-center gap-2 rounded-lg border border-slate-300 bg-white py-1 pr-2.5 pl-1.5 text-xs font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
className="flex items-center gap-2 rounded-lg border border-slate-300 bg-surface py-1 pr-2.5 pl-1.5 text-xs font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="size-4 rounded border border-slate-300"
|
className="size-4 rounded border border-slate-300"
|
||||||
@@ -111,7 +111,7 @@ export function DesignPanel({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
patch({ fgColor: design.bgColor, bgColor: design.fgColor })
|
patch({ fgColor: design.bgColor, bgColor: design.fgColor })
|
||||||
}
|
}
|
||||||
className="rounded-lg border border-slate-300 bg-white px-2.5 py-1 text-xs font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
className="rounded-lg border border-slate-300 bg-surface px-2.5 py-1 text-xs font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
||||||
>
|
>
|
||||||
Farben tauschen
|
Farben tauschen
|
||||||
</button>
|
</button>
|
||||||
@@ -158,7 +158,7 @@ export function DesignPanel({
|
|||||||
if (Number.isNaN(raw)) return;
|
if (Number.isNaN(raw)) return;
|
||||||
patch({ size: Math.min(MAX_SIZE, Math.max(MIN_SIZE, Math.round(raw))) });
|
patch({ size: Math.min(MAX_SIZE, Math.max(MIN_SIZE, Math.round(raw))) });
|
||||||
}}
|
}}
|
||||||
className="w-24 shrink-0 rounded-lg border border-slate-300 bg-white px-2.5 py-2 text-sm tabular-nums shadow-sm"
|
className="w-24 shrink-0 rounded-lg border border-slate-300 bg-surface px-2.5 py-2 text-sm tabular-nums shadow-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
@@ -182,8 +182,8 @@ export function DesignPanel({
|
|||||||
className={cx(
|
className={cx(
|
||||||
"rounded-lg border py-2 text-sm font-medium transition-colors",
|
"rounded-lg border py-2 text-sm font-medium transition-colors",
|
||||||
design.ecLevel === level
|
design.ecLevel === level
|
||||||
? "border-indigo-600 bg-indigo-50 text-indigo-700"
|
? "border-indigo-600 bg-accent-soft text-accent-soft-fg"
|
||||||
: "border-slate-300 bg-white text-slate-600 hover:bg-slate-50",
|
: "border-slate-300 bg-surface text-slate-600 hover:bg-slate-50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{level}
|
{level}
|
||||||
|
|||||||
@@ -214,6 +214,24 @@ const PATHS = {
|
|||||||
<path d="M4 12h16" />
|
<path d="M4 12h16" />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
sun: (
|
||||||
|
<>
|
||||||
|
<circle cx="12" cy="12" r="4" />
|
||||||
|
<path d="M12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2" />
|
||||||
|
<path d="m5.3 5.3 1.4 1.4M17.3 17.3l1.4 1.4M18.7 5.3l-1.4 1.4M6.7 17.3l-1.4 1.4" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
moon: (
|
||||||
|
<>
|
||||||
|
<path d="M20 14.3A8.5 8.5 0 0 1 9.7 4a8.5 8.5 0 1 0 10.3 10.3Z" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
monitor: (
|
||||||
|
<>
|
||||||
|
<rect x="2.5" y="4" width="19" height="13" rx="2" />
|
||||||
|
<path d="M9 21h6M12 17v4" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
lock: (
|
lock: (
|
||||||
<>
|
<>
|
||||||
<rect x="4.5" y="10.5" width="15" height="10.5" rx="2" />
|
<rect x="4.5" y="10.5" width="15" height="10.5" rx="2" />
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function FeedbackLanding({ data, codeId }: LandingProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LandingShell title={title}>
|
<LandingShell title={title}>
|
||||||
<div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
|
<div className="rounded-2xl border border-slate-200 bg-surface p-5 shadow-sm sm:p-6">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend className="text-center text-base font-medium text-slate-900">
|
<legend className="text-center text-base font-medium text-slate-900">
|
||||||
{question}
|
{question}
|
||||||
@@ -118,7 +118,7 @@ export function FeedbackLanding({ data, codeId }: LandingProps) {
|
|||||||
value={kommentar}
|
value={kommentar}
|
||||||
disabled={laeuft}
|
disabled={laeuft}
|
||||||
onChange={(event) => setKommentar(event.target.value)}
|
onChange={(event) => setKommentar(event.target.value)}
|
||||||
className="w-full rounded-lg border border-slate-300 bg-white px-3 py-2.5 text-sm text-slate-900 shadow-sm placeholder:text-slate-400"
|
className="w-full rounded-lg border border-slate-300 bg-surface px-3 py-2.5 text-sm text-slate-900 shadow-sm placeholder:text-slate-400"
|
||||||
placeholder="Optional"
|
placeholder="Optional"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function ImageLanding({ data }: LandingProps) {
|
|||||||
{images.map((image, index) => (
|
{images.map((image, index) => (
|
||||||
<figure
|
<figure
|
||||||
key={image.fileId}
|
key={image.fileId}
|
||||||
className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm"
|
className="overflow-hidden rounded-2xl border border-slate-200 bg-surface shadow-sm"
|
||||||
>
|
>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element -- eigene
|
{/* eslint-disable-next-line @next/next/no-img-element -- eigene
|
||||||
Auslieferung ohne bekannte Maße; der Optimierer von Next
|
Auslieferung ohne bekannte Maße; der Optimierer von Next
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function LinksLanding({ data }: LandingProps) {
|
|||||||
href={link.url}
|
href={link.url}
|
||||||
// Fremde Ziele: kein Zugriff auf dieses Fenster.
|
// Fremde Ziele: kein Zugriff auf dieses Fenster.
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-3 rounded-2xl border border-slate-200 bg-white px-5 py-4 text-slate-900 shadow-sm transition-colors hover:border-indigo-300 hover:bg-indigo-50/60"
|
className="flex items-center gap-3 rounded-2xl border border-slate-200 bg-surface px-5 py-4 text-slate-900 shadow-sm transition-colors hover:border-indigo-300 hover:bg-accent-soft/60"
|
||||||
>
|
>
|
||||||
<span className="min-w-0 flex-1 font-medium break-words">
|
<span className="min-w-0 flex-1 font-medium break-words">
|
||||||
{link.label}
|
{link.label}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function PdfLanding({ data }: LandingProps) {
|
|||||||
title={title || filename || "PDF-Dokument"}
|
title={title || filename || "PDF-Dokument"}
|
||||||
subtitle={description || null}
|
subtitle={description || null}
|
||||||
>
|
>
|
||||||
<div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
<div className="rounded-2xl border border-slate-200 bg-surface p-5 shadow-sm">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-red-50 text-red-600">
|
<span className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-red-50 text-red-600">
|
||||||
<Icon name="document" className="size-6" />
|
<Icon name="document" className="size-6" />
|
||||||
@@ -37,7 +37,7 @@ export function PdfLanding({ data }: LandingProps) {
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={`${href}?download`}
|
href={`${href}?download`}
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
className="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-surface px-4 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
||||||
>
|
>
|
||||||
<Icon name="download" className="size-4" />
|
<Icon name="download" className="size-4" />
|
||||||
Herunterladen
|
Herunterladen
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function SocialLanding({ data }: LandingProps) {
|
|||||||
<a
|
<a
|
||||||
href={profile.url}
|
href={profile.url}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-3.5 rounded-2xl border border-slate-200 bg-white px-4 py-3.5 text-slate-900 shadow-sm transition-colors hover:border-slate-300 hover:bg-slate-50"
|
className="flex items-center gap-3.5 rounded-2xl border border-slate-200 bg-surface px-4 py-3.5 text-slate-900 shadow-sm transition-colors hover:border-slate-300 hover:bg-slate-50"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function VideoLanding({ data }: LandingProps) {
|
|||||||
<a
|
<a
|
||||||
href={url}
|
href={url}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-3 rounded-2xl border border-slate-200 bg-white px-5 py-4 text-slate-900 shadow-sm transition-colors hover:border-indigo-300 hover:bg-indigo-50/60"
|
className="flex items-center gap-3 rounded-2xl border border-slate-200 bg-surface px-5 py-4 text-slate-900 shadow-sm transition-colors hover:border-indigo-300 hover:bg-accent-soft/60"
|
||||||
>
|
>
|
||||||
<span className="flex size-10 shrink-0 items-center justify-center rounded-full bg-slate-100 text-slate-600">
|
<span className="flex size-10 shrink-0 items-center justify-center rounded-full bg-slate-100 text-slate-600">
|
||||||
<Icon name="video" className="size-5" />
|
<Icon name="video" className="size-5" />
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ export function EmbeddedVideo({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setGestartet(true)}
|
onClick={() => setGestartet(true)}
|
||||||
className="group flex aspect-video w-full flex-col items-center justify-center gap-3 rounded-2xl border border-slate-300 bg-slate-900 px-6 text-center shadow-sm transition-colors hover:bg-slate-800"
|
className="group flex aspect-video w-full flex-col items-center justify-center gap-3 rounded-2xl border border-slate-300 bg-scrim px-6 text-center shadow-sm transition-colors hover:brightness-125"
|
||||||
>
|
>
|
||||||
<span className="flex size-16 items-center justify-center rounded-full bg-white/10 text-white transition-colors group-hover:bg-white/20">
|
<span className="flex size-16 items-center justify-center rounded-full bg-white/10 text-white transition-colors group-hover:bg-white/20">
|
||||||
<Icon name="video" className="size-8" />
|
<Icon name="video" className="size-8" />
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium text-white">Video abspielen</span>
|
<span className="font-medium text-white">Video abspielen</span>
|
||||||
<span className="max-w-xs text-xs leading-relaxed text-slate-300">
|
<span className="max-w-xs text-xs leading-relaxed text-white/75">
|
||||||
Beim Abspielen wird eine Verbindung zu {provider} hergestellt. Dabei
|
Beim Abspielen wird eine Verbindung zu {provider} hergestellt. Dabei
|
||||||
werden Daten an den Anbieter übertragen.
|
werden Daten an den Anbieter übertragen.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function Modal({
|
|||||||
// Klick auf die Hintergrundfläche des Dialogs schließt ihn.
|
// Klick auf die Hintergrundfläche des Dialogs schließt ihn.
|
||||||
if (event.target === ref.current) onClose();
|
if (event.target === ref.current) onClose();
|
||||||
}}
|
}}
|
||||||
className="m-auto w-[calc(100%-2rem)] max-w-lg rounded-2xl border border-slate-200 bg-white p-0 shadow-xl backdrop:bg-slate-900/40"
|
className="m-auto w-[calc(100%-2rem)] max-w-lg rounded-2xl border border-slate-200 bg-surface p-0 shadow-xl backdrop:bg-scrim/50"
|
||||||
>
|
>
|
||||||
<div className="p-5 sm:p-6">
|
<div className="p-5 sm:p-6">
|
||||||
<h2 className="text-lg font-semibold text-slate-900">{title}</h2>
|
<h2 className="text-lg font-semibold text-slate-900">{title}</h2>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function ImageForm({ value, onChange, error, disabled }: QrFormProps) {
|
|||||||
<img
|
<img
|
||||||
src={`/api/uploads/${image.fileId}`}
|
src={`/api/uploads/${image.fileId}`}
|
||||||
alt=""
|
alt=""
|
||||||
className="size-14 shrink-0 rounded-lg border border-slate-200 bg-white object-cover"
|
className="size-14 shrink-0 rounded-lg border border-slate-200 bg-surface object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0 flex-1 space-y-2">
|
<div className="min-w-0 flex-1 space-y-2">
|
||||||
<p className="truncate text-sm text-slate-600">
|
<p className="truncate text-sm text-slate-600">
|
||||||
@@ -95,7 +95,7 @@ export function ImageForm({ value, onChange, error, disabled }: QrFormProps) {
|
|||||||
disabled={disabled || index === 0}
|
disabled={disabled || index === 0}
|
||||||
onClick={() => move(index, -1)}
|
onClick={() => move(index, -1)}
|
||||||
aria-label={`Bild ${index + 1} nach oben`}
|
aria-label={`Bild ${index + 1} nach oben`}
|
||||||
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-white hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-surface hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
||||||
>
|
>
|
||||||
<Icon name="arrowLeft" className="size-4 rotate-90" />
|
<Icon name="arrowLeft" className="size-4 rotate-90" />
|
||||||
</button>
|
</button>
|
||||||
@@ -104,7 +104,7 @@ export function ImageForm({ value, onChange, error, disabled }: QrFormProps) {
|
|||||||
disabled={disabled || index === images.length - 1}
|
disabled={disabled || index === images.length - 1}
|
||||||
onClick={() => move(index, 1)}
|
onClick={() => move(index, 1)}
|
||||||
aria-label={`Bild ${index + 1} nach unten`}
|
aria-label={`Bild ${index + 1} nach unten`}
|
||||||
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-white hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-surface hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
||||||
>
|
>
|
||||||
<Icon name="arrowLeft" className="size-4 -rotate-90" />
|
<Icon name="arrowLeft" className="size-4 -rotate-90" />
|
||||||
</button>
|
</button>
|
||||||
@@ -115,7 +115,7 @@ export function ImageForm({ value, onChange, error, disabled }: QrFormProps) {
|
|||||||
setImages(images.filter((_, i) => i !== index))
|
setImages(images.filter((_, i) => i !== index))
|
||||||
}
|
}
|
||||||
aria-label={`Bild ${index + 1} entfernen`}
|
aria-label={`Bild ${index + 1} entfernen`}
|
||||||
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-white hover:text-red-600 disabled:opacity-40"
|
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-surface hover:text-red-600 disabled:opacity-40"
|
||||||
>
|
>
|
||||||
<Icon name="trash" className="size-4" />
|
<Icon name="trash" className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export function LinksForm({ value, onChange, error, disabled }: QrFormProps) {
|
|||||||
disabled={disabled || index === 0}
|
disabled={disabled || index === 0}
|
||||||
onClick={() => move(index, -1)}
|
onClick={() => move(index, -1)}
|
||||||
aria-label={`Link ${index + 1} nach oben`}
|
aria-label={`Link ${index + 1} nach oben`}
|
||||||
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-white hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-surface hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
||||||
>
|
>
|
||||||
<Icon name="arrowLeft" className="size-4 rotate-90" />
|
<Icon name="arrowLeft" className="size-4 rotate-90" />
|
||||||
</button>
|
</button>
|
||||||
@@ -134,7 +134,7 @@ export function LinksForm({ value, onChange, error, disabled }: QrFormProps) {
|
|||||||
disabled={disabled || index === links.length - 1}
|
disabled={disabled || index === links.length - 1}
|
||||||
onClick={() => move(index, 1)}
|
onClick={() => move(index, 1)}
|
||||||
aria-label={`Link ${index + 1} nach unten`}
|
aria-label={`Link ${index + 1} nach unten`}
|
||||||
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-white hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-surface hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
||||||
>
|
>
|
||||||
<Icon name="arrowLeft" className="size-4 -rotate-90" />
|
<Icon name="arrowLeft" className="size-4 -rotate-90" />
|
||||||
</button>
|
</button>
|
||||||
@@ -143,7 +143,7 @@ export function LinksForm({ value, onChange, error, disabled }: QrFormProps) {
|
|||||||
disabled={disabled || links.length === 1}
|
disabled={disabled || links.length === 1}
|
||||||
onClick={() => setLinks(links.filter((_, i) => i !== index))}
|
onClick={() => setLinks(links.filter((_, i) => i !== index))}
|
||||||
aria-label={`Link ${index + 1} entfernen`}
|
aria-label={`Link ${index + 1} entfernen`}
|
||||||
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-white hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-surface hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
||||||
>
|
>
|
||||||
<Icon name="trash" className="size-4" />
|
<Icon name="trash" className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function PdfForm({ value, onChange, error, disabled }: QrFormProps) {
|
|||||||
onChange({ ...value, fileId: "", filename: "", size: 0 })
|
onChange({ ...value, fileId: "", filename: "", size: 0 })
|
||||||
}
|
}
|
||||||
aria-label="Datei entfernen"
|
aria-label="Datei entfernen"
|
||||||
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-white hover:text-red-600 disabled:opacity-40"
|
className="rounded-md p-1.5 text-slate-500 transition-colors hover:bg-surface hover:text-red-600 disabled:opacity-40"
|
||||||
>
|
>
|
||||||
<Icon name="trash" className="size-4" />
|
<Icon name="trash" className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export function SocialForm({ value, onChange, error, disabled }: QrFormProps) {
|
|||||||
setProfiles(profiles.filter((_, i) => i !== index))
|
setProfiles(profiles.filter((_, i) => i !== index))
|
||||||
}
|
}
|
||||||
aria-label={`Profil ${index + 1} entfernen`}
|
aria-label={`Profil ${index + 1} entfernen`}
|
||||||
className="mt-1 rounded-md p-1.5 text-slate-500 transition-colors hover:bg-white hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
className="mt-1 rounded-md p-1.5 text-slate-500 transition-colors hover:bg-surface hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
||||||
>
|
>
|
||||||
<Icon name="trash" className="size-4" />
|
<Icon name="trash" className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function WebsiteForm({ value, onChange, error, disabled }: QrFormProps) {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
onChange({ ...value, url: `https://${url.trim().replace(/^\/+/, "")}` })
|
onChange({ ...value, url: `https://${url.trim().replace(/^\/+/, "")}` })
|
||||||
}
|
}
|
||||||
className="mt-2 rounded-lg border border-slate-300 bg-white px-2.5 py-1 text-xs font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
className="mt-2 rounded-lg border border-slate-300 bg-surface px-2.5 py-1 text-xs font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
||||||
>
|
>
|
||||||
https:// voranstellen
|
https:// voranstellen
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="mb-7 flex flex-wrap items-center gap-3">
|
<div className="mb-7 flex flex-wrap items-center gap-3">
|
||||||
<span className="flex size-11 items-center justify-center rounded-xl bg-indigo-50 text-indigo-600">
|
<span className="flex size-11 items-center justify-center rounded-xl bg-accent-soft text-accent-text">
|
||||||
<Icon name={qrType.icon} className="size-6" />
|
<Icon name={qrType.icon} className="size-6" />
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -290,8 +290,8 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
|||||||
className={cx(
|
className={cx(
|
||||||
"rounded-xl border p-4 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
"rounded-xl border p-4 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
!isDynamic
|
!isDynamic
|
||||||
? "border-indigo-600 bg-indigo-50/60"
|
? "border-indigo-600 bg-accent-soft/60"
|
||||||
: "border-slate-300 bg-white hover:bg-slate-50",
|
: "border-slate-300 bg-surface hover:bg-slate-50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2 font-medium text-slate-900">
|
<span className="flex items-center gap-2 font-medium text-slate-900">
|
||||||
@@ -314,8 +314,8 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
|||||||
className={cx(
|
className={cx(
|
||||||
"rounded-xl border p-4 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
"rounded-xl border p-4 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
isDynamic
|
isDynamic
|
||||||
? "border-indigo-600 bg-indigo-50/60"
|
? "border-indigo-600 bg-accent-soft/60"
|
||||||
: "border-slate-300 bg-white hover:bg-slate-50",
|
: "border-slate-300 bg-surface hover:bg-slate-50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2 font-medium text-slate-900">
|
<span className="flex items-center gap-2 font-medium text-slate-900">
|
||||||
@@ -353,7 +353,7 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
|||||||
<span className="font-medium">Code gespeichert.</span> Der
|
<span className="font-medium">Code gespeichert.</span> Der
|
||||||
QR-Code zeigt dauerhaft auf diese Kurz-URL:
|
QR-Code zeigt dauerhaft auf diese Kurz-URL:
|
||||||
<span className="mt-2 flex flex-wrap items-center gap-2">
|
<span className="mt-2 flex flex-wrap items-center gap-2">
|
||||||
<code className="rounded bg-white/70 px-2 py-1 font-mono text-xs break-all">
|
<code className="rounded bg-surface/70 px-2 py-1 font-mono text-xs break-all">
|
||||||
{saved.shortUrl}
|
{saved.shortUrl}
|
||||||
</code>
|
</code>
|
||||||
<Button
|
<Button
|
||||||
@@ -383,7 +383,7 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
|||||||
) : null}
|
) : null}
|
||||||
<Link
|
<Link
|
||||||
href="/meine-codes"
|
href="/meine-codes"
|
||||||
className="inline-flex items-center gap-1.5 rounded-lg text-sm font-medium text-indigo-600 hover:text-indigo-700"
|
className="inline-flex items-center gap-1.5 rounded-lg text-sm font-medium text-accent-text hover:text-accent-soft-fg"
|
||||||
>
|
>
|
||||||
Zu „Meine Codes“
|
Zu „Meine Codes“
|
||||||
<Icon name="external" className="size-3.5" />
|
<Icon name="external" className="size-3.5" />
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function QrPreview({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<span className="absolute top-3 right-3 rounded-full bg-white/90 px-2 py-0.5 text-xs font-medium text-slate-500 shadow-sm">
|
<span className="absolute top-3 right-3 rounded-full bg-surface/90 px-2 py-0.5 text-xs font-medium text-slate-500 shadow-sm">
|
||||||
aktualisiert …
|
aktualisiert …
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useSyncExternalStore } from "react";
|
||||||
|
|
||||||
|
import { Icon, type IconName } from "@/components/icons";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Umschalter für das Farbthema.
|
||||||
|
*
|
||||||
|
* Drei Zustände statt zwei: Wer „System“ wählt, folgt weiterhin dem
|
||||||
|
* Betriebssystem — abends dunkel, morgens hell, ohne Zutun. Ein reiner
|
||||||
|
* Ein/Aus-Schalter kann das nicht abbilden, weil er nach dem ersten Klick
|
||||||
|
* für immer eine feste Wahl bedeutet.
|
||||||
|
*
|
||||||
|
* Umgesetzt wird die Wahl allein über das Attribut `data-theme` am
|
||||||
|
* <html>-Element; die Farben stecken in `globals.css`. Ohne Attribut gilt die
|
||||||
|
* Systemeinstellung — deshalb wird bei „System“ nicht etwa „light“ gesetzt,
|
||||||
|
* sondern das Attribut entfernt.
|
||||||
|
*/
|
||||||
|
export const THEME_STORAGE_KEY = "qrgen.theme";
|
||||||
|
|
||||||
|
export type Theme = "system" | "light" | "dark";
|
||||||
|
|
||||||
|
const OPTIONEN: { wert: Theme; icon: IconName; label: string }[] = [
|
||||||
|
{ wert: "system", icon: "monitor", label: "System" },
|
||||||
|
{ wert: "light", icon: "sun", label: "Hell" },
|
||||||
|
{ wert: "dark", icon: "moon", label: "Dunkel" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function applyTheme(theme: Theme): void {
|
||||||
|
if (theme === "system") {
|
||||||
|
delete document.documentElement.dataset.theme;
|
||||||
|
} else {
|
||||||
|
document.documentElement.dataset.theme = theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Läuft vor dem ersten Zeichnen und verhindert das kurze Aufblitzen der
|
||||||
|
* hellen Fassung bei jemandem, der ausdrücklich „Dunkel“ gewählt hat.
|
||||||
|
*
|
||||||
|
* Bewusst winzig und ohne Abhängigkeiten: Alles, was hier steht, blockiert
|
||||||
|
* den Seitenaufbau. Wer der Systemeinstellung folgt, braucht das Skript gar
|
||||||
|
* nicht — dafür sorgt schon das CSS.
|
||||||
|
*/
|
||||||
|
export const THEME_INIT_SCRIPT = `try{var t=localStorage.getItem(${JSON.stringify(
|
||||||
|
THEME_STORAGE_KEY,
|
||||||
|
)});if(t==="dark"||t==="light")document.documentElement.dataset.theme=t}catch(e){}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Wahl liegt im `localStorage` — also außerhalb von React. Dafür ist
|
||||||
|
* `useSyncExternalStore` gedacht: Es liest den Wert beim Einhängen, ohne dass
|
||||||
|
* der Server-Aufbau davon abweicht, und benachrichtigt alle Stellen, wenn er
|
||||||
|
* sich ändert. Nebenbei zieht ein zweiter Tab automatisch nach.
|
||||||
|
*/
|
||||||
|
const zuhoerer = new Set<() => void>();
|
||||||
|
|
||||||
|
function abonnieren(melden: () => void): () => void {
|
||||||
|
zuhoerer.add(melden);
|
||||||
|
window.addEventListener("storage", melden);
|
||||||
|
return () => {
|
||||||
|
zuhoerer.delete(melden);
|
||||||
|
window.removeEventListener("storage", melden);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function lesen(): Theme {
|
||||||
|
try {
|
||||||
|
const wert = localStorage.getItem(THEME_STORAGE_KEY);
|
||||||
|
return wert === "dark" || wert === "light" ? wert : "system";
|
||||||
|
} catch {
|
||||||
|
return "system";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Auf dem Server ist nichts gespeichert — dort gilt die Systemeinstellung. */
|
||||||
|
function aufDemServer(): Theme {
|
||||||
|
return "system";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const theme = useSyncExternalStore(abonnieren, lesen, aufDemServer);
|
||||||
|
|
||||||
|
const waehlen = (wert: Theme) => {
|
||||||
|
applyTheme(wert);
|
||||||
|
try {
|
||||||
|
if (wert === "system") localStorage.removeItem(THEME_STORAGE_KEY);
|
||||||
|
else localStorage.setItem(THEME_STORAGE_KEY, wert);
|
||||||
|
} catch {
|
||||||
|
// Privater Modus ohne Speicher — die Wahl gilt dann bis zum Neuladen.
|
||||||
|
}
|
||||||
|
for (const melden of zuhoerer) melden();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
aria-label="Farbthema"
|
||||||
|
className="flex items-center gap-0.5 rounded-lg border border-slate-200 p-0.5"
|
||||||
|
>
|
||||||
|
{OPTIONEN.map((option) => {
|
||||||
|
const aktiv = theme === option.wert;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={option.wert}
|
||||||
|
type="button"
|
||||||
|
data-theme-option={option.wert}
|
||||||
|
aria-pressed={aktiv}
|
||||||
|
title={`Farbthema: ${option.label}`}
|
||||||
|
aria-label={`Farbthema: ${option.label}`}
|
||||||
|
onClick={() => waehlen(option.wert)}
|
||||||
|
className={
|
||||||
|
aktiv
|
||||||
|
? "rounded-md bg-accent-soft p-1.5 text-accent-soft-fg"
|
||||||
|
: "rounded-md p-1.5 text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-900"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon name={option.icon} className="size-4" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ function Toggle({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cx(
|
className={cx(
|
||||||
"inline-block size-5 rounded-full bg-white shadow-sm transition-transform",
|
"inline-block size-5 rounded-full bg-surface shadow-sm transition-transform",
|
||||||
checked ? "translate-x-5.5" : "translate-x-0.5",
|
checked ? "translate-x-5.5" : "translate-x-0.5",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -114,7 +114,7 @@ export function TypeSettings({ initial }: { initial: TypeSetting[] }) {
|
|||||||
className={cx(
|
className={cx(
|
||||||
"flex size-10 shrink-0 items-center justify-center rounded-xl",
|
"flex size-10 shrink-0 items-center justify-center rounded-xl",
|
||||||
entry.enabled && entry.implemented
|
entry.enabled && entry.implemented
|
||||||
? "bg-indigo-50 text-indigo-600"
|
? "bg-accent-soft text-accent-text"
|
||||||
: "bg-slate-100 text-slate-400",
|
: "bg-slate-100 text-slate-400",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function Card({
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={cx(
|
className={cx(
|
||||||
"rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6",
|
"rounded-2xl border border-slate-200 bg-surface p-5 shadow-sm sm:p-6",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -57,7 +57,7 @@ const BUTTON_VARIANTS: Record<NonNullable<ButtonProps["variant"]>, string> = {
|
|||||||
primary:
|
primary:
|
||||||
"bg-indigo-600 text-white hover:bg-indigo-700 disabled:hover:bg-indigo-600",
|
"bg-indigo-600 text-white hover:bg-indigo-700 disabled:hover:bg-indigo-600",
|
||||||
secondary:
|
secondary:
|
||||||
"border border-slate-300 bg-white text-slate-700 hover:bg-slate-50 disabled:hover:bg-white",
|
"border border-slate-300 bg-surface text-slate-700 hover:bg-slate-50 disabled:hover:bg-surface",
|
||||||
ghost: "text-slate-600 hover:bg-slate-100 disabled:hover:bg-transparent",
|
ghost: "text-slate-600 hover:bg-slate-100 disabled:hover:bg-transparent",
|
||||||
danger: "bg-red-600 text-white hover:bg-red-700 disabled:hover:bg-red-600",
|
danger: "bg-red-600 text-white hover:bg-red-700 disabled:hover:bg-red-600",
|
||||||
};
|
};
|
||||||
@@ -124,7 +124,7 @@ export function TextInput({
|
|||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
className={cx(
|
className={cx(
|
||||||
"w-full rounded-lg border bg-white px-3 py-2.5 text-sm text-slate-900 shadow-sm transition-colors placeholder:text-slate-400",
|
"w-full rounded-lg border bg-surface px-3 py-2.5 text-sm text-slate-900 shadow-sm transition-colors placeholder:text-slate-400",
|
||||||
invalid
|
invalid
|
||||||
? "border-red-400 focus-visible:outline-red-500"
|
? "border-red-400 focus-visible:outline-red-500"
|
||||||
: "border-slate-300",
|
: "border-slate-300",
|
||||||
@@ -137,7 +137,7 @@ export function TextInput({
|
|||||||
|
|
||||||
/** Gemeinsame Grundgestaltung aller Eingabefelder. */
|
/** Gemeinsame Grundgestaltung aller Eingabefelder. */
|
||||||
export const CONTROL_BASE =
|
export const CONTROL_BASE =
|
||||||
"w-full rounded-lg border bg-white px-3 py-2.5 text-sm text-slate-900 shadow-sm transition-colors placeholder:text-slate-400 disabled:cursor-not-allowed disabled:bg-slate-50 disabled:text-slate-500";
|
"w-full rounded-lg border bg-surface px-3 py-2.5 text-sm text-slate-900 shadow-sm transition-colors placeholder:text-slate-400 disabled:cursor-not-allowed disabled:bg-slate-50 disabled:text-slate-500";
|
||||||
|
|
||||||
export function TextArea({
|
export function TextArea({
|
||||||
invalid,
|
invalid,
|
||||||
@@ -243,7 +243,7 @@ export function Badge({
|
|||||||
neutral: "bg-slate-100 text-slate-600",
|
neutral: "bg-slate-100 text-slate-600",
|
||||||
// Hervorhebung — hieß früher "popular", als sie nur für das Abzeichen
|
// Hervorhebung — hieß früher "popular", als sie nur für das Abzeichen
|
||||||
// „Beliebt" auf der Startseite gedacht war.
|
// „Beliebt" auf der Startseite gedacht war.
|
||||||
accent: "bg-indigo-100 text-indigo-700",
|
accent: "bg-accent-soft text-accent-soft-fg",
|
||||||
soon: "bg-slate-100 text-slate-500",
|
soon: "bg-slate-100 text-slate-500",
|
||||||
success: "bg-emerald-100 text-emerald-700",
|
success: "bg-emerald-100 text-emerald-700",
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user