Files
perinco-CRM/src/components/Header.tsx
T
Sascha WolfandClaude Opus 5 dc701f8522 feat(ui): Hintergrundgrafiken fuer Leiste und Seitenleiste je Theme
Pixel Art, Outdoor und Cyberpunk legen jetzt eine Grafik hinter die obere
Leiste und die Seitenleiste: 8-bit-Himmel mit Wolken und Huegelkante
sowie eine Steinmauer, blauer Himmel mit Schleierwolken sowie eine Wiese,
Neonroehren im Regen sowie eine regnerische Gasse. perinco bleibt
einfarbig.

Sechs SVG in public/themes/, von Hand gezeichnet - stilisierte Grafik
statt Foto, damit sie in jeder Aufloesung scharf bleibt; zusammen 36 kB.
Die Pixel-Grafiken haben einen absichtlich winzigen viewBox (eine Einheit
= ein Bildpunkt) und shape-rendering="crispEdges", damit die Kanten beim
Hochskalieren hart bleiben; die Mauer blendet nach oben stufenweise aus
und nicht mit einem Verlauf - Pixelgrafik kennt keine Zwischentoene.

Im Markup nur zwei Anker: app-header und app-sidebar. Den Rest machen
zwei Variablen mit Rueckfall auf none, weshalb perinco unberuehrt bleibt.

Die Variable enthaelt zwei Ebenen: einen Schleier aus der Flaechenfarbe
des Themes und darunter die Grafik. Das erledigt zwei Dinge zugleich -
derselbe blaue Himmel wird im Dunkelmodus zur Nacht (keine zweite Datei
noetig), und die Schrift bleibt lesbar, weil der Schleier in der Leiste an
den Raendern dicht liegt und in der Seitenleiste oben ueber den
Menuepunkten.

Aus dem ersten Versuch gelernt: die Cyberpunk-Grafik hatte eine deckende
Nachtflaeche. Im hellen Modus wurde daraus unter dem weissen Schleier ein
grauer Schmutzstreifen. Sie ist jetzt durchsichtig und zeigt nur Licht -
ueber Dunkel leuchtet das Neon, ueber Weiss bleiben blasse farbige
Linien. Ebenso zurueckgenommen wurde der Schleier ueber der Steinmauer:
"Einstellungen" und der Benutzername stehen darauf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 15:08:17 +02:00

92 lines
3.9 KiB
TypeScript

import React from 'react';
import { useTheme } from '../lib/theme';
import { THEME_PALETTES, THEME_PALETTE_LABELS } from '../types';
interface HeaderProps {
/** Beschriftung der Ansicht - meist ihr Name, siehe viewTitle(). */
title: string;
subtitle?: string;
onToggleSidebar: () => void;
}
const SUN_ICON =
'M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z';
const MOON_ICON = 'M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z';
export const Header: React.FC<HeaderProps> = ({ title, subtitle, onToggleSidebar }) => {
const { theme, preference, toggle, useSystem, palette, setPalette } = useTheme();
const isDark = theme === 'dark';
/*
* `app-header` ist keine Utility-Klasse, sondern ein Anker: darueber legt ein
* Theme ein Hintergrundbild hinter die Leiste (siehe index.css). `relative`
* haelt den Inhalt darueber.
*/
return (
<header className="app-header relative flex shrink-0 items-center gap-4 border-b border-ink-200 bg-surface px-4 py-4 md:px-6">
<button
onClick={onToggleSidebar}
className="rounded-lg p-2 text-ink-500 transition hover:bg-ink-100 hover:text-ink-800"
title="Menü ein-/ausklappen"
aria-label="Menü ein-/ausklappen"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<div className="min-w-0">
<h1 className="truncate text-lg text-ink-900">{title}</h1>
{subtitle && <p className="truncate text-xs font-semibold text-ink-400">{subtitle}</p>}
</div>
<div className="ml-auto flex items-center gap-2">
{/*
Farbwelt. Ein Auswahlfeld und keine Knopfreihe: es kommen weitere
Designs dazu, und drei bis fuenf Knoepfe im Kopf wuerden den Titel der
Ansicht verdraengen. Jede Wahl gilt nur fuer den angemeldeten
Benutzer - sie steht an seinem Konto, nicht im Browser.
*/}
<label className="sr-only" htmlFor="theme-palette">
Design
</label>
<select
id="theme-palette"
value={palette}
onChange={(event) => setPalette(event.target.value as typeof palette)}
title={`Design: ${THEME_PALETTE_LABELS[palette].hint}`}
className="rounded-xl border border-ink-200 bg-surface px-3 py-2 text-xs font-bold text-ink-600 outline-none transition hover:border-brand-300 hover:text-brand-700 focus:border-brand-500"
>
{THEME_PALETTES.map((option) => (
<option key={option} value={option}>
{THEME_PALETTE_LABELS[option].name}
</option>
))}
</select>
{/* Doppelklick loest die manuelle Wahl und folgt wieder dem System. */}
<button
onClick={toggle}
onDoubleClick={useSystem}
title={`${isDark ? 'Helles' : 'Dunkles'} Design einschalten${
preference === null ? ' (folgt derzeit dem System)' : ' · Doppelklick: wieder dem System folgen'
}`}
aria-label={`${isDark ? 'Helles' : 'Dunkles'} Design einschalten`}
aria-pressed={isDark}
className="flex items-center gap-2 rounded-xl border border-ink-200 px-3 py-2 text-xs font-bold text-ink-600 transition hover:border-brand-300 hover:text-brand-700"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d={isDark ? SUN_ICON : MOON_ICON}
/>
</svg>
<span className="hidden sm:inline">{isDark ? 'Hell' : 'Dunkel'}</span>
</button>
</div>
</header>
);
};