Fundament: Branding Craftvia
- Farb-Tokens laut Brandbook §11.4 (Lotsenblau, Signalorange, Graphit, Hafengrau, Stahlgrau, Zink, Funktionsfarben) in globals.css + src/lib/brand.ts; helles Theme - Inline-SVG-Logo src/components/brand/craftvia-logo.tsx (horizontal/signet, color/mono/reversed, App-Icon-Kachel, optionale Tagline); Certvia-/GEFIM-Logos entfernt - Favicon/PWA-Icons aus dem Signet erzeugt (scripts/generate-brand-icons.ts), site.webmanifest (Craftvia, theme_color #082E5B) - Inter selbst gehostet (next/font/local, lokale OFL-Datei), Open Sans/Poppins entfernt - Metadaten, WebAuthn-RP-Name, Mail-/Dokument-CD auf Craftvia umgestellt - docs/craftvia/BRANDING.md ersetzt docs/BRANDING-CERTVIA.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,119 +0,0 @@
|
||||
import { BRAND, BRAND_COLORS } from "@/lib/brand";
|
||||
|
||||
/**
|
||||
* Certvia-Logo als Inline-SVG.
|
||||
*
|
||||
* Bewusst inline statt <Image src="…svg">: die Wortmarke setzt „Cert" in Poppins
|
||||
* Light und „via" in Poppins Bold. Ein extern geladenes SVG hat keinen Zugriff auf
|
||||
* die per next/font/local eingebundene Schrift (gehashter Family-Name) und würde
|
||||
* auf eine System-Schrift zurückfallen. Inline greift `var(--font-poppins)`.
|
||||
*
|
||||
* Zeichen-Konstruktion (unverändert aus dem Design-Paket, viewBox 64×64):
|
||||
* „C" nach rechts offen, der Haken liegt bündig in der Öffnung. Kein Verlauf.
|
||||
*
|
||||
* theme="light" → heller Grund (C Violett, Haken/„via" Magenta, „Cert" Anthrazit)
|
||||
* theme="dark" → dunkler Grund (C #7d6fd6, Haken #d17bcf, „Cert" weiß)
|
||||
*/
|
||||
|
||||
type LogoTheme = "light" | "dark";
|
||||
|
||||
const PALETTE: Record<LogoTheme, { arc: string; hook: string; word: string; accent: string }> = {
|
||||
light: {
|
||||
arc: BRAND_COLORS.violet,
|
||||
hook: BRAND_COLORS.magenta,
|
||||
word: BRAND_COLORS.anthracite,
|
||||
accent: BRAND_COLORS.magenta,
|
||||
},
|
||||
dark: {
|
||||
arc: BRAND_COLORS.violetOnDark,
|
||||
hook: BRAND_COLORS.magentaOnDark,
|
||||
word: BRAND_COLORS.white,
|
||||
accent: BRAND_COLORS.magentaOnDark,
|
||||
},
|
||||
};
|
||||
|
||||
/** Das Monogramm allein — Pfade aus `certvia-mark-*.svg`. */
|
||||
function Monogram({ arc, hook }: { arc: string; hook: string }) {
|
||||
return (
|
||||
<>
|
||||
<path
|
||||
d="M46 20 A17 17 0 1 0 46 44"
|
||||
fill="none"
|
||||
stroke={arc}
|
||||
strokeWidth={6}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M27 32 l6 6 l12 -15"
|
||||
fill="none"
|
||||
stroke={hook}
|
||||
strokeWidth={5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function CertviaLogo({
|
||||
variant = "lockup",
|
||||
theme = "dark",
|
||||
height = 40,
|
||||
className,
|
||||
title = BRAND.name,
|
||||
}: {
|
||||
variant?: "lockup" | "mark";
|
||||
theme?: LogoTheme;
|
||||
/** Renderhöhe in px; die Breite ergibt sich aus dem Seitenverhältnis. */
|
||||
height?: number;
|
||||
className?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
const c = PALETTE[theme];
|
||||
|
||||
if (variant === "mark") {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 64 64"
|
||||
width={height}
|
||||
height={height}
|
||||
role="img"
|
||||
aria-label={title}
|
||||
className={className}
|
||||
>
|
||||
<title>{title}</title>
|
||||
<Monogram arc={c.arc} hook={c.hook} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 380 96"
|
||||
width={(height * 380) / 96}
|
||||
height={height}
|
||||
role="img"
|
||||
aria-label={title}
|
||||
className={className}
|
||||
>
|
||||
<title>{title}</title>
|
||||
<g transform="translate(6,8) scale(1.25)">
|
||||
<Monogram arc={c.arc} hook={c.hook} />
|
||||
</g>
|
||||
<text
|
||||
x={112}
|
||||
y={62}
|
||||
fontSize={52}
|
||||
letterSpacing={-1}
|
||||
style={{ fontFamily: "var(--font-poppins), Poppins, 'Segoe UI', sans-serif" }}
|
||||
>
|
||||
<tspan fontWeight={300} fill={c.word}>
|
||||
Cert
|
||||
</tspan>
|
||||
<tspan fontWeight={700} fill={c.accent}>
|
||||
via
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { useId } from "react";
|
||||
import { BRAND, BRAND_COLORS } from "@/lib/brand";
|
||||
|
||||
/**
|
||||
* Craftvia-Logo als Inline-SVG (Referenz: docs/craftvia/assets/craftvia-brand-identity-v1.png).
|
||||
*
|
||||
* Signet: kräftiges „C" in Lotsenblau, rechts oben geöffnet. Ein Haken in Signalorange
|
||||
* startet links außerhalb, durchquert das C (das C ist dort freigestellt) und läuft als
|
||||
* Pfeil nach rechts oben durch die Öffnung hinaus („auf Kurs").
|
||||
* Wortmarke: „Craft" Lotsenblau + „via" Signalorange, fett; Tagline optional.
|
||||
* Keine Schatten, keine Verläufe.
|
||||
*
|
||||
* Inline statt <img>: die Wortmarke nutzt die per next/font eingebundene App-Schrift.
|
||||
*
|
||||
* Varianten:
|
||||
* variant="horizontal" (Default) | "signet"
|
||||
* tone="color" (Default) | "mono" (einfarbig Lotsenblau) | "reversed" (weiß auf dunklem Grund)
|
||||
* tile (nur signet): App-Icon — Lotsenblau-Kachel, weißes C, oranger Haken
|
||||
*/
|
||||
|
||||
export type CraftviaLogoTone = "color" | "mono" | "reversed";
|
||||
|
||||
/** Geometrie des Signets (viewBox 0 0 64 64) — auch von scripts/generate-brand-icons.ts genutzt. */
|
||||
export const SIGNET = {
|
||||
/** C: Kreisbogen um (30,34), r=19, von -70° gegen den Uhrzeigersinn bis +10° (Öffnung rechts oben). */
|
||||
arc: "M36.5 16.14 A19 19 0 1 0 48.71 37.3",
|
||||
arcWidth: 11,
|
||||
/** Haken: Schwanz links außerhalb → Tal → Richtung Öffnung. */
|
||||
check: "M6 28 L22 42 L44 21",
|
||||
checkWidth: 7,
|
||||
/** Pfeilspitze nach rechts oben. */
|
||||
arrow: "M52.7 12.7 L47.73 27.8 L37.37 16.96 Z",
|
||||
} as const;
|
||||
|
||||
function palette(tone: CraftviaLogoTone, tile: boolean) {
|
||||
if (tile) return { arc: BRAND_COLORS.white, check: BRAND_COLORS.signalorange, craft: BRAND_COLORS.white, via: BRAND_COLORS.signalorange, tagline: BRAND_COLORS.white };
|
||||
switch (tone) {
|
||||
case "mono":
|
||||
return { arc: BRAND_COLORS.lotsenblau, check: BRAND_COLORS.lotsenblau, craft: BRAND_COLORS.lotsenblau, via: BRAND_COLORS.lotsenblau, tagline: BRAND_COLORS.lotsenblau };
|
||||
case "reversed":
|
||||
return { arc: BRAND_COLORS.white, check: BRAND_COLORS.white, craft: BRAND_COLORS.white, via: BRAND_COLORS.white, tagline: BRAND_COLORS.white };
|
||||
default:
|
||||
return { arc: BRAND_COLORS.lotsenblau, check: BRAND_COLORS.signalorange, craft: BRAND_COLORS.lotsenblau, via: BRAND_COLORS.signalorange, tagline: BRAND_COLORS.lotsenblau };
|
||||
}
|
||||
}
|
||||
|
||||
function Signet({ maskId, arc, check }: { maskId: string; arc: string; check: string }) {
|
||||
return (
|
||||
<>
|
||||
<defs>
|
||||
{/* Freistellung: das C ist dort unterbrochen, wo der Haken es durchquert. */}
|
||||
<mask id={maskId} maskUnits="userSpaceOnUse" x="0" y="0" width="64" height="64">
|
||||
<rect x="0" y="0" width="64" height="64" fill="#fff" />
|
||||
<path d={SIGNET.check} fill="none" stroke="#000" strokeWidth={SIGNET.checkWidth + 6} strokeLinejoin="miter" />
|
||||
</mask>
|
||||
</defs>
|
||||
<path d={SIGNET.arc} fill="none" stroke={arc} strokeWidth={SIGNET.arcWidth} strokeLinecap="butt" mask={`url(#${maskId})`} />
|
||||
<path d={SIGNET.check} fill="none" stroke={check} strokeWidth={SIGNET.checkWidth} strokeLinejoin="miter" strokeLinecap="butt" />
|
||||
<path d={SIGNET.arrow} fill={check} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function CraftviaLogo({
|
||||
variant = "horizontal",
|
||||
tone = "color",
|
||||
tile = false,
|
||||
tagline = false,
|
||||
height = 36,
|
||||
className,
|
||||
title = BRAND.name,
|
||||
}: {
|
||||
variant?: "horizontal" | "signet";
|
||||
tone?: CraftviaLogoTone;
|
||||
/** Nur für variant="signet": App-Icon-Kachel in Lotsenblau. */
|
||||
tile?: boolean;
|
||||
/** Tagline „Handwerk. Digital auf Kurs." unter der Wortmarke (nur horizontal). */
|
||||
tagline?: boolean;
|
||||
/** Renderhöhe in px; die Breite ergibt sich aus dem Seitenverhältnis. */
|
||||
height?: number;
|
||||
className?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
const maskId = `cv-mask-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
||||
const isTile = variant === "signet" && tile;
|
||||
const c = palette(tone, isTile);
|
||||
|
||||
if (variant === "signet") {
|
||||
return (
|
||||
<svg viewBox="0 0 64 64" width={height} height={height} role="img" aria-label={title} className={className}>
|
||||
<title>{title}</title>
|
||||
{isTile ? (
|
||||
<>
|
||||
<rect x="0" y="0" width="64" height="64" rx="14" fill={BRAND_COLORS.lotsenblau} />
|
||||
<g transform="translate(9.6 9.6) scale(0.7)">
|
||||
<Signet maskId={maskId} arc={c.arc} check={c.check} />
|
||||
</g>
|
||||
</>
|
||||
) : (
|
||||
<Signet maskId={maskId} arc={c.arc} check={c.check} />
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const vbW = 292;
|
||||
const vbH = tagline ? 82 : 64;
|
||||
return (
|
||||
<svg viewBox={`0 0 ${vbW} ${vbH}`} width={(height * vbW) / vbH} height={height} role="img" aria-label={title} className={className}>
|
||||
<title>{title}</title>
|
||||
<Signet maskId={maskId} arc={c.arc} check={c.check} />
|
||||
<text
|
||||
x={70}
|
||||
y={48}
|
||||
fontSize={46}
|
||||
fontWeight={800}
|
||||
letterSpacing={-1.2}
|
||||
style={{ fontFamily: "var(--font-sans), Inter, system-ui, 'Segoe UI', Arial, sans-serif" }}
|
||||
>
|
||||
<tspan fill={c.craft}>Craft</tspan>
|
||||
<tspan fill={c.via}>via</tspan>
|
||||
</text>
|
||||
{tagline && (
|
||||
<text
|
||||
x={72}
|
||||
y={74}
|
||||
fontSize={15}
|
||||
fontWeight={600}
|
||||
fill={c.tagline}
|
||||
style={{ fontFamily: "var(--font-sans), Inter, system-ui, 'Segoe UI', Arial, sans-serif" }}
|
||||
>
|
||||
{BRAND.tagline}
|
||||
</text>
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import { BRAND, BRAND_ASSETS } from "@/lib/brand";
|
||||
|
||||
/**
|
||||
* Dachmarken-Hinweis „Ein Produkt von GEFIM".
|
||||
*
|
||||
* Bewusste Leitplanke der Branding-Umstellung: Certvia ist die Produktmarke,
|
||||
* GEFIM bleibt Dachmarke und erscheint weiterhin in Fußzeilen, auf den
|
||||
* Anmeldeseiten sowie später in Export- und Mail-Fußzeilen.
|
||||
* Siehe docs/BRANDING-CERTVIA.md.
|
||||
*/
|
||||
export function PoweredByGefim({
|
||||
withLogo = true,
|
||||
className,
|
||||
}: {
|
||||
withLogo?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<p
|
||||
className={`flex items-center justify-center gap-2 text-[11px] text-muted-foreground ${className ?? ""}`}
|
||||
>
|
||||
<span>{BRAND.byline}</span>
|
||||
{withLogo && (
|
||||
<Image
|
||||
src={BRAND_ASSETS.parentLogo}
|
||||
alt={BRAND.parentBrand}
|
||||
// Seitenverhältnis der Quelldatei (1000×353)
|
||||
width={76}
|
||||
height={27}
|
||||
// Negativdarstellung auf dunklem Grund
|
||||
className="opacity-80 brightness-0 invert"
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
import type { TenantBranding } from "@/lib/brand";
|
||||
|
||||
/**
|
||||
* Kopfbereichs-Marke eines Mandanten (Story S8).
|
||||
* Kopfbereichs-Marke eines Mandanten.
|
||||
*
|
||||
* Default ist **Certvia**; ein mandanteneigenes Logo überschreibt nur, wenn es
|
||||
* gesetzt ist. Der Upload je Mandant kommt mit Admin Phase 2 (Objektspeicher) —
|
||||
* bis dahin liefert `resolveTenantBranding` konstant `logoUrl: null`, die
|
||||
* Verzweigung ist aber bereits vorhanden und muss dann nicht nachgezogen werden.
|
||||
* Default ist **Craftvia**; ein mandanteneigenes Logo überschreibt nur, wenn es
|
||||
* gesetzt ist (TenantSettings.logoKey → Upload folgt). Bis dahin liefert
|
||||
* `resolveTenantBranding` konstant `logoUrl: null`.
|
||||
*/
|
||||
export function TenantBrand({
|
||||
branding,
|
||||
@@ -20,8 +19,7 @@ export function TenantBrand({
|
||||
}) {
|
||||
if (branding.logoUrl) {
|
||||
return (
|
||||
// Mandanten-Logos liegen später im Objektspeicher (beliebige Domain) —
|
||||
// daher bewusst <img> statt next/image ohne Remote-Pattern-Konfiguration.
|
||||
// Mandanten-Logos liegen im Objektspeicher — daher bewusst <img> statt next/image.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={branding.logoUrl}
|
||||
@@ -31,5 +29,5 @@ export function TenantBrand({
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <CertviaLogo variant="lockup" theme="dark" height={height} className={className} />;
|
||||
return <CraftviaLogo variant="horizontal" height={height} className={className} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user