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>
92 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
};
|