feat(quick-260907-fgv-01): Markenbausteine — Icons, Farbkonstanten, TesseraLogo-Komponente

- favicon.svg und apple-touch-icon.png byteidentisch nach src/app/ kopiert
  (dateibasierte Next.js-Metadaten fuer den Browser-Tab)
- brand.ts: einzige Stelle im Web-Quellcode fuer die drei Markenfarben
  (BRAND_YELLOW, BRAND_OLIVE, BRAND_PLATE), aus favicon.svg uebernommen
- TesseraLogo-Komponente mit den Varianten mark/horizontal/stacked, als
  JSX-SVG von Hand gesetzt (kein dangerouslySetInnerHTML), Kontur-Klasse
  fuer die dunkle Platte per plateOutline-Eigenschaft schaltbar
- common.brand.logoAlt in de.json und en.json ergaenzt, Schluesselparitaet
  per Test abgesichert

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
This commit is contained in:
2026-09-07 11:21:40 +02:00
parent 17a8355f27
commit 49d3df4485
7 changed files with 256 additions and 2 deletions
@@ -0,0 +1,130 @@
'use client';
import { useTranslations } from 'next-intl';
import { BRAND_OLIVE, BRAND_PLATE, BRAND_YELLOW } from './brand';
export type TesseraLogoVariant = 'mark' | 'horizontal' | 'stacked';
export interface TesseraLogoProps {
/** Welche Bausteine gerendert werden. Vorgabe: `'horizontal'`. */
variant?: TesseraLogoVariant;
/** Kantenlaenge der Bildmarke in Pixeln. Vorgabe: 32. */
size?: number;
/**
* Schaltet die halbtransparente helle Kontur der Grundplatte im dunklen
* Erscheinungsbild ein. Auf Flaechen, die unabhaengig vom
* Erscheinungsbild immer hell sind (etwa das gelbe Anmelde-Panel), muss
* dies auf `false` gesetzt werden. Vorgabe: `true`.
*/
plateOutline?: boolean;
/** Zusaetzliche Klassen fuer das aeusserste Element der gewaehlten Variante. */
className?: string;
/** Zusaetzliche Klassen fuer den Schriftzug (nur `horizontal`/`stacked`). */
wordmarkClassName?: string;
}
function LogoMark({
size,
plateOutline,
className,
ariaLabel,
}: {
size: number;
plateOutline: boolean;
className?: string;
ariaLabel: string;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 72 72"
width={size}
height={size}
role="img"
aria-label={ariaLabel}
className={className}
>
<rect
x="4"
y="4"
width="64"
height="64"
rx="14"
fill={BRAND_PLATE}
strokeWidth={2}
className={
plateOutline
? 'stroke-transparent dark:stroke-white/25'
: 'stroke-transparent'
}
/>
<g>
<rect x="16" y="16" width="12" height="12" rx="2.5" fill={BRAND_OLIVE} />
<rect x="30" y="16" width="12" height="12" rx="2.5" fill={BRAND_OLIVE} />
<rect
x="45"
y="15"
width="12"
height="12"
rx="2.5"
transform="rotate(12 51 21)"
fill={BRAND_YELLOW}
/>
<rect x="30" y="30" width="12" height="12" rx="2.5" fill={BRAND_OLIVE} />
<rect x="30" y="44" width="12" height="12" rx="2.5" fill={BRAND_OLIVE} />
</g>
</svg>
);
}
/**
* Zentrale Bildmarken-Komponente von Tessera. Buendelt die Bildmarke, den
* Schriftzug und deren Anordnung an einer einzigen Stelle -- eine spaetere
* mandantenspezifische Gestaltung setzt hier an, nicht an den einzelnen
* Verwendungsstellen.
*/
export function TesseraLogo({
variant = 'horizontal',
size = 32,
plateOutline = true,
className,
wordmarkClassName,
}: TesseraLogoProps) {
const t = useTranslations('common');
const ariaLabel = t('brand.logoAlt');
if (variant === 'mark') {
return (
<LogoMark
size={size}
plateOutline={plateOutline}
className={className}
ariaLabel={ariaLabel}
/>
);
}
if (variant === 'stacked') {
return (
<span className={`inline-flex flex-col items-center gap-2 ${className ?? ''}`}>
<LogoMark size={size} plateOutline={plateOutline} ariaLabel={ariaLabel} />
<span
className={`text-2xl font-bold tracking-tight text-foreground ${wordmarkClassName ?? ''}`}
>
{t('appName')}
</span>
</span>
);
}
return (
<span className={`inline-flex items-center gap-2 ${className ?? ''}`}>
<LogoMark size={size} plateOutline={plateOutline} ariaLabel={ariaLabel} />
<span
className={`font-bold tracking-tight text-foreground ${wordmarkClassName ?? ''}`}
>
{t('appName')}
</span>
</span>
);
}