From 3c62a1a29c7cc7bf7cc99ec969d386fa3dfa47e5 Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 25 Sep 2026 14:32:37 +0200 Subject: [PATCH] style(web): Design-Tokens Mosaik (Fluent 2 / GitHub) Kuehlgraue Arbeitsflaeche, weisse Karten mit Fluent-Schatten statt Rahmen, dunkle App-Leiste, Link-Blau, neutraler Fokusring, 6/8/12 px Radien, Segoe-UI-Schriftstapel, Knopf-/Link-Klassen, Fluent-Eingabefeld und reduzierte Bewegung. Die persoenliche Akzentfarbe setzt nur noch --primary. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/web/src/app/globals.css | 298 ++++++++++++++++++++++---- apps/web/src/lib/stores/auth-store.ts | 22 +- 2 files changed, 264 insertions(+), 56 deletions(-) diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index ac5a1b5..1e60bc2 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -57,45 +57,85 @@ --color-status-idle-fg: var(--status-idle-fg); --color-status-orphan-fg: var(--status-orphan-fg); --color-well: var(--well); + --color-link: var(--link); + --color-primary-strong: var(--primary-strong); + --color-input-strong: var(--input-strong); + --color-appbar: var(--appbar); + --color-appbar-foreground: var(--appbar-foreground); + --color-appbar-muted: var(--appbar-muted); + --color-appbar-hover: var(--appbar-hover); + --color-sidebar-hover: var(--sidebar-hover); --radius-sm: calc(var(--radius) - 2px); --radius-md: var(--radius); --radius-lg: calc(var(--radius) + 2px); } +/* Fluent-2-Hoehenstufen: shadow-sm = shadow2 (ruhende Karte), shadow-md = + shadow4 (angehobene/Hover-Karte), shadow-lg = shadow16 (Menues, Dialoge). + Im dunklen Erscheinungsbild tragen Haarlinien statt Schatten (--border). */ +@theme { + --shadow-sm: 0 0 2px oklch(0 0 0 / 0.12), 0 1px 2px oklch(0 0 0 / 0.14); + --shadow-md: 0 0 2px oklch(0 0 0 / 0.12), 0 4px 8px oklch(0 0 0 / 0.14); + --shadow-lg: 0 0 2px oklch(0 0 0 / 0.12), 0 8px 16px oklch(0 0 0 / 0.14); + --shadow-xl: 0 0 8px oklch(0 0 0 / 0.12), 0 14px 28px oklch(0 0 0 / 0.14); +} + :root { - --radius: 0.5rem; - --font-sans: "Inter", system-ui, -apple-system, sans-serif; + /* Design „Mosaik“ (Fluent 2 / GitHub als Vorbild): 6 px fuer Bedien- + elemente (rounded-md), 8 px fuer Karten (rounded-lg), 12 px fuer grosse + Flaechen (rounded-xl). */ + --radius: 0.375rem; + /* Segoe UI Variable zuerst: unter Windows (Desktop-Client) wirkt die + Oberflaeche damit wie eine native Anwendung; sonst Inter bzw. Systemschrift. */ + --font-sans: "Segoe UI Variable Text", "Segoe UI", Inter, system-ui, -apple-system, sans-serif; /* Layout dimensions */ --sidebar-width: 240px; --sidebar-width-collapsed: 64px; - --header-height: 60px; + --header-height: 48px; - /* Light mode colors (OKLCH) */ - --background: oklch(0.99 0 0); - --foreground: oklch(0.15 0 0); + /* Light mode colors (OKLCH) — Arbeitsflaeche leicht kuehlgrau, Karten weiss */ + --background: oklch(0.972 0.003 260); + --foreground: oklch(0.2 0.006 260); --card: oklch(1 0 0); - --card-foreground: oklch(0.15 0 0); + --card-foreground: oklch(0.2 0.006 260); --primary: oklch(0.91 0.19 102); --primary-foreground: oklch(0.20 0.02 90); - --secondary: oklch(0.96 0 0); - --secondary-foreground: oklch(0.15 0 0); - --muted: oklch(0.96 0 0); - --muted-foreground: oklch(0.55 0 0); - --accent: oklch(0.96 0 0); - --accent-foreground: oklch(0.15 0 0); + --secondary: oklch(0.955 0.003 260); + --secondary-foreground: oklch(0.2 0.006 260); + --muted: oklch(0.955 0.003 260); + --muted-foreground: oklch(0.49 0.01 260); + --accent: oklch(0.955 0.003 260); + --accent-foreground: oklch(0.2 0.006 260); --destructive: oklch(0.55 0.2 27); --destructive-foreground: oklch(0.98 0 0); - --border: oklch(0.90 0 0); - --input: oklch(0.90 0 0); - --ring: oklch(0.91 0.19 102); + --border: oklch(0.915 0.004 260); + --input: oklch(0.87 0 0); + /* Untere Kante von Eingabefeldern (Fluent-Eingabefeld). */ + --input-strong: oklch(0.58 0.005 260); + /* Tastaturfokus: dunkler Ring statt Gelb — Gelb auf Weiss erreicht nur + 1,3:1 und waere als Fokusanzeige kaum sichtbar. */ + --ring: oklch(0.3 0.01 260); + /* Verweise: nie gelbe Schrift auf Weiss. 5,9:1 auf --card. */ + --link: oklch(0.50 0.15 255); + /* Akzent fuer duenne Markierungen (Auswahlpille, Reiter-Unterstrich) auf + hellem Grund: die Akzentfarbe um knapp ein Drittel abgedunkelt, damit + auch Gelb neben Hellgrau noch erkennbar bleibt. */ + --primary-strong: color-mix(in oklab, var(--primary) 70%, oklch(0.3 0.03 90)); + + /* Kopfzeile (dunkle App-Leiste wie GitHub / Microsoft 365) */ + --appbar: oklch(0.24 0.012 260); + --appbar-foreground: oklch(0.985 0 0); + --appbar-muted: oklch(0.985 0 0 / 0.72); + --appbar-hover: oklch(1 0 0 / 0.08); /* Sidebar tokens */ - --sidebar: oklch(0.97 0 0); - --sidebar-foreground: oklch(0.15 0 0); - --sidebar-accent: oklch(0.93 0 0); - --sidebar-accent-foreground: oklch(0.25 0 0); - --sidebar-border: oklch(0.90 0 0); + --sidebar: oklch(0.975 0.003 260); + --sidebar-foreground: oklch(0.2 0.006 260); + --sidebar-accent: oklch(0.925 0.005 260); + --sidebar-accent-foreground: oklch(0.16 0.006 260); + --sidebar-hover: oklch(0.945 0.004 260); + --sidebar-border: oklch(0.915 0.004 260); /* Statusfarben (quick-260924-h7x). Die Flaechentoene stammen aus dem Design-Plan; als Schrift auf Weiss erreichen sie aber nur 2,6:1 bis @@ -113,34 +153,44 @@ --status-idle-fg: oklch(0.50 0.02 260); --status-orphan-fg: oklch(0.50 0.01 260); /* Eingelassene Messfelder. */ - --well: oklch(0.975 0.003 260); + --well: oklch(0.972 0.003 260); } .dark { - --background: oklch(0.23 0.01 260); - --foreground: oklch(0.95 0 0); - --card: oklch(0.27 0.01 260); - --card-foreground: oklch(0.95 0 0); + --background: oklch(0.2 0.01 260); + --foreground: oklch(0.95 0.003 260); + --card: oklch(0.245 0.01 260); + --card-foreground: oklch(0.95 0.003 260); --primary: oklch(0.91 0.19 102); --primary-foreground: oklch(0.20 0.02 90); - --secondary: oklch(0.30 0.01 260); - --secondary-foreground: oklch(0.95 0 0); - --muted: oklch(0.30 0.01 260); - --muted-foreground: oklch(0.65 0 0); - --accent: oklch(0.30 0.01 260); - --accent-foreground: oklch(0.95 0 0); - --destructive: oklch(0.55 0.2 27); + --secondary: oklch(0.28 0.01 260); + --secondary-foreground: oklch(0.95 0.003 260); + --muted: oklch(0.28 0.01 260); + --muted-foreground: oklch(0.72 0.01 260); + --accent: oklch(0.28 0.01 260); + --accent-foreground: oklch(0.95 0.003 260); + --destructive: oklch(0.6 0.2 27); --destructive-foreground: oklch(0.98 0 0); - --border: oklch(0.30 0.01 260); - --input: oklch(0.30 0.01 260); - --ring: oklch(0.91 0.19 102); + /* Dunkel: feine helle Haarlinie statt Schatten. */ + --border: oklch(1 0 0 / 0.09); + --input: oklch(1 0 0 / 0.16); + --input-strong: oklch(1 0 0 / 0.42); + --ring: oklch(0.92 0.005 260); + --link: oklch(0.78 0.11 250); + --primary-strong: var(--primary); + + --appbar: oklch(0.18 0.01 260); + --appbar-foreground: oklch(0.985 0 0); + --appbar-muted: oklch(0.985 0 0 / 0.7); + --appbar-hover: oklch(1 0 0 / 0.08); /* Sidebar tokens (dark) */ - --sidebar: oklch(0.20 0.01 260); - --sidebar-foreground: oklch(0.95 0 0); - --sidebar-accent: oklch(0.91 0.19 102 / 0.15); - --sidebar-accent-foreground: oklch(0.91 0.19 102); - --sidebar-border: oklch(0.30 0.01 260); + --sidebar: oklch(0.21 0.01 260); + --sidebar-foreground: oklch(0.93 0.003 260); + --sidebar-accent: oklch(1 0 0 / 0.08); + --sidebar-accent-foreground: oklch(0.98 0 0); + --sidebar-hover: oklch(1 0 0 / 0.05); + --sidebar-border: oklch(1 0 0 / 0.08); /* Statusfarben (dark). Textvariante auf --card gemessen: ok 7,4:1, warn 8,5:1, down 6,1:1, idle 6,1:1, orphan 5,6:1. */ @@ -154,11 +204,13 @@ --status-down-fg: oklch(0.74 0.16 27); --status-idle-fg: oklch(0.72 0.02 260); --status-orphan-fg: oklch(0.70 0.01 260); - --well: oklch(0.235 0.01 260); + --well: oklch(0.215 0.01 260); } body { font-family: var(--font-sans); + font-size: 0.875rem; + line-height: 1.25rem; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -181,3 +233,165 @@ body { .wmde-markdown ul ul { list-style: circle; } .wmde-markdown ol { list-style: decimal; } .wmde-markdown ul.contains-task-list, .wmde-markdown li.task-list-item { list-style: none; } + +/* Tastaturfokus sichtbar ueberall, auch wo ein Bauteil keinen eigenen + Fokusring setzt (im Basis-Layer, damit `outline-none` + eigener Ring + eines Bauteils Vorrang behalten). Mausklicks loesen :focus-visible nicht aus. */ +@layer base { + :where(a, button, [role="tab"], [role="button"], summary, [tabindex]):focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + } +} + +/* Fluent-Eingabefeld: untere Kante eine Stufe dunkler; Fokus = dunklerer + Rahmen plus 2 px Akzentlinie unten. Gilt fuer Textfelder und Auswahllisten + mit dem ueblichen `border-input`/`border-border`-Rahmen; Fehlerzustaende + (aria-invalid, border-destructive) behalten ihre Farbe. Bewusst ausserhalb + eines @layer, damit die Regel die Tailwind-Utilities schlaegt. */ +:is(input:not([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="file"]), select, textarea):is(.border-input, .border-border):not([aria-invalid="true"], .border-destructive) { + border-bottom-color: var(--input-strong); +} +:is(input:not([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="file"]), select, textarea):is(.border-input, .border-border):not([aria-invalid="true"], .border-destructive):focus { + outline: none; + border-color: var(--input-strong); + border-bottom-color: var(--primary); + box-shadow: inset 0 -1px 0 var(--primary); +} + +/* Karteneffekt in einer Klasse fuer neue Flaechen: hell Schatten, dunkel Haarlinie. */ +.surface { + background: var(--card); + border-radius: 0.5rem; + box-shadow: var(--shadow-sm); +} +.dark .surface { + box-shadow: none; + outline: 1px solid var(--border); + outline-offset: -1px; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +/* Knoepfe (Design „Mosaik“, Fluent 2): 32 px hoch, 6 px Radius, 14 px + halbfett. Primaer = Akzentflaeche mit dunkler Schrift, Sekundaer = weisse + Flaeche mit Rahmen, Subtil = ohne Flaeche. Im Komponenten-Layer, damit + einzelne Utilities (etwa `w-full`) weiter ueberschreiben koennen. */ +@layer components { + .btn { + display: inline-flex; + height: 2rem; + align-items: center; + justify-content: center; + gap: 0.5rem; + border-radius: 0.375rem; + border: 1px solid transparent; + padding-inline: 0.75rem; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 600; + white-space: nowrap; + transition: background-color 120ms, border-color 120ms, color 120ms; + } + .btn:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + } + .btn:disabled { + cursor: not-allowed; + opacity: 0.55; + } + .btn-primary { + background-color: var(--primary); + color: var(--primary-foreground); + } + .btn-primary:hover:not(:disabled) { + background-color: color-mix(in oklab, var(--primary) 88%, black); + } + .btn-secondary { + background-color: var(--card); + color: var(--foreground); + border-color: oklch(0.87 0 0); + box-shadow: 0 1px 2px oklch(0 0 0 / 0.06); + } + .btn-secondary:hover:not(:disabled) { + background-color: oklch(0.965 0 0); + } + .dark .btn-secondary { + border-color: var(--input); + box-shadow: none; + } + .dark .btn-secondary:hover:not(:disabled) { + background-color: var(--muted); + } + .btn-subtle { + background-color: transparent; + color: var(--foreground); + } + .btn-subtle:hover:not(:disabled) { + background-color: var(--muted); + } + .btn-icon { + width: 2rem; + padding-inline: 0; + } + + /* Verweis: dunkle bzw. blaue Schrift, nie gelb auf Weiss. */ + .link { + color: var(--link); + text-decoration-line: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; + text-decoration-color: color-mix(in oklab, var(--link) 40%, transparent); + } + .link:hover { + text-decoration-color: currentColor; + } +} + +/* Markdown (Notiz-Kachel, „Was ist neu“): Fluent-Schriftgroessen statt der + GitHub-Vorgaben (16 px Grundschrift, 2em-Ueberschriften), Hintergrund + transparent, Farben aus den Tokens. */ +:root .wmde-markdown { + font-family: var(--font-sans); + font-size: 0.875rem; + line-height: 1.5; + background-color: transparent !important; + color: var(--foreground) !important; + --color-border-default: var(--border); + --color-border-muted: var(--border); + --color-accent-fg: var(--link); + --color-fg-default: var(--foreground); + --color-fg-muted: var(--muted-foreground); +} +:root .wmde-markdown h1 { font-size: 1.25rem; line-height: 1.75rem; font-weight: 600; } +:root .wmde-markdown h2 { font-size: 1rem; line-height: 1.375rem; font-weight: 600; } +:root .wmde-markdown h3, :root .wmde-markdown h4 { font-size: 0.875rem; line-height: 1.25rem; font-weight: 600; } +:root .w-md-editor { + background-color: transparent !important; + box-shadow: none !important; +} + +/* Navigationseintrag mit Fluent-Auswahlpille, gesteuert ueber aria-current + (Einstellungs- und Verwaltungsnavigation). */ +.nav-item { + position: relative; +} +.nav-item[aria-current="page"]::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + width: 3px; + height: 1rem; + margin-top: -0.5rem; + border-radius: 9999px; + background-color: var(--primary-strong); +} diff --git a/apps/web/src/lib/stores/auth-store.ts b/apps/web/src/lib/stores/auth-store.ts index 026b004..67301e8 100644 --- a/apps/web/src/lib/stores/auth-store.ts +++ b/apps/web/src/lib/stores/auth-store.ts @@ -21,24 +21,18 @@ interface AuthState { function applyAccentColor(color: string | null | undefined) { if (typeof document === 'undefined') return; const root = document.documentElement; + // Design „Mosaik“: die persoenliche Akzentfarbe setzt nur noch `--primary` + // (Hauptknoepfe, Auswahlpille der Seitenleiste, Reiter-Unterstrich, Logo). + // Fokusring und Auswahlflaeche der Seitenleiste bleiben neutral — eine + // helle Akzentfarbe (Standard Gelb) waere als Fokusring auf Weiss kaum + // sichtbar (1,3:1). Alte Werte frueherer Fassungen werden entfernt. + root.style.removeProperty('--ring'); + root.style.removeProperty('--sidebar-accent'); + root.style.removeProperty('--sidebar-accent-foreground'); if (color) { root.style.setProperty('--primary', color); - root.style.setProperty('--ring', color); - // Sidebar active state: accent color with 15% opacity (#rrggbbaa, 0x26 ≈ 15%) - root.style.setProperty('--sidebar-accent', `${color}26`); - // Full-saturation accent text only reads well against the dark-mode sidebar's - // tinted-dark background; in light mode it's near-white behind it, so keep - // the theme's default (dark, high-contrast) foreground there instead. - if (root.classList.contains('dark')) { - root.style.setProperty('--sidebar-accent-foreground', color); - } else { - root.style.removeProperty('--sidebar-accent-foreground'); - } } else { root.style.removeProperty('--primary'); - root.style.removeProperty('--ring'); - root.style.removeProperty('--sidebar-accent'); - root.style.removeProperty('--sidebar-accent-foreground'); } }