Preset b5vnF8SMi uebernommen
Some checks failed
CI / Backend (Go) (push) Failing after 30s
CI / Frontend (React/TypeScript) (push) Successful in 43s
CI / Sicherheitsprüfungen (push) Successful in 27s

Violett als Handlungsfarbe, Radius 0, schattenlos, durchgehend Geist Mono.
Kantig und ruhig — die praegenden Merkmale des Presets.

Drei begruendete Abweichungen, im Code festgehalten:

- **Die Statusfarben bleiben.** Das Preset kennt nur `destructive` und fuenf
  Diagrammfarben; PROMPT §106 verlangt fuenf Bedeutungen. Ohne sie liesse sich
  ein Teilfehler nicht von einem Erfolg unterscheiden — die eine Aussage, auf
  die es in dieser Konsole ankommt. Sie sind an die Neutraltoene des Presets
  angepasst; Rot stammt aus dessen `destructive`.
- **Das dunkle Thema haengt weiter an `[data-theme='dark']`.** Der Umschalter
  setzt dieses Attribut und ist getestet; `.dark` wird zusaetzlich unterstuetzt.
- **Die Schrift laedt nicht nach.** Geist Mono steht zuerst im Stapel und wird
  verwendet, wenn sie auf dem Geraet liegt; sonst greift die System-Monospace.
  Eine Schrift von einem fremden Host zu holen verbietet die CSP der
  Auslieferung — und ein Backup-Server, der fuer seine Oberflaeche ins Internet
  greift, waere auch ohne CSP falsch.

Eine Zuordnung war die Stolperstelle: In shadcn ist `accent` die dezente
Hover-Flaeche und `primary` die Farbe der Handlung. Sie zu verwechseln machte
jede Schaltflaeche grau. Das Preset-`primary` wird deshalb zu `--accent`, das
Preset-`accent` zu `--surface-hover`.

Die Benennung bleibt semantisch (`--surface-card`, `--text-primary`) statt
shadcn-typisch: Die Zuordnung steht an genau einer Stelle, und ein Wechsel des
Presets fasst keine einzige Komponente an.

Karten, Schaltflaechen, Dialoge, Toasts und die Anmeldemaske sind auf
schattenlos umgestellt; Dialog und Toast behalten einen kraeftigen Rand, weil
sie sonst ohne Trennung ueber der Seite schwebten — im dunklen Thema ist der
normale Rand schwarz. Die Seitenleiste bekommt die eigene Flaeche des Presets.

Nachgewiesen gegen Debian 12 mit nginx: alle 18 Seiten liefern 200, das
ausgelieferte CSS traegt Akzent, Radius 0 und Geist Mono, /retention zeigt
0 eigene Regeln und 6 anklickbare Vorlagen.

84 Tests gruen, tsc sauber, eslint ohne Warnung. CSS 28,7 KB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jerrit Fritzsche 2026-08-18 15:07:55 +02:00
parent 77e61dc2b3
commit fef0bd88f7
8 changed files with 162 additions and 140 deletions

View File

@ -87,7 +87,7 @@ export function AppShell({
<aside
className={cn(
'fixed inset-y-0 left-0 z-40 flex flex-col border-r border-line bg-card transition-[width,transform]',
'fixed inset-y-0 left-0 z-40 flex flex-col border-r border-line bg-sidebar transition-[width,transform]',
isSidebarCollapsed ? 'w-16' : 'w-60',
isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
)}

View File

@ -24,7 +24,7 @@ const buttonVariants = cva(
{
variants: {
variant: {
primary: 'bg-accent text-accent-fg shadow-[var(--shadow-card)] hover:bg-accent-hover',
primary: 'bg-accent text-accent-fg hover:bg-accent-hover',
secondary: 'border border-line-strong bg-card text-fg hover:bg-hover',
ghost: 'text-fg-muted hover:bg-hover hover:text-fg',
destructive:

View File

@ -21,7 +21,7 @@ export function Card({
return (
<RenderedComponent
className={cn('rounded-xl border border-line bg-card shadow-[var(--shadow-card)]', className)}
className={cn('border border-line bg-card', className)}
{...restProps}
/>
);

View File

@ -46,7 +46,7 @@ export function DialogContent({
className={cn(
'fixed left-1/2 top-1/2 z-50 w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2',
'max-h-[calc(100vh-4rem)] overflow-y-auto',
'rounded-xl border border-line bg-card shadow-[var(--shadow-raised)]',
'border border-line-strong bg-card',
sizeClasses[size],
className,
)}

View File

@ -177,7 +177,7 @@ export function CheckboxField({
checked={checked}
disabled={disabled}
onChange={(changeEvent) => onCheckedChange(changeEvent.target.checked)}
className="mt-0.5 size-4 shrink-0 rounded border-line-strong accent-[hsl(var(--accent))]"
className="mt-0.5 size-4 shrink-0 rounded border-line-strong accent-[var(--accent)]"
/>
<div className="min-w-0">
<label

View File

@ -140,7 +140,7 @@ function ToastCard({
return (
<div
className={cn(
'pointer-events-auto rounded-lg border border-l-4 border-line bg-raised p-3 shadow-lg',
'pointer-events-auto border border-l-4 border-line-strong bg-raised p-3',
toneClasses[message.tone],
)}
>

View File

@ -106,7 +106,7 @@ export function LoginPage({ onAuthenticated }: LoginPageProps): React.JSX.Elemen
return (
<div className="flex min-h-screen items-center justify-center bg-page p-4">
<main className="w-full max-w-sm rounded-lg border border-line bg-card p-6 shadow-sm">
<main className="w-full max-w-sm border border-line-strong bg-card p-6">
<h1 className="text-lg font-semibold text-fg">Syncova</h1>
{loginStep === 'credentials' ? (

View File

@ -1,168 +1,189 @@
/*
* Design-System der Syncova-Oberflaeche (Tailwind v4 + shadcn/ui).
* Design-System der Syncova-Oberfläche.
*
* Der Grundsatz aus PROMPT.md §106 gilt unveraendert und ist der Grund fuer den
* Zuschnitt dieser Datei: Semantische Farben ausschliesslich fuer Status. Die
* gesamte Oberflaeche ist neutral gehalten; wo Farbe erscheint, bedeutet sie
* etwas. Eine bunte Oberflaeche verschleiert, welche Information wirklich
* dringend ist — und in einer Backup-Konsole ist genau das die einzige Frage,
* die zaehlt.
* Die Farben, Radien, Schatten und die Schrift stammen aus dem Preset
* `b5vnF8SMi` (tweakcn). Es ist bewusst kantig (Radius 0), schattenlos und
* durchgehend in einer dicktengleichen Schrift — ein technischer, ruhiger Look.
*
* Deshalb gibt es hier keine "Markenfarbe" als Flaeche. Der Akzent ist ein
* gedecktes Blau und wird nur fuer Bedienelemente verwendet, nie fuer Zustaende.
* Drei Stellen weichen ab, und zwar begründet:
*
* 1. **Die Statusfarben bleiben.** Das Preset kennt nur `destructive` und fünf
* Diagrammfarben, aber PROMPT §106 verlangt fünf Bedeutungen: Grün, Gelb,
* Orange, Rot, Blau. Ohne sie ließe sich ein Teilfehler nicht von einem
* Erfolg unterscheiden — die eine Aussage, auf die es in dieser Konsole
* ankommt. Sie sind an die Neutraltöne des Presets angepasst.
* 2. **Das dunkle Thema hängt an `[data-theme='dark']`, nicht an `.dark`.** Der
* Umschalter der Konsole setzt dieses Attribut, und er ist getestet. Beide
* Schreibweisen werden unterstützt.
* 3. **Die Schrift lädt nicht nach.** Geist Mono steht zuerst im Stapel und
* wird verwendet, wenn sie auf dem Gerät liegt; sonst greift die
* System-Monospace. Eine Schrift von einem fremden Host zu holen verbietet
* die Content-Security-Policy der Auslieferung — und ein Backup-Server, der
* für seine Oberfläche ins Internet greift, wäre auch ohne CSP falsch.
*
* Die Benennung ist bewusst semantisch (`--surface-card`, `--text-primary`)
* statt shadcn-typisch (`--card`, `--foreground`): Die Zuordnung des Presets
* steht damit an genau einer Stelle, und ein Wechsel des Presets fasst keine
* einzige Komponente an.
*/
@import 'tailwindcss';
/* Dark Mode wird ueber ein Attribut geschaltet, nicht ueber die Medienabfrage
* allein: Der Betreiber soll die Wahl haben, und eine Konsole, die nachts von
* selbst umschaltet, waehrend jemand an einer Stoerung arbeitet, ist laestig. */
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
/* Beide Schreibweisen: Der Umschalter setzt `data-theme`, `.dark` ist die
* shadcn-übliche Form und kostet nichts. */
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *, .dark, .dark *));
@theme {
/* --- Schrift --- */
--font-sans:
system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
--font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
/* Geist Mono für alles — die Wahl des Presets. In einer Konsole voller
* Zahlen, Pfade und Kennungen ist das kein Stilbruch, sondern hilfreich:
* Ziffern stehen untereinander, und ein Pfad bricht dort, wo er soll. */
--font-sans: 'Geist Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
--font-mono: 'Geist Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
/* --- Radien --- */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
/* Radius 0 — das prägendste Merkmal des Presets. */
--radius-sm: 0;
--radius-md: 0;
--radius-lg: 0;
--radius-xl: 0;
--radius-2xl: 0;
/* Schatten statt Rahmen, wo Tiefe gemeint ist. Ein Rahmen trennt, ein
* Schatten hebt — Karten sollen heben, Tabellenzeilen trennen. */
--shadow-card: 0 1px 2px 0 hsl(240 20% 12% / 0.04), 0 1px 3px 0 hsl(240 20% 12% / 0.06);
--shadow-raised: 0 4px 12px -2px hsl(240 20% 12% / 0.10), 0 2px 4px -2px hsl(240 20% 12% / 0.06);
/* Das Preset ist schattenlos. Tiefe entsteht über Flächenkontrast. */
--shadow-card: none;
--shadow-raised: none;
}
/*
* Die Farben liegen als CSS-Variablen vor, damit dieselbe Komponente in beiden
* Themen funktioniert, ohne dass jede Klasse eine dark:-Variante braucht.
*/
:root,
[data-theme='light'] {
/* Flaechen — vier Ebenen reichen: Seite, Karte, erhoben, Rand. */
--surface-page: 240 20% 97%;
--surface-card: 0 0% 100%;
--surface-raised: 0 0% 100%;
--surface-sunken: 240 20% 96%;
--surface-hover: 240 20% 94%;
[data-theme='light'],
.light {
/* --- Flächen --- */
--surface-page: oklch(1 0 0);
--surface-card: oklch(1 0 0);
--surface-raised: oklch(1 0 0);
--surface-sunken: oklch(0.9702 0 0);
--surface-hover: oklch(0.9702 0 0);
/* Die Seitenleiste hat im Preset eine eigene, leicht abgesetzte Fläche. */
--surface-sidebar: oklch(0.9851 0 0);
--border-default: 240 12% 90%;
--border-strong: 240 10% 80%;
--border-default: oklch(0.9219 0 0);
--border-strong: oklch(0.709 0 0);
--text-primary: 240 20% 12%;
--text-secondary: 240 8% 44%;
--text-muted: 240 6% 58%;
--text-inverted: 0 0% 100%;
--text-primary: oklch(0.1448 0 0);
--text-secondary: oklch(0.5555 0 0);
--text-muted: oklch(0.709 0 0);
--text-inverted: oklch(0.9691 0.0161 293.7558);
/* Akzent — Bedienelemente, niemals Zustand. */
--accent: 243 75% 58%;
--accent-hover: 243 75% 50%;
--accent-text: 0 0% 100%;
--accent-subtle: 243 75% 96%;
/* Akzent = `primary` des Presets, nicht dessen `accent`.
*
* In shadcn ist `accent` die dezente Hover-Fläche und `primary` die Farbe der
* Handlung. Die beiden zu verwechseln machte jede Schaltfläche grau. */
--accent: oklch(0.4907 0.2699 292.5633);
--accent-hover: oklch(0.4316 0.232 292.7118);
--accent-text: oklch(0.9691 0.0161 293.7558);
--accent-subtle: oklch(0.9674 0.0013 286.3752);
/* --- Statusfarben (PROMPT.md §106) ---
* Gruen = Healthy, Gelb = Warning, Orange = High, Rot = Critical,
* Blau/Neutral = Information. Sonst nichts. */
--status-healthy: 142 72% 29%;
--status-healthy-bg: 142 60% 95%;
--status-warning: 38 92% 33%;
--status-warning-bg: 45 90% 94%;
--status-high: 21 90% 40%;
--status-high-bg: 24 90% 95%;
--status-critical: 0 74% 42%;
--status-critical-bg: 0 85% 96%;
--status-info: 221 83% 45%;
--status-info-bg: 214 95% 96%;
--status-neutral: 240 4% 42%;
--status-neutral-bg: 240 5% 95%;
/* --- Statusfarben (PROMPT §106) ---
* Grün = Healthy, Gelb = Warning, Orange = High, Rot = Critical,
* Blau/Neutral = Information. Rot stammt aus `destructive` des Presets. */
--status-healthy: oklch(0.52 0.14 152);
--status-healthy-bg: oklch(0.96 0.03 152);
--status-warning: oklch(0.55 0.12 85);
--status-warning-bg: oklch(0.96 0.04 85);
--status-high: oklch(0.56 0.17 45);
--status-high-bg: oklch(0.96 0.04 45);
--status-critical: oklch(0.583 0.2387 28.4765);
--status-critical-bg: oklch(0.96 0.03 28);
--status-info: oklch(0.5405 0.2811 292.8523);
--status-info-bg: oklch(0.96 0.02 293);
--status-neutral: oklch(0.5555 0 0);
--status-neutral-bg: oklch(0.9702 0 0);
--focus-ring: 221 83% 53%;
--focus-ring: oklch(0.4907 0.2699 292.5633);
}
[data-theme='dark'] {
--surface-page: 240 15% 7%;
--surface-card: 240 13% 10%;
--surface-raised: 240 12% 13%;
--surface-sunken: 240 16% 5%;
--surface-hover: 240 12% 16%;
[data-theme='dark'],
.dark {
--surface-page: oklch(0.1448 0 0);
--surface-card: oklch(0.2046 0 0);
--surface-raised: oklch(0.2046 0 0);
--surface-sunken: oklch(0.2686 0 0);
--surface-hover: oklch(0.2686 0 0);
--surface-sidebar: oklch(0.2046 0 0);
--border-default: 240 8% 20%;
--border-strong: 240 8% 30%;
/* Das Preset setzt im dunklen Thema reines Schwarz als Rand: Flächen trennen
* sich über ihre Helligkeit, nicht über Linien. Übernommen. */
--border-default: oklch(0 0 0);
--border-strong: oklch(0.3 0 0);
--text-primary: 240 20% 97%;
--text-secondary: 240 8% 70%;
--text-muted: 240 6% 54%;
--text-inverted: 240 15% 7%;
--text-primary: oklch(0.9851 0 0);
--text-secondary: oklch(0.709 0 0);
--text-muted: oklch(0.5555 0 0);
--text-inverted: oklch(0.9691 0.0161 293.7558);
--accent: 243 80% 70%;
--accent-hover: 243 80% 78%;
--accent-text: 240 15% 7%;
--accent-subtle: 243 45% 20%;
--accent: oklch(0.6006 0.2425 293.8885);
--accent-hover: oklch(0.8101 0.1055 293.6966);
--accent-text: oklch(0.1448 0 0);
--accent-subtle: oklch(0.2739 0.0055 286.0326);
/* Im dunklen Thema werden die Statusfarben aufgehellt. Die dunklen Toene des
* hellen Themas waeren auf dunklem Grund nicht mehr unterscheidbar — und ein
* nicht unterscheidbarer Statuston ist schlimmer als gar keiner. */
--status-healthy: 142 65% 55%;
--status-healthy-bg: 142 45% 15%;
--status-warning: 45 90% 60%;
--status-warning-bg: 40 50% 15%;
--status-high: 25 92% 60%;
--status-high-bg: 22 55% 15%;
--status-critical: 0 85% 65%;
--status-critical-bg: 0 50% 16%;
--status-info: 217 90% 65%;
--status-info-bg: 220 50% 17%;
--status-neutral: 240 5% 65%;
--status-neutral-bg: 240 5% 18%;
/* Aufgehellt, sonst wären die Töne auf dunklem Grund nicht unterscheidbar —
* und ein nicht unterscheidbarer Statuston ist schlimmer als gar keiner. */
--status-healthy: oklch(0.75 0.16 152);
--status-healthy-bg: oklch(0.28 0.05 152);
--status-warning: oklch(0.82 0.15 85);
--status-warning-bg: oklch(0.29 0.05 85);
--status-high: oklch(0.75 0.16 45);
--status-high-bg: oklch(0.29 0.06 45);
--status-critical: oklch(0.7022 0.1892 22.2279);
--status-critical-bg: oklch(0.29 0.07 22);
--status-info: oklch(0.8101 0.1055 293.6966);
--status-info-bg: oklch(0.29 0.06 293);
--status-neutral: oklch(0.709 0 0);
--status-neutral-bg: oklch(0.2686 0 0);
--focus-ring: 217 90% 62%;
--focus-ring: oklch(0.6006 0.2425 293.8885);
}
/* Die Variablen werden Tailwind als Farben bekannt gemacht, damit
* bg-surface-card, text-status-critical usw. unmittelbar funktionieren. */
/* Die Variablen werden Tailwind als Farben bekannt gemacht, damit `bg-card`,
* `text-fg` und `text-critical` unmittelbar funktionieren. */
@theme inline {
--color-page: hsl(var(--surface-page));
--color-card: hsl(var(--surface-card));
--color-raised: hsl(var(--surface-raised));
--color-sunken: hsl(var(--surface-sunken));
--color-hover: hsl(var(--surface-hover));
--color-page: var(--surface-page);
--color-card: var(--surface-card);
--color-raised: var(--surface-raised);
--color-sunken: var(--surface-sunken);
--color-hover: var(--surface-hover);
--color-sidebar: var(--surface-sidebar);
--color-line: hsl(var(--border-default));
--color-line-strong: hsl(var(--border-strong));
--color-line: var(--border-default);
--color-line-strong: var(--border-strong);
--color-fg: hsl(var(--text-primary));
--color-fg-muted: hsl(var(--text-secondary));
--color-fg-subtle: hsl(var(--text-muted));
--color-fg-inverted: hsl(var(--text-inverted));
--color-fg: var(--text-primary);
--color-fg-muted: var(--text-secondary);
--color-fg-subtle: var(--text-muted);
--color-fg-inverted: var(--text-inverted);
--color-accent: hsl(var(--accent));
--color-accent-hover: hsl(var(--accent-hover));
--color-accent-fg: hsl(var(--accent-text));
--color-accent-subtle: hsl(var(--accent-subtle));
--color-accent: var(--accent);
--color-accent-hover: var(--accent-hover);
--color-accent-fg: var(--accent-text);
--color-accent-subtle: var(--accent-subtle);
--color-healthy: hsl(var(--status-healthy));
--color-healthy-bg: hsl(var(--status-healthy-bg));
--color-warning: hsl(var(--status-warning));
--color-warning-bg: hsl(var(--status-warning-bg));
--color-high: hsl(var(--status-high));
--color-high-bg: hsl(var(--status-high-bg));
--color-critical: hsl(var(--status-critical));
--color-critical-bg: hsl(var(--status-critical-bg));
--color-info: hsl(var(--status-info));
--color-info-bg: hsl(var(--status-info-bg));
--color-neutral: hsl(var(--status-neutral));
--color-neutral-bg: hsl(var(--status-neutral-bg));
--color-healthy: var(--status-healthy);
--color-healthy-bg: var(--status-healthy-bg);
--color-warning: var(--status-warning);
--color-warning-bg: var(--status-warning-bg);
--color-high: var(--status-high);
--color-high-bg: var(--status-high-bg);
--color-critical: var(--status-critical);
--color-critical-bg: var(--status-critical-bg);
--color-info: var(--status-info);
--color-info-bg: var(--status-info-bg);
--color-neutral: var(--status-neutral);
--color-neutral-bg: var(--status-neutral-bg);
--color-ring: hsl(var(--focus-ring));
--color-ring: var(--focus-ring);
}
@layer base {
* {
border-color: hsl(var(--border-default));
border-color: var(--border-default);
}
html {
@ -172,21 +193,22 @@
}
body {
background-color: hsl(var(--surface-page));
color: hsl(var(--text-primary));
background-color: var(--surface-page);
color: var(--text-primary);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
}
/* Der Fokusring bleibt sichtbar. Ihn zu entfernen macht die Konsole fuer
* Tastaturbedienung unbrauchbar — und an einer Stoerung arbeitet niemand mit
/* Der Fokusring bleibt sichtbar. Ihn zu entfernen macht die Konsole für
* Tastaturbedienung unbrauchbar — und an einer Störung arbeitet niemand mit
* der Maus. */
:focus-visible {
outline: 2px solid hsl(var(--focus-ring));
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
/* Zahlen in Tabellen sollen untereinander stehen. */
/* Bei einer dicktengleichen Schrift stehen Ziffern ohnehin untereinander;
* die Klasse bleibt, damit die Aufrufstellen unverändert gelten. */
.tabular {
font-variant-numeric: tabular-nums;
}