feat: App-Hülle mit Header, Theme-Umschalter und Übersicht
Layout mit Inline-Skript, das data-theme setzt, bevor der erste Pixel gemalt wird — sonst blitzt beim Laden das falsche Farbschema auf. Logo als React-Komponente statt als Bilddatei: nimmt Theme-Farben an, spart einen Netzwerkabruf und die Verläufe liegen einmalig im Root-Layout. Im hellen Modus greifen eigene Tokens für Verlaufstext und Wortmarke; der vollfarbige Signaturverlauf fällt auf warmem Weiß sonst unter WCAG AA.
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import { clsx } from "clsx";
|
||||
|
||||
/**
|
||||
* Die Marke als React-Komponenten.
|
||||
*
|
||||
* Inline statt <img src="/brand/…">: so kostet das Logo keinen zweiten
|
||||
* Netzwerkabruf, lässt sich animieren und die monochrome Variante nimmt die
|
||||
* Textfarbe des Kontexts an.
|
||||
*
|
||||
* Die Verläufe liegen einmalig in <BrandDefs /> (im Root-Layout gerendert) und
|
||||
* werden von allen Logo-Instanzen referenziert — sonst gäbe es doppelte IDs.
|
||||
*/
|
||||
|
||||
const GRADIENT = "url(#fl-brand-gradient)";
|
||||
const HALO = "url(#fl-brand-halo)";
|
||||
const BLOOM = "url(#fl-brand-bloom)";
|
||||
|
||||
/** Einmalig pro Dokument rendern (Root-Layout). */
|
||||
export function BrandDefs() {
|
||||
return (
|
||||
<svg width="0" height="0" aria-hidden focusable="false" style={{ position: "absolute" }}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="fl-brand-gradient"
|
||||
x1="20"
|
||||
y1="24"
|
||||
x2="108"
|
||||
y2="102"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="var(--fl-brand-1)" />
|
||||
<stop offset="0.38" stopColor="var(--fl-brand-2)" />
|
||||
<stop offset="1" stopColor="var(--fl-brand-3)" />
|
||||
</linearGradient>
|
||||
<radialGradient id="fl-brand-halo" cx="64" cy="58" r="52" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stopColor="var(--fl-brand-1)" stopOpacity="0.5" />
|
||||
<stop offset="0.45" stopColor="var(--fl-brand-2)" stopOpacity="0.24" />
|
||||
<stop offset="1" stopColor="var(--fl-brand-3)" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<filter id="fl-brand-bloom" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="6" />
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="fl-brand-word"
|
||||
x1="0"
|
||||
y1="10"
|
||||
x2="418"
|
||||
y2="108"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="var(--fl-brand-1)" />
|
||||
<stop offset="0.38" stopColor="var(--fl-brand-2)" />
|
||||
<stop offset="1" stopColor="var(--fl-brand-3)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
type MarkProps = {
|
||||
className?: string;
|
||||
/** Ohne Glühen und ohne Verlauf — nimmt die Textfarbe an (currentColor). */
|
||||
mono?: boolean;
|
||||
/** Verlauf, aber kein Glühen — für sehr kleine Darstellungen. */
|
||||
flat?: boolean;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export function FlimmerMark({ className, mono = false, flat = false, title }: MarkProps) {
|
||||
const paint = mono ? "currentColor" : GRADIENT;
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 128 128"
|
||||
className={clsx("h-8 w-8 shrink-0", className)}
|
||||
role={title ? "img" : "presentation"}
|
||||
aria-label={title}
|
||||
aria-hidden={title ? undefined : true}
|
||||
>
|
||||
{!mono && !flat && (
|
||||
<>
|
||||
<ellipse cx="64" cy="58" rx="50" ry="40" fill={HALO} filter={BLOOM} />
|
||||
<rect
|
||||
x="22"
|
||||
y="26"
|
||||
width="84"
|
||||
height="64"
|
||||
rx="18"
|
||||
fill="none"
|
||||
stroke={paint}
|
||||
strokeWidth="8"
|
||||
filter={BLOOM}
|
||||
opacity="0.7"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<rect x="22" y="26" width="84" height="64" rx="18" fill="none" stroke={paint} strokeWidth="7" />
|
||||
<path
|
||||
d="M55.5 45.5 L80 58 L55.5 70.5 Z"
|
||||
fill={paint}
|
||||
stroke={paint}
|
||||
strokeWidth="6.5"
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M64 90 V101 M49 105.5 H79"
|
||||
stroke={paint}
|
||||
strokeWidth="6.5"
|
||||
strokeLinecap="round"
|
||||
strokeOpacity="0.62"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wortmarke als Pfad — unabhängig von installierten Schriften und damit
|
||||
* überall identisch (Header, Login, E-Mail-Vorschau, Druck).
|
||||
*/
|
||||
export function FlimmerWordmark({
|
||||
className,
|
||||
mono = false,
|
||||
}: {
|
||||
className?: string;
|
||||
mono?: boolean;
|
||||
}) {
|
||||
const paint = mono ? "currentColor" : "url(#fl-brand-word)";
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 432 118"
|
||||
className={clsx("h-6 w-auto", className)}
|
||||
role="presentation"
|
||||
aria-hidden
|
||||
>
|
||||
<g
|
||||
transform="translate(0 8)"
|
||||
fill="none"
|
||||
stroke={paint}
|
||||
strokeWidth="15"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M14 100 V10 H62" />
|
||||
<path d="M14 54 H52" />
|
||||
<path d="M84 100 V6" />
|
||||
<path d="M106 100 V46" />
|
||||
<path d="M128 100 V64 Q128 46 146 46 Q164 46 164 64 V100" />
|
||||
<path d="M164 64 Q164 46 182 46 Q200 46 200 64 V100" />
|
||||
<path d="M218 100 V64 Q218 46 236 46 Q254 46 254 64 V100" />
|
||||
<path d="M254 64 Q254 46 272 46 Q290 46 290 64 V100" />
|
||||
<path d="M344.5 91.8 A29 19.5 0 1 1 366 73 H308" />
|
||||
<path d="M392 100 V46" />
|
||||
<path d="M392 64 Q392 46 414 46" />
|
||||
</g>
|
||||
<circle cx="106" cy="30" r="7.5" fill={paint} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Bildmarke plus Wortmarke — die Standard-Sperrform. */
|
||||
export function FlimmerLogo({
|
||||
className,
|
||||
markClassName,
|
||||
wordClassName,
|
||||
mono = false,
|
||||
}: {
|
||||
className?: string;
|
||||
markClassName?: string;
|
||||
wordClassName?: string;
|
||||
mono?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<span className={clsx("inline-flex items-center gap-2.5", className)}>
|
||||
<FlimmerMark className={clsx("h-9 w-9", markClassName)} mono={mono} />
|
||||
<FlimmerWordmark className={clsx("h-[1.35rem]", wordClassName)} mono={mono} />
|
||||
<span className="sr-only">Flimmer</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user