Die Oberflaeche auf das Manner-CD umstellen
Some checks failed
CI / Lint, Typen, Tests, Build (push) Successful in 11m45s
CI / Migrationen auf leerer Datenbank (push) Has been cancelled

Farben, Schrift, Logo und Symbole folgen jetzt dem CD Manual 2022. An der
Logik aendert sich nichts: keine Migration, keine Abfrage, keine Berechtigung.

Die zwei Markenfarben stehen in §1.1 als #F69686 (Rosa) und #164194 (Blau).
Welche davon was traegt, ist nicht gewaehlt, sondern nachgerechnet: Weiss auf
Rosa kommt auf 2.18:1 und faellt damit auch fuer Grossschrift durch, Blau auf
Rosa auf 4.34:1 und reicht fuer das Logo, nicht fuer Text. Schwarz auf Rosa
sind 7.47:1, Weiss auf Blau 9.47:1. Rosa ist deshalb Flaeche, Blau ist
Interaktion — zwei Skalen, brand-* und accent-*, statt einer geteilten.

Von den 26 Stellen mit brand-50/100/200 sind nur sieben auf accent gewandert.
Der Rest meint einen Zustand und keinen Markenton: die Zeile unter dem Zeiger,
der gewaehlte Listeneintrag, der aktive Menuepunkt. Gegen die warme
Seitenflaeche ist ein kuehler Ton dort deutlicher, und Blau heisst in dieser
Oberflaeche ab jetzt "reagiert auf dich". Nach accent gingen die Faelle ohne
eigene Bedeutung — "Geplant", "Offen", der neutrale Protokoll-Chip — und die
offene Planstelle im Organigramm, die vorher ein blasses Blau war und damit
wie eine schwaechere Person aussah.

Die Funktionsfarben bleiben, was sie sind. Das Manual regelt die Identitaet,
nicht die Rueckmeldung: Rot heisst Fehler, weil die Benutzerin das mitbringt.
`info` bleibt bewusst tuerkis — Blau saehe ab jetzt bedienbar aus, und gemessen
kaeme ein blaues info dem violetten Chip auf dE 6.8 nahe, also nicht
unterscheidbar. Violett ist dabei nachgezogen: gegen den Fehler-Chip stand es
bei dE 8.0, "Austritt" und "Befoerderung" waren im Vorbeigehen dieselbe blasse
Flaeche. Jetzt dE 15.0.

Die Schrift ist Barlow. Nachgezaehlt ist das Manual zu 95 % in DIN gesetzt
(Regular 79 %, Bold 16 %); Helvetica Neue steht nur in den Visitenkarten und im
Claim. DIN laesst sich nicht ausliefern — eine Drucklizenz deckt keinen Webfont
—, und die freien Nachbauten der DIN 1451 sind Schilderschriften, bei 14 px in
einer langen Tabelle schlechter lesbar als das, was sie ersetzen. Die Variable
heisst --font-din und nicht --font-barlow: liegt eines Tages eine Web-Lizenz
vor, ist der Wechsel diese eine Deklaration.

Nebenbei zwei Dinge repariert, die vorher schon falsch waren. Die Umrandung von
Eingabefeldern, Knoepfen und Suchfeldern lag bei 1.30:1 und damit unter den 3:1,
die WCAG 1.4.11 fuer Bedienelemente verlangt; border-strong bringt 3.56:1. Und
das mitgelieferte favicon.ico liess sich gar nicht bauen: eingebettet waren
24-Bit-RGB-PNG, waehrend der Kopf 32 bpp behauptete. Alle Rastersymbole liegen
jetzt als RGBA vor, und ihr Blau ist auf den CD-Wert gezogen — samt der
kantengeglaetteten Raender, indem je Pixel der Blauanteil bestimmt und neu
gemischt wurde.

Das Logo ist das Markenlogo (§4.1), nicht das Unternehmenslogo, das §3.2 fuer
eine Anwendung mit der AG als Absender vorsaehe — es liegt nicht vor. Der
Freiraum X/3 steckt im Bauteil selbst und nicht in den Aufrufstellen, sonst
haengt seine Einhaltung daran, dass jede einzelne daran denkt.

docs/farbschema.html zeigt Token, Kontraste und Bauteile nebeneinander und
laesst sich ohne Server oeffnen.

Nicht im Browser gesehen: Anmeldung laeuft ueber das Firmenkonto und die
Datenbank ist von hier nicht erreichbar. Lint, Typen, Schemaabgleich, 524 Tests
und der Build sind sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 19:12:52 +02:00
parent 99e4357c02
commit f7a5c48615
33 changed files with 1305 additions and 92 deletions

View File

@@ -1,5 +1,6 @@
import { EntraSignInButton } from "@/components/auth/EntraSignInButton"; import { EntraSignInButton } from "@/components/auth/EntraSignInButton";
import { PasswortFormular } from "@/components/auth/PasswortFormular"; import { PasswortFormular } from "@/components/auth/PasswortFormular";
import { AppWortmarke } from "@/components/brand/Logo";
import { logout, signInWithEntra } from "@/actions/auth"; import { logout, signInWithEntra } from "@/actions/auth";
// The query string is attacker-controlled, so the login page renders a message // The query string is attacker-controlled, so the login page renders a message
@@ -56,8 +57,8 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
<main className="flex items-center justify-center px-6 py-12 lg:py-6"> <main className="flex items-center justify-center px-6 py-12 lg:py-6">
<div className="w-full max-w-sm"> <div className="w-full max-w-sm">
<div className="lg:hidden"> <div className="-ml-2 lg:hidden">
<Wordmark className="text-ink" /> <AppWortmarke hoehe={26} textKlasse="text-ink" />
</div> </div>
<h2 className="mt-8 text-2xl font-extrabold tracking-tight text-ink lg:mt-0">Anmelden</h2> <h2 className="mt-8 text-2xl font-extrabold tracking-tight text-ink lg:mt-0">Anmelden</h2>
@@ -130,8 +131,10 @@ function BrandPanel() {
}} }}
/> />
<div className="relative"> {/* -ml-4 gleicht den Freiraum des Logos aus, damit die Wortmarke
<Wordmark className="text-white" /> buendig mit dem Text darunter beginnt und nicht eingerueckt wirkt. */}
<div className="relative -ml-4">
<AppWortmarke hoehe={36} textKlasse="text-white" />
</div> </div>
<div className="relative max-w-md"> <div className="relative max-w-md">
@@ -149,19 +152,6 @@ function BrandPanel() {
); );
} }
function Wordmark({ className = "" }: { className?: string }) { // Die vier Quadrate, die hier als Platzhalter-Bildmarke standen, sind
return ( // entfallen: an ihrer Stelle steht jetzt das Manner-Logo
<div className={`flex items-center gap-2.5 ${className}`}> // (components/brand/Logo.tsx).
{/* Vier Quadrate wie ein Organigramm-Ausschnitt: eine Wurzel, darunter
drei Einheiten. */}
<svg viewBox="0 0 24 24" className="h-7 w-7" aria-hidden="true">
<rect x="9" y="2" width="6" height="6" rx="1.5" fill="currentColor" />
<rect x="1" y="16" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.55" />
<rect x="9" y="16" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.75" />
<rect x="17" y="16" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.55" />
<path d="M12 8v4M4 16v-4h16v4" stroke="currentColor" strokeWidth="1.5" fill="none" opacity="0.5" />
</svg>
<span className="text-lg font-extrabold tracking-tight">Alpenwerk HR</span>
</div>
);
}

BIN
app/apple-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -6,6 +6,12 @@ import { useEffect } from "react";
// (app)/error.tsx is not mounted yet. It replaces <html>, so it cannot use // (app)/error.tsx is not mounted yet. It replaces <html>, so it cannot use
// the app's fonts, Tailwind layer or shared components — hence the inline // the app's fonts, Tailwind layer or shared components — hence the inline
// styles. Kept deliberately plain; anything clever here can fail too. // styles. Kept deliberately plain; anything clever here can fail too.
//
// Die Farben sind dieselben wie in app/globals.css, nur von Hand: surface,
// border, ink, ink-body, ink-muted und brand-500. Sie stehen hier doppelt,
// weil diese Seite gerade dann erscheint, wenn das Stylesheet nicht
// getragen hat — eine Variable waere hier genau das, worauf man sich nicht
// verlassen kann.
export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
useEffect(() => { useEffect(() => {
console.error("Global error:", error); console.error("Global error:", error);
@@ -13,19 +19,19 @@ export default function GlobalError({ error, reset }: { error: Error & { digest?
return ( return (
<html lang="de-AT"> <html lang="de-AT">
<body style={{ margin: 0, backgroundColor: "#f9f1f5", fontFamily: "system-ui, sans-serif", color: "#2d1c26" }}> <body style={{ margin: 0, backgroundColor: "#fdf6f4", fontFamily: "system-ui, sans-serif", color: "#231f20" }}>
<div style={{ maxWidth: 480, margin: "15vh auto", padding: 24, background: "#fff", border: "1px solid #eedde6", borderRadius: 8 }}> <div style={{ maxWidth: 480, margin: "15vh auto", padding: 24, background: "#fff", border: "1px solid #f0dbd4", borderRadius: 8 }}>
<h1 style={{ fontSize: 18, margin: "0 0 8px" }}>Die Anwendung konnte nicht geladen werden</h1> <h1 style={{ fontSize: 18, margin: "0 0 8px" }}>Die Anwendung konnte nicht geladen werden</h1>
<p style={{ fontSize: 14, lineHeight: 1.5, color: "#503b47", margin: "0 0 16px" }}> <p style={{ fontSize: 14, lineHeight: 1.5, color: "#4b4446", margin: "0 0 16px" }}>
Es ist ein unerwarteter Fehler aufgetreten. Ihre Daten sind davon nicht betroffen. Es ist ein unerwarteter Fehler aufgetreten. Ihre Daten sind davon nicht betroffen.
</p> </p>
{error.digest && ( {error.digest && (
<p style={{ fontSize: 12, fontFamily: "monospace", color: "#7a636f", margin: "0 0 16px" }}>Fehlerkennung: {error.digest}</p> <p style={{ fontSize: 12, fontFamily: "monospace", color: "#756c6e", margin: "0 0 16px" }}>Fehlerkennung: {error.digest}</p>
)} )}
<button <button
onClick={reset} onClick={reset}
style={{ style={{
background: "#d6046e", background: "#164194",
color: "#fff", color: "#fff",
border: "none", border: "none",
borderRadius: 4, borderRadius: 4,

View File

@@ -1,23 +1,74 @@
@import "tailwindcss"; @import "tailwindcss";
/* Design tokens from spec §7 (ported from tailwind.config.ts for Tailwind v4) */ /* ═══ Farben nach dem Manner CD Manual 2022 ═══════════════════════════
§1.1 kennt genau zwei Markenfarben:
Manner Rosa #F69686 CMYK 0/50/40/0 sRGB 246 150 134
Manner Blau #164194 CMYK 100/80/0/0 sRGB 22 65 148 (Reflex Blue)
Beide behalten ihren Wert unveraendert; die Abstufungen daneben sind
Aufhellungen und Abdunklungen davon, weil eine Oberflaeche Zwischentoene
fuer Hover, Chips und Flaechen braucht und das Manual nur fuer den Druck
geschrieben ist.
── Warum Blau die Interaktion traegt und Rosa die Flaeche ──
Nachgerechnet nach WCAG 2.1:
Weiss auf Rosa 2.18:1 faellt durch, auch fuer Grossschrift
Schwarz auf Rosa 7.47:1 AAA
Weiss auf Blau 9.47:1 AAA
Blau auf Weiss 9.47:1 AAA
Rosa kann also keine Schrift tragen — weder weisse noch, bei 4.34:1,
blaue Fliesschrift. Es ist eine Flaeche, auf der Dunkles steht. Blau
traegt in beide Richtungen und wird damit zur Farbe von Knopf, Link und
Fokusring. Das ist keine Auslegung des Manuals, sondern das, was die
beiden Farben physikalisch hergeben. */
@theme { @theme {
--color-brand-50: #fdf3f8; /* ── Blau: alles, was man anfassen kann ── */
--color-brand-100: #fdeaf3; --color-brand-50: #eef3fb;
--color-brand-200: #f6cfe2; --color-brand-100: #dae4f5;
--color-brand-500: #d6046e; --color-brand-200: #afc3e6;
--color-brand-600: #b0035a; --color-brand-500: #164194; /* Manner Blau, CD §1.1 */
--color-brand-700: #a30354; --color-brand-600: #11326f;
--color-brand-700: #0d2857;
--color-surface: #f9f1f5; /* ── Rosa: alles, was die Marke traegt ──
accent-500 ist der Markenwert und bleibt Flaeche. Die Schrift darauf
ist ink (7.47:1), nie Weiss. */
--color-accent-50: #fef5f2;
--color-accent-100: #fde7e1;
--color-accent-200: #facec2;
--color-accent-500: #f69686; /* Manner Rosa, CD §1.1 */
--color-accent-600: #d9604b;
--color-accent-700: #a8402e;
--color-border: #eedde6; --color-surface: #fdf6f4;
--color-border-subtle: #f7e8ef;
--color-ink: #2d1c26; --color-border: #f0dbd4;
--color-ink-body: #503b47; --color-border-subtle: #f8ece8;
--color-ink-muted: #7a636f; /* Fuer die Umrandung von Eingabefeldern. Der dekorative Kartenrahmen
darueber darf blass sein; die Grenze eines Bedienelements muss nach
WCAG 1.4.11 3:1 erreichen, und das tut dieser Ton mit 3.56:1 gegen
Weiss und 3.34:1 gegen die Seitenflaeche. */
--color-border-strong: #a67f70;
/* Das Schwarz aus dem Manual selbst (#231F20), nicht reines Schwarz. */
--color-ink: #231f20;
--color-ink-body: #4b4446;
--color-ink-muted: #756c6e;
/* ── Funktionsfarben ──
Sie bleiben, was sie sind. Das Manual regelt die Identitaet, nicht die
Rueckmeldung: Rot heisst Fehler, weil die Benutzerin das mitbringt,
nicht weil es jemand entworfen hat. In einem System, in dem ein Knopf
einen Austritt erfasst, waere eine markenkonforme Fehlerfarbe kein
Stil, sondern ein Mangel.
`info` bleibt bewusst tuerkis statt blau zu werden: Blau heisst ab
jetzt „hier kann man klicken", und ein Hinweischip in derselben Farbe
saehe bedienbar aus. Gemessen kommt dazu, dass ein blaues info dem
violetten Chip zu nahe kaeme (dE 6.8 — nicht unterscheidbar). */
--color-success-bg: #dff6dd; --color-success-bg: #dff6dd;
--color-success-text: #0e700e; --color-success-text: #0e700e;
@@ -31,8 +82,11 @@
--color-info-bg: #d7f0f0; --color-info-bg: #d7f0f0;
--color-info-text: #00666d; --color-info-text: #00666d;
--color-purple-bg: #f0ecf7; /* Nachgezogen: gegen den alten Wert #f0ecf7 stand der Fehler-Chip bei
--color-purple-text: #5c2e91; dE 8.0 — „Austritt" und „Befoerderung" waren im Vorbeigehen dieselbe
blasse Flaeche. Jetzt dE 15.0 bei 7.9:1 Kontrast. */
--color-purple-bg: #e4d9f5;
--color-purple-text: #54277f;
/* A bare `--radius` in Tailwind v4 collapses the whole scale onto one /* A bare `--radius` in Tailwind v4 collapses the whole scale onto one
value — `rounded` and `rounded-lg` both came out at 8px, so a chip, an value — `rounded` and `rounded-lg` both came out at 8px, so a chip, an
@@ -46,12 +100,17 @@
/* Warm, brand-tinted rather than neutral grey: a pure black shadow over /* Warm, brand-tinted rather than neutral grey: a pure black shadow over
the pink surface reads as dirt. Kept shallow — this is a data-dense the pink surface reads as dirt. Kept shallow — this is a data-dense
admin UI, the depth only has to separate a card from the page. */ admin UI, the depth only has to separate a card from the page.
--shadow-card: 0 1px 2px rgb(107 33 71 / 0.04), 0 1px 3px rgb(107 33 71 / 0.06); Der Grundton folgt dem Rosa der Flaeche, nicht mehr dem alten Magenta. */
--shadow-card-hover: 0 2px 4px rgb(107 33 71 / 0.06), 0 4px 12px rgb(107 33 71 / 0.08); --shadow-card: 0 1px 2px rgb(122 60 45 / 0.05), 0 1px 3px rgb(122 60 45 / 0.07);
--shadow-overlay: 0 8px 32px rgb(45 28 38 / 0.16); --shadow-card-hover: 0 2px 4px rgb(122 60 45 / 0.07), 0 4px 12px rgb(122 60 45 / 0.09);
--shadow-overlay: 0 8px 32px rgb(35 31 32 / 0.18);
--font-sans: var(--font-nunito), system-ui, sans-serif; /* Siehe app/layout.tsx: DIN waere die Schrift des Manuals, ist aber nicht
als Webfont lizenziert; geladen wird Barlow als naechstliegender freier
Ersatz. Die Ersatzkette bleibt neutral — faellt der Webfont aus, soll
keine gerundete Systemschrift einspringen. */
--font-sans: var(--font-din), "Segoe UI", system-ui, sans-serif;
} }
body { body {

315
app/icon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -1,11 +1,33 @@
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { Nunito } from "next/font/google"; import { Barlow } from "next/font/google";
import { ToastProvider } from "@/components/ui/Toast"; import { ToastProvider } from "@/components/ui/Toast";
import "./globals.css"; import "./globals.css";
const nunito = Nunito({ // ═══ Die Schrift ═══
variable: "--font-nunito", //
subsets: ["latin"], // Das CD Manual ist selbst in DIN gesetzt: nachgezaehlt tragen DIN-Regular
// 79 % und DIN-Bold 16 % des Textes, zusammen 95 %. Helvetica Neue taucht nur
// in den Visitenkarten-Mustern und im Claim auf.
//
// DIN laesst sich hier trotzdem nicht einsetzen: FF DIN und DIN Next sind
// lizenzpflichtig, und eine Druck-Lizenz deckt die Auslieferung als Webfont
// nicht ab — das ist eine eigene, nach Domain oder Abrufen bemessene Lizenz.
// Die freien Nachbauten der DIN 1451 sind Schilderschriften: enge Punzen,
// wenige Schnitte, und bei 14 px in einer Tabelle mit achthundert Zeilen
// schlechter lesbar als das, was sie ersetzen sollen.
//
// Barlow ist die naechstliegende freie Schrift mit UI-Tauglichkeit: dieselbe
// Grotesk-Familie, schmale Proportionen, geringer Strichkontrast, gerade
// Abschluesse — und neun Schnitte mit vollem Latin-Extended, also allen
// Umlauten.
//
// Die Variable heisst nach der Absicht und nicht nach dem Ersatz: liegt
// eines Tages eine Web-Lizenz fuer DIN vor, ist der Wechsel diese eine
// Deklaration und sonst nichts.
const din = Barlow({
variable: "--font-din",
// latin-ext wegen der Umlaute und des scharfen S, die in Namen vorkommen.
subsets: ["latin", "latin-ext"],
weight: ["400", "600", "700", "800"], weight: ["400", "600", "700", "800"],
}); });
@@ -15,6 +37,11 @@ export const metadata: Metadata = {
// Added to the home screen on iOS this opens without Safari's chrome and // Added to the home screen on iOS this opens without Safari's chrome and
// keeps the app's own name rather than the page title. // keeps the app's own name rather than the page title.
appleWebApp: { capable: true, title: "Alpenwerk HR", statusBarStyle: "default" }, appleWebApp: { capable: true, title: "Alpenwerk HR", statusBarStyle: "default" },
// app/favicon.ico, app/icon.svg und app/apple-icon.png findet Next von
// selbst; die Datei-Konventionen brauchen keinen Eintrag hier. Der Verweis
// auf das Manifest dagegen schon — app/manifest.webmanifest wird sonst
// zwar ausgeliefert, aber von keiner Seite genannt.
manifest: "/manifest.webmanifest",
}; };
export const viewport: Viewport = { export const viewport: Viewport = {
@@ -23,7 +50,9 @@ export const viewport: Viewport = {
// The layout extends under the notch and home indicator; every edge that // The layout extends under the notch and home indicator; every edge that
// matters pads itself back out with env(safe-area-inset-*). // matters pads itself back out with env(safe-area-inset-*).
viewportFit: "cover", viewportFit: "cover",
themeColor: "#ffffff", // Manner Rosa (CD §1.1): die Farbe, in der mobile Browser ihre eigene
// Leiste ueber der Seite einfaerben. Weiss war hier nur die Vorgabe.
themeColor: "#F69686",
}; };
export default function RootLayout({ export default function RootLayout({
@@ -32,7 +61,7 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="de-AT" className={`${nunito.variable} h-full antialiased`}> <html lang="de-AT" className={`${din.variable} h-full antialiased`}>
<body className="flex min-h-dvh flex-col font-sans"> <body className="flex min-h-dvh flex-col font-sans">
<ToastProvider>{children}</ToastProvider> <ToastProvider>{children}</ToastProvider>
</body> </body>

29
app/manifest.webmanifest Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "Alpenwerk HR",
"short_name": "Alpenwerk HR",
"description": "Interne HR-Stammdatenverwaltung",
"icons": [
{
"src": "/brand/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/brand/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/brand/icon-512-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#F69686",
"background_color": "#FDF6F4",
"display": "standalone",
"start_url": "/"
}

View File

@@ -49,7 +49,7 @@ export function AuditDetail({ eintrag }: { eintrag: AuditEintrag }) {
> >
<span>{eintrag.details ?? "–"}</span> <span>{eintrag.details ?? "–"}</span>
{anzahl > 0 && ( {anzahl > 0 && (
<span className="ml-2 whitespace-nowrap rounded-full bg-brand-50 px-2 py-0.5 text-[11px] font-semibold text-brand-700"> <span className="ml-2 whitespace-nowrap rounded-full bg-accent-100 px-2 py-0.5 text-[11px] font-semibold text-brand-700">
{anzahl} {anzahl === 1 ? "Feld" : "Felder"} {anzahl} {anzahl === 1 ? "Feld" : "Felder"}
</span> </span>
)} )}

75
components/brand/Logo.tsx Normal file
View File

@@ -0,0 +1,75 @@
// Das Manner-Logo und die Wortmarke der Anwendung — an einer Stelle, weil
// Kopfleiste und Anmeldeseite dasselbe zeigen sollen.
//
// ═══ Welches Logo ═══
//
// Verwendet wird das **Markenlogo** (CD §4.1): der Schriftzug allein, ohne
// den Stephansdom-Medaillon. Das Unternehmenslogo (§3.1) waere nach §3.2 das
// formal richtige, wo „Josef Manner & Comp. AG" als Absender auftritt — es
// steht hier aber nicht zur Verfuegung, und das Markenlogo ist die Fassung,
// die vorliegt.
//
// ═══ Freiraum ═══
//
// §4.1 verlangt „genuegend Freiraum rundum (X, X/3)". X ist der Modul des
// Logos; gelesen wird er hier als seine Hoehe, der Freiraum also als ein
// Drittel davon. Er steckt als Polsterung in diesem Bauteil und nicht in den
// Aufrufstellen — sonst haengt die Einhaltung daran, dass jede einzelne
// daran denkt.
//
// ═══ Rosa ═══
//
// §1.2: der Schriftzug darf ausschliesslich zur Gaenze auf Rosa stehen. Die
// Datei bringt ihr rosa Feld selbst mit, das Logo ist also auf jedem
// Untergrund regelkonform — auch auf dem blauen Panel der Anmeldeseite.
//
// Als <img> und nicht inline: die Datei wiegt 73 kB, weil die Kontur des
// Schriftzugs aus 77 Pfaden besteht. Inline laege sie in jeder einzelnen
// Seitenauslieferung; als Datei holt der Browser sie einmal und nimmt sie
// danach aus dem Zwischenspeicher.
/** Seitenverhaeltnis der Datei: viewBox 288.6781 x 116.18912. */
const VERHAELTNIS = 288.6781 / 116.18912;
export function MannerLogo({ hoehe = 28, className = "" }: { hoehe?: number; className?: string }) {
const breite = Math.round(hoehe * VERHAELTNIS);
return (
// next/image bringt Groessenvarianten und Ladelogik fuer Fotos mit; fuer
// eine Vektordatei fester Groesse ist beides Ballast, und die
// Bildoptimierung laesst SVG ohnehin unveraendert durch.
// eslint-disable-next-line @next/next/no-img-element
<img
src="/brand/manner-logo.svg"
alt="Manner"
width={breite}
height={hoehe}
// Der Freiraum nach §4.1, als Polsterung. `box-content`, damit die
// Breite oben die des Logos bleibt und nicht die des Feldes.
style={{ padding: Math.round(hoehe / 3), width: breite, height: hoehe }}
className={`box-content shrink-0 ${className}`}
/>
);
}
/**
* Logo plus Produktname.
*
* Die beiden gehoeren zusammen und meinen Verschiedenes: das Logo sagt, wem
* das System gehoert, der Name sagt, was es ist.
*/
export function AppWortmarke({
hoehe = 26,
textKlasse = "text-ink",
className = "",
}: {
hoehe?: number;
textKlasse?: string;
className?: string;
}) {
return (
<span className={`flex items-center ${className}`}>
<MannerLogo hoehe={hoehe} />
<span className={`text-base font-extrabold tracking-tight ${textKlasse}`}>Alpenwerk HR</span>
</span>
);
}

View File

@@ -52,7 +52,7 @@ export function AngehoerigeSection({ employeeId, dependents, effectiveDate }: {
<div className="overflow-x-auto rounded border border-border"> <div className="overflow-x-auto rounded border border-border">
<table className="w-full min-w-[600px] text-sm"> <table className="w-full min-w-[600px] text-sm">
<thead> <thead>
<tr className="border-b border-border bg-brand-50 text-left text-xs font-semibold uppercase tracking-wide text-ink-muted"> <tr className="border-b border-border bg-accent-50 text-left text-xs font-semibold uppercase tracking-wide text-ink-muted">
<th className="px-4 py-2.5">Name</th> <th className="px-4 py-2.5">Name</th>
<th className="px-4 py-2.5">Verhältnis</th> <th className="px-4 py-2.5">Verhältnis</th>
<th className="px-4 py-2.5">SVNR</th> <th className="px-4 py-2.5">SVNR</th>

View File

@@ -103,7 +103,7 @@ export function NotizenTab({ employeeId, notes }: { employeeId: string; notes: N
{n.done ? ( {n.done ? (
<span className="rounded-full bg-success-bg px-2 py-0.5 text-xs font-semibold text-success-text">Erledigt</span> <span className="rounded-full bg-success-bg px-2 py-0.5 text-xs font-semibold text-success-text">Erledigt</span>
) : ( ) : (
<span className="rounded-full bg-brand-100 px-2 py-0.5 text-xs font-semibold text-brand-700">Offen</span> <span className="rounded-full bg-accent-200 px-2 py-0.5 text-xs font-semibold text-brand-700">Offen</span>
)} )}
</div> </div>
<p className="mt-1 text-sm text-ink">{n.note_text}</p> <p className="mt-1 text-sm text-ink">{n.note_text}</p>

View File

@@ -156,7 +156,7 @@ export function EmployeeTree({ employees, focusId = null }: { employees: OrgEmpl
<div key={e.id}> <div key={e.id}>
<div <div
ref={e.id === focusId ? focusRef : undefined} ref={e.id === focusId ? focusRef : undefined}
className={`flex items-center gap-2 rounded px-2 py-1.5 hover:bg-surface ${isMatch ? "bg-brand-100" : ""}`} className={`flex items-center gap-2 rounded px-2 py-1.5 hover:bg-surface ${isMatch ? "bg-accent-200" : ""}`}
style={{ paddingLeft: depth * 24 + 8 }} style={{ paddingLeft: depth * 24 + 8 }}
> >
{hasChildren ? ( {hasChildren ? (

View File

@@ -23,11 +23,15 @@ import type { ChartNode, ChartNodeKind } from "./types";
// object as a change and remounts every custom node otherwise. // object as a change and remounts every custom node otherwise.
const NODE_TYPES = { orgNode: OrgChartNode }; const NODE_TYPES = { orgNode: OrgChartNode };
// React Flow nimmt echte Farbwerte, keine Klassen — deshalb stehen die
// Token hier als Literal. Sie folgen KIND_ACCENT in OrgChartNode; laeuft das
// eine dem anderen davon, zeigt die Uebersichtskarte andere Farben als die
// Karten, ueber die sie liegt.
const MINIMAP_COLORS: Record<ChartNodeKind, string> = { const MINIMAP_COLORS: Record<ChartNodeKind, string> = {
person: "#d6046e", person: "#164194", // brand-500, Manner Blau
role: "#5c2e91", role: "#54277f", // purple-text
group: "#c9b3c0", group: "#756c6e", // ink-muted
vacancy: "#f6cfe2", vacancy: "#f69686", // accent-500, Manner Rosa
}; };
export type GraphOrgChartProps = { export type GraphOrgChartProps = {
@@ -118,7 +122,7 @@ function GraphOrgChartInner({ tree, isExpanded, onToggle, matchedIds }: GraphOrg
fitView fitView
fitViewOptions={{ padding: 0.2 }} fitViewOptions={{ padding: 0.2 }}
> >
<Background variant={BackgroundVariant.Dots} gap={22} size={1.4} color="#eedde6" /> <Background variant={BackgroundVariant.Dots} gap={22} size={1.4} color="#f0dbd4" />
<Controls showInteractive={false} /> <Controls showInteractive={false} />
{/* Hidden under lg via CSS: on a phone it would cover a real {/* Hidden under lg via CSS: on a phone it would cover a real
fraction of the canvas for little navigational benefit. */} fraction of the canvas for little navigational benefit. */}
@@ -126,8 +130,8 @@ function GraphOrgChartInner({ tree, isExpanded, onToggle, matchedIds }: GraphOrg
pannable pannable
zoomable zoomable
ariaLabel="Übersichtskarte" ariaLabel="Übersichtskarte"
maskColor="rgba(249, 241, 245, 0.75)" maskColor="rgba(253, 246, 244, 0.75)"
nodeColor={(n) => MINIMAP_COLORS[(n.data as OrgChartNodeDataLike).chartNode.kind] ?? "#d6046e"} nodeColor={(n) => MINIMAP_COLORS[(n.data as OrgChartNodeDataLike).chartNode.kind] ?? "#164194"}
nodeStrokeWidth={0} nodeStrokeWidth={0}
nodeBorderRadius={3} nodeBorderRadius={3}
/> />

View File

@@ -25,14 +25,18 @@ const KIND_ACCENT: Record<ChartNodeKind, string> = {
person: "bg-brand-500", person: "bg-brand-500",
role: "bg-purple-text", role: "bg-purple-text",
group: "bg-ink-muted", group: "bg-ink-muted",
vacancy: "bg-brand-200", // Rosa statt einer blassen Blaustufe: eine offene Stelle ist kein
// schwaecheres Abbild einer Person, sondern etwas anderes. Gegen das
// Blau der Person ist der Unterschied auch dann noch zu sehen, wenn
// herausgezoomt nur noch die Streifen uebrig sind.
vacancy: "bg-accent-500",
}; };
const KIND_SHELL: Record<ChartNodeKind, string> = { const KIND_SHELL: Record<ChartNodeKind, string> = {
person: "border-border bg-white", person: "border-border bg-white",
role: "border-border bg-white", role: "border-border bg-white",
group: "border-border-subtle bg-surface", group: "border-border-subtle bg-surface",
vacancy: "border-dashed border-brand-200 bg-brand-50", vacancy: "border-dashed border-accent-500 bg-accent-50",
}; };
// React Flow re-renders node components on every pan/zoom frame — memo is // React Flow re-renders node components on every pan/zoom frame — memo is
@@ -48,7 +52,7 @@ export const OrgChartNode = memo(function OrgChartNode({ id, data }: NodeProps<O
{kind === "person" && avatar ? ( {kind === "person" && avatar ? (
<Avatar firstName={avatar.firstName} lastName={avatar.lastName} size="sm" /> <Avatar firstName={avatar.firstName} lastName={avatar.lastName} size="sm" />
) : kind === "vacancy" ? ( ) : kind === "vacancy" ? (
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-dashed border-brand-200 text-brand-500"> <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-dashed border-accent-500 text-accent-700">
<Plus className="h-3.5 w-3.5" /> <Plus className="h-3.5 w-3.5" />
</span> </span>
) : kind === "role" ? ( ) : kind === "role" ? (

View File

@@ -538,10 +538,12 @@ function Stapel({ units, ebenen, mitPersonen }: { units: PrintUnit[]; ebenen: nu
const KOPFFARBE: Record<PrintUnit["unitType"], string> = { const KOPFFARBE: Record<PrintUnit["unitType"], string> = {
// Nach unten hin blasser: so bleibt die Rangfolge auch auf dem // Nach unten hin blasser: so bleibt die Rangfolge auch auf dem
// Schwarzweissdrucker sichtbar, auf dem die meisten Ausdrucke landen. // Schwarzweissdrucker sichtbar, auf dem die meisten Ausdrucke landen.
// Vier Stufen einer Farbe statt drei plus der Seitenflaeche: so ist es
// eine Reihe und nicht eine Reihe mit einem Fremdkoerper am Ende.
Gesellschaft: "bg-brand-500 text-white", Gesellschaft: "bg-brand-500 text-white",
Bereich: "bg-brand-100 text-ink", Bereich: "bg-brand-200 text-ink",
Abteilung: "bg-brand-50 text-ink", Abteilung: "bg-brand-100 text-ink",
Team: "bg-surface text-ink", Team: "bg-brand-50 text-ink",
}; };
function Kasten({ unit, mitPersonen }: { unit: PrintUnit; mitPersonen: boolean }) { function Kasten({ unit, mitPersonen }: { unit: PrintUnit; mitPersonen: boolean }) {

View File

@@ -4,6 +4,7 @@ import { BarChart3, Building2, History, LayoutGrid, Network, Upload, Users, X }
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { AppWortmarke } from "@/components/brand/Logo";
const NAV_ITEMS = [ const NAV_ITEMS = [
{ href: "/", label: "Übersicht", icon: LayoutGrid }, { href: "/", label: "Übersicht", icon: LayoutGrid },
@@ -55,8 +56,12 @@ export function Sidebar({ open, onClose }: { open: boolean; onClose: () => void
open ? "translate-x-0" : "-translate-x-full" open ? "translate-x-0" : "-translate-x-full"
}`} }`}
> >
<div className="flex h-14 shrink-0 items-center justify-between border-b border-border pl-5 pr-3"> {/* pl-2 statt pl-5: der Freiraum des Logos bringt seinen eigenen
<span className="text-base font-extrabold text-brand-700">Alpenwerk HR</span> Abstand mit (siehe components/brand/Logo.tsx), und beides
uebereinander schoebe die Wortmarke sichtbar aus der Flucht der
Navigationspunkte darunter. */}
<div className="flex h-14 shrink-0 items-center justify-between border-b border-border pl-2 pr-3">
<AppWortmarke hoehe={24} textKlasse="text-brand-700" />
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}

View File

@@ -14,7 +14,10 @@ const BASE =
const VARIANTS = { const VARIANTS = {
primary: "bg-brand-500 text-white hover:bg-brand-600", primary: "bg-brand-500 text-white hover:bg-brand-600",
secondary: "border border-border bg-white text-ink-body hover:bg-surface", // border-strong wie bei den Eingabefeldern: die Umrandung ist hier das
// Einzige, was den Knopf vom Hintergrund trennt, und muss deshalb die
// 3:1 aus WCAG 1.4.11 erreichen.
secondary: "border border-border-strong bg-white text-ink-body hover:bg-surface",
ghost: "text-ink-body hover:bg-surface", ghost: "text-ink-body hover:bg-surface",
danger: "bg-danger-solid text-white hover:brightness-110", danger: "bg-danger-solid text-white hover:brightness-110",
// Square icon-only button; pair with an aria-label. // Square icon-only button; pair with an aria-label.

View File

@@ -12,16 +12,21 @@ import { useId, type ReactNode, type SelectHTMLAttributes, type InputHTMLAttribu
// impossible to get wrong, and gives every field one place to fix focus // impossible to get wrong, and gives every field one place to fix focus
// styling, error display and sizing. // styling, error display and sizing.
// `border-strong` und nicht `border`: der blasse Kartenrahmen kommt gegen
// Weiss auf 1.3:1 und ist als Schmuck in Ordnung, die Umrandung eines
// Bedienelements aber nicht — WCAG 1.4.11 verlangt dafuer 3:1, und ohne sie
// ist auf einem hellen Bildschirm nicht zu sehen, wo ein Feld anfaengt.
// border-strong liegt bei 3.56:1.
export const CONTROL_CLASS = export const CONTROL_CLASS =
"w-full rounded border border-border bg-white px-3 py-2 text-sm text-ink " + "w-full rounded border border-border-strong bg-white px-3 py-2 text-sm text-ink " +
"focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-brand-500 " + "focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-brand-500 " +
"disabled:cursor-not-allowed disabled:bg-surface disabled:text-ink-muted"; "disabled:cursor-not-allowed disabled:border-border disabled:bg-surface disabled:text-ink-muted";
const INVALID_CLASS = "border-danger-solid"; const INVALID_CLASS = "border-danger-solid";
/** Auto-width select for filter bars, where the label is an aria-label. */ /** Auto-width select for filter bars, where the label is an aria-label. */
export const FILTER_SELECT_CLASS = export const FILTER_SELECT_CLASS =
"rounded border border-border bg-white px-3 py-2 text-sm text-ink " + "rounded border border-border-strong bg-white px-3 py-2 text-sm text-ink " +
"focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-brand-500"; "focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-brand-500";
type FieldChildProps = { type FieldChildProps = {

View File

@@ -126,7 +126,7 @@ export function Lookup<T>({
return ( return (
<div ref={containerRef} className="relative"> <div ref={containerRef} className="relative">
<div className="flex items-center gap-2 rounded border border-border bg-white px-3 py-2 focus-within:outline-2 focus-within:outline-offset-1 focus-within:outline-brand-500"> <div className="flex items-center gap-2 rounded border border-border-strong bg-white px-3 py-2 focus-within:outline-2 focus-within:outline-offset-1 focus-within:outline-brand-500">
<Search className="h-4 w-4 shrink-0 text-ink-muted" aria-hidden /> <Search className="h-4 w-4 shrink-0 text-ink-muted" aria-hidden />
<input <input
id={id} id={id}

View File

@@ -25,7 +25,7 @@ export function SearchInput({
const id = useId(); const id = useId();
return ( return (
<div <div
className={`flex min-w-[240px] flex-1 items-center gap-2 rounded border border-border bg-white px-3 py-2 focus-within:outline-2 focus-within:outline-offset-1 focus-within:outline-brand-500 ${className}`} className={`flex min-w-[240px] flex-1 items-center gap-2 rounded border border-border-strong bg-white px-3 py-2 focus-within:outline-2 focus-within:outline-offset-1 focus-within:outline-brand-500 ${className}`}
> >
<Search className="h-4 w-4 shrink-0 text-ink-muted" aria-hidden /> <Search className="h-4 w-4 shrink-0 text-ink-muted" aria-hidden />
<label htmlFor={id} className="sr-only"> <label htmlFor={id} className="sr-only">

366
docs/farbschema.html Normal file
View File

@@ -0,0 +1,366 @@
<!doctype html>
<html lang="de-AT">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Alpenwerk HR — Farbschema nach Manner CD</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--brand-50:#eef3fb; --brand-100:#dae4f5; --brand-200:#afc3e6;
--brand-500:#164194; --brand-600:#11326f; --brand-700:#0d2857;
--accent-50:#fef5f2; --accent-100:#fde7e1; --accent-200:#facec2;
--accent-500:#f69686; --accent-600:#d9604b; --accent-700:#a8402e;
--surface:#fdf6f4; --border:#f0dbd4; --border-subtle:#f8ece8; --border-strong:#a67f70;
--ink:#231f20; --ink-body:#4b4446; --ink-muted:#756c6e;
--success-bg:#dff6dd; --success-text:#0e700e;
--danger-bg:#fde7e9; --danger-text:#b10e1c; --danger-solid:#c50f1f;
--warning-bg:#fff4ce; --warning-text:#835b00;
--info-bg:#d7f0f0; --info-text:#00666d;
--purple-bg:#e4d9f5; --purple-text:#54277f;
--shadow-card: 0 1px 2px rgb(122 60 45 / .05), 0 1px 3px rgb(122 60 45 / .07);
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--surface); color: var(--ink);
font: 14px/1.55 "Barlow", "Segoe UI", system-ui, sans-serif;
}
.wrap { max-width: 1120px; margin: 0 auto; padding: 32px 24px 80px; }
h1 { font-size: 28px; font-weight: 800; letter-spacing: -.02em; margin: 0 0 6px; }
h2 { font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .09em;
color: var(--ink-muted); margin: 44px 0 14px; }
h3 { font-size: 15px; font-weight: 800; margin: 0 0 10px; }
p { color: var(--ink-body); max-width: 68ch; }
.lead { font-size: 15px; }
code { font: 12px/1.4 ui-monospace, "Cascadia Mono", Consolas, monospace;
background: #fff; border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; }
.card { background: #fff; border: 1px solid var(--border); border-radius: 8px;
box-shadow: var(--shadow-card); padding: 20px; }
.grid { display: grid; gap: 14px; }
.g2 { grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); }
.g3 { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
/* ── Farbfelder ── */
.swatch { border-radius: 8px; overflow: hidden; border: 1px solid var(--border); background: #fff; }
.swatch .fl { height: 84px; display: flex; align-items: flex-end; padding: 8px 10px; font-weight: 700; font-size: 12px; }
.swatch .mt { padding: 8px 10px; font-size: 11px; color: var(--ink-muted); line-height: 1.45; }
.swatch .mt b { color: var(--ink); font-weight: 700; display: block; }
.ramp { display: flex; border-radius: 8px; overflow: hidden; border: 1px solid var(--border); }
.ramp div { flex: 1; height: 62px; display: flex; align-items: flex-end; justify-content: center;
padding-bottom: 5px; font-size: 10px; font-weight: 700; }
/* ── Tabelle ── */
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: .07em;
color: var(--ink-muted); font-weight: 800; padding: 7px 10px; border-bottom: 1px solid var(--border); }
td { padding: 7px 10px; border-bottom: 1px solid var(--border-subtle); }
tbody tr:hover { background: var(--brand-50); }
.num { font-variant-numeric: tabular-nums; text-align: right; }
.ok { color: var(--success-text); font-weight: 700; }
.warn { color: var(--warning-text); font-weight: 700; }
.bad { color: var(--danger-text); font-weight: 700; }
/* ── Bauteile ── */
.btn { display: inline-flex; align-items: center; gap: 6px; border-radius: 6px;
font-weight: 700; font-size: 14px; padding: 8px 16px; border: 1px solid transparent; cursor: pointer; }
.btn-primary { background: var(--brand-500); color: #fff; }
.btn-primary:hover { background: var(--brand-600); }
.btn-secondary { background: #fff; color: var(--ink-body); border-color: var(--border-strong); }
.btn-secondary:hover { background: var(--surface); }
.btn-danger { background: var(--danger-solid); color: #fff; }
.btn-ghost { background: transparent; color: var(--ink-body); }
.btn:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
.field { display: block; margin-bottom: 12px; }
.field label { display: block; font-size: 13px; font-weight: 700; margin-bottom: 4px; }
.field input, .field select {
width: 100%; border: 1px solid var(--border-strong); border-radius: 6px;
padding: 8px 12px; font: inherit; font-size: 14px; color: var(--ink); background: #fff; }
.field input:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 1px; }
.field .hint { font-size: 12px; color: var(--ink-muted); margin-top: 4px; }
.field.invalid input { border-color: var(--danger-solid); }
.field.invalid .hint { color: var(--danger-text); font-weight: 700; }
.chip { display: inline-block; border-radius: 999px; padding: 2px 10px; font-size: 12px; font-weight: 700; }
.chips { display: flex; flex-wrap: wrap; gap: 7px; }
.nav { width: 232px; border: 1px solid var(--border); border-radius: 8px; background: #fff; overflow: hidden; }
.nav .hd { height: 56px; display: flex; align-items: center; gap: 0; padding-left: 8px;
border-bottom: 1px solid var(--border); }
.nav a { display: flex; align-items: center; gap: 10px; padding: 9px 14px; margin: 3px 10px;
border-radius: 6px; font-size: 13.5px; font-weight: 700; color: var(--ink-body); text-decoration: none; }
.nav a.active { background: var(--brand-100); color: var(--brand-700); }
.nav a:hover:not(.active) { background: var(--surface); }
.dot { width: 15px; height: 15px; border-radius: 4px; background: currentColor; opacity: .55; flex: none; }
.note { border-left: 3px solid var(--accent-500); background: #fff;
padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 14px 0; }
.note b { color: var(--ink); }
.freiraum { display: inline-block; outline: 1px dashed var(--accent-600); outline-offset: 0; }
.logo-demo { display: flex; align-items: center; gap: 26px; flex-wrap: wrap; }
.on-blue { background: var(--brand-700); padding: 22px; border-radius: 8px; }
.on-white { background: #fff; padding: 22px; border-radius: 8px; border: 1px solid var(--border); }
</style>
</head>
<body>
<div class="wrap">
<h1>Farbschema nach dem Manner CD Manual 2022</h1>
<p class="lead">Erster Entwurf. Diese Seite zeigt die Token und die Bauteile, die daraus
entstehen — nebeneinander, damit sich vor dem Blick in die Anwendung beurteilen laesst,
ob die Richtung stimmt. Alle Werte hier sind dieselben wie in
<code>app/globals.css</code>.</p>
<!-- ══════════════════════════════════════════════════════════ -->
<h2>1 · Die zwei Markenfarben</h2>
<div class="grid g2">
<div class="swatch">
<div class="fl" style="background:#f69686;color:#231f20">Schrift darauf: ink</div>
<div class="mt"><b>Manner Rosa — #F69686</b>
CMYK 0/50/40/0 · sRGB 246 150 134<br>
Pantone: Orange 021 7.20 + Bright Red 7.20 + Trans. White 85.60<br>
CD §1.1 · im Projekt <code>accent-500</code></div>
</div>
<div class="swatch">
<div class="fl" style="background:#164194;color:#fff">Schrift darauf: weiss</div>
<div class="mt"><b>Manner Blau — #164194</b>
CMYK 100/80/0/0 · sRGB 22 65 148<br>
Pantone Reflex Blue C / U<br>
CD §1.1 · im Projekt <code>brand-500</code></div>
</div>
</div>
<div class="note">
<b>Eine Abweichung, die auffallen wird.</b> Die mitgelieferte Logodatei trug
<code>#034ea2</code> als Blau — den Wert, mit dem das PDF gerendert wurde, nicht den,
den §1.1 im Text als verbindlich nennt. Beide liegen sichtbar auseinander (dE 6.3).
Das Logo ist deshalb auf <code>#164194</code> gezogen worden, damit es neben den
blauen Bedienelementen nicht wie ein Fehler aussieht. Beim Rosa war die Differenz
bedeutungslos (dE 0.9).
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<h2>2 · Warum Blau bedient und Rosa traegt</h2>
<p>Die Rollenverteilung ist nicht gewaehlt, sondern gemessen. Nach WCAG 2.1 braucht
Fliesstext 4.5:1, Grossschrift und Rahmen 3:1.</p>
<div class="card">
<table>
<thead><tr><th>Kombination</th><th class="num">Kontrast</th><th>Fliesstext</th><th>Folge</th></tr></thead>
<tbody>
<tr><td>Weiss auf Manner Rosa</td><td class="num">2.18:1</td><td class="bad">faellt durch</td>
<td>Rosa kann keine weisse Schrift tragen — auch nicht in gross</td></tr>
<tr><td>Manner Blau auf Manner Rosa</td><td class="num">4.34:1</td><td class="warn">knapp daneben</td>
<td>reicht fuer das Logo, nicht fuer Text</td></tr>
<tr><td>ink #231F20 auf Manner Rosa</td><td class="num">7.47:1</td><td class="ok">AAA</td>
<td>so wird Rosa benutzt: Flaeche unter Dunklem</td></tr>
<tr><td>Weiss auf Manner Blau</td><td class="num">9.47:1</td><td class="ok">AAA</td>
<td>Knopf, Panel</td></tr>
<tr><td>Manner Blau auf Weiss</td><td class="num">9.47:1</td><td class="ok">AAA</td>
<td>Link, Fokusring, aktiver Menuepunkt</td></tr>
</tbody>
</table>
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<h2>3 · Die Abstufungen</h2>
<p><b>Blau — alles, was man anfassen kann.</b></p>
<div class="ramp">
<div style="background:var(--brand-50);color:#231f20">50</div>
<div style="background:var(--brand-100);color:#231f20">100</div>
<div style="background:var(--brand-200);color:#231f20">200</div>
<div style="background:var(--brand-500);color:#fff">500 · CD</div>
<div style="background:var(--brand-600);color:#fff">600</div>
<div style="background:var(--brand-700);color:#fff">700</div>
</div>
<p style="margin-top:20px"><b>Rosa — alles, was die Marke traegt.</b></p>
<div class="ramp">
<div style="background:var(--accent-50);color:#231f20">50</div>
<div style="background:var(--accent-100);color:#231f20">100</div>
<div style="background:var(--accent-200);color:#231f20">200</div>
<div style="background:var(--accent-500);color:#231f20">500 · CD</div>
<div style="background:var(--accent-600);color:#fff">600</div>
<div style="background:var(--accent-700);color:#fff">700</div>
</div>
<p style="margin-top:20px"><b>Flaechen und Schrift.</b> Das Schwarz ist <code>#231F20</code>
aus dem Manual, nicht reines Schwarz.</p>
<div class="ramp">
<div style="background:var(--surface);color:#231f20">surface</div>
<div style="background:var(--border-subtle);color:#231f20">border-subtle</div>
<div style="background:var(--border);color:#231f20">border</div>
<div style="background:var(--border-strong);color:#fff">border-strong</div>
<div style="background:var(--ink-muted);color:#fff">ink-muted</div>
<div style="background:var(--ink-body);color:#fff">ink-body</div>
<div style="background:var(--ink);color:#fff">ink</div>
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<h2>4 · Bauteile</h2>
<div class="grid g2">
<div class="card">
<h3>Knoepfe</h3>
<div style="display:flex;gap:9px;flex-wrap:wrap;align-items:center">
<button class="btn btn-primary">Speichern</button>
<button class="btn btn-secondary">Abbrechen</button>
<button class="btn btn-danger">Austritt erfassen</button>
<button class="btn btn-ghost">Zurueck</button>
</div>
<p style="font-size:12px;color:var(--ink-muted);margin:14px 0 0">
Der sekundaere Knopf hat jetzt <code>border-strong</code> — vorher war seine
Umrandung mit 1.3:1 kaum zu sehen.</p>
</div>
<div class="card">
<h3>Eingabefelder</h3>
<div class="field">
<label>Nachname</label>
<input value="Gruber">
</div>
<div class="field invalid">
<label>SV-Nummer</label>
<input value="1234 010180">
<div class="hint">Die Pruefziffer stimmt nicht.</div>
</div>
<p style="font-size:12px;color:var(--ink-muted);margin:0">
Rahmen 3.56:1 statt 1.30:1 — WCAG 1.4.11 fuer Bedienelemente.</p>
</div>
<div class="card">
<h3>Status</h3>
<div class="chips">
<span class="chip" style="background:var(--success-bg);color:var(--success-text)">Aktiv</span>
<span class="chip" style="background:var(--warning-bg);color:var(--warning-text)">Langzeitabwesenheit</span>
<span class="chip" style="background:var(--accent-200);color:var(--brand-700)">Geplant</span>
<span class="chip" style="background:var(--danger-bg);color:var(--danger-text)">Ausgetreten</span>
</div>
<p style="font-size:12px;color:var(--ink-muted);margin:14px 0 0">
„Geplant" ist der Fall ohne besondere Faerbung und bekommt deshalb die
Markenfarbe. Blau saehe hier nach Schaltflaeche aus.</p>
</div>
<div class="card">
<h3>Vorgaenge im Protokoll</h3>
<div class="chips">
<span class="chip" style="background:var(--success-bg);color:var(--success-text)">Neueinstellung</span>
<span class="chip" style="background:var(--danger-bg);color:var(--danger-text)">Austritt</span>
<span class="chip" style="background:var(--info-bg);color:var(--info-text)">Versetzung</span>
<span class="chip" style="background:var(--purple-bg);color:var(--purple-text)">Befoerderung</span>
<span class="chip" style="background:var(--warning-bg);color:var(--warning-text)">Vertragsaenderung</span>
<span class="chip" style="background:var(--accent-200);color:var(--brand-700)">Import</span>
</div>
<p style="font-size:12px;color:var(--ink-muted);margin:14px 0 0">
Rot bleibt Rot, Gruen bleibt Gruen: das Manual regelt die Identitaet, nicht die
Rueckmeldung. Violett ist nachgezogen — gegen den Fehler-Chip lag es bei dE 8.0
und war im Vorbeigehen dieselbe Flaeche, jetzt bei dE 15.0.</p>
</div>
<div class="card" style="grid-column:1/-1">
<h3>Liste</h3>
<table>
<thead><tr><th>Name</th><th>Pers.-Nr.</th><th>Bereich</th><th>Eintritt</th><th>Status</th></tr></thead>
<tbody>
<tr><td><b>Aigner, Katharina</b></td><td class="num">1042</td><td>Produktion</td><td class="num">01.03.2019</td>
<td><span class="chip" style="background:var(--success-bg);color:var(--success-text)">Aktiv</span></td></tr>
<tr><td><b>Berger, Anna</b></td><td class="num">1187</td><td>Vertrieb</td><td class="num">15.09.2021</td>
<td><span class="chip" style="background:var(--warning-bg);color:var(--warning-text)">Abwesend</span></td></tr>
<tr><td><b>Winkler, Michaela</b></td><td class="num">1590</td><td>Technik</td><td class="num">01.10.2026</td>
<td><span class="chip" style="background:var(--accent-200);color:var(--brand-700)">Geplant</span></td></tr>
</tbody>
</table>
<p style="font-size:12px;color:var(--ink-muted);margin:12px 0 0">
Die Zeile unter dem Zeiger wird blau angedeutet, nicht rosa: gegen die warme
Seitenflaeche ist ein kuehler Ton deutlicher, und Blau heisst in dieser
Oberflaeche „reagiert auf dich".</p>
</div>
<div class="card">
<h3>Navigation</h3>
<div class="nav">
<div class="hd">
<img src="../public/brand/manner-logo.svg" alt="Manner" width="60" height="24"
style="padding:8px;box-sizing:content-box">
<span style="font-weight:800;font-size:15px;color:var(--brand-700)">Alpenwerk HR</span>
</div>
<a href="#" class="active"><span class="dot"></span>Uebersicht</a>
<a href="#"><span class="dot"></span>Mitarbeiter:innen</a>
<a href="#"><span class="dot"></span>Organigramm</a>
</div>
</div>
<div class="card">
<h3>Organigramm — die vier Knotenarten</h3>
<div style="display:flex;gap:8px;flex-wrap:wrap;font-size:12px;font-weight:700">
<span style="display:flex;align-items:center;gap:6px"><span style="width:6px;height:26px;border-radius:3px;background:var(--brand-500)"></span>Person</span>
<span style="display:flex;align-items:center;gap:6px"><span style="width:6px;height:26px;border-radius:3px;background:var(--purple-text)"></span>Rolle</span>
<span style="display:flex;align-items:center;gap:6px"><span style="width:6px;height:26px;border-radius:3px;background:var(--ink-muted)"></span>Einheit</span>
<span style="display:flex;align-items:center;gap:6px"><span style="width:6px;height:26px;border-radius:3px;background:var(--accent-500)"></span>offene Stelle</span>
</div>
<p style="font-size:12px;color:var(--ink-muted);margin:14px 0 0">
Die offene Stelle war vorher ein blasses Blau und damit eine schwaechere Person.
In Rosa ist sie etwas anderes — auch dann noch, wenn herausgezoomt nur die
Streifen uebrig sind.</p>
</div>
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<h2>5 · Das Logo und sein Freiraum</h2>
<p>CD §4.1 verlangt „genuegend Freiraum rundum (X, X/3)". X ist hier als Hoehe des
Logos gelesen, der Freiraum also als ein Drittel davon — die gestrichelte Linie zeigt
ihn. Er steckt im Bauteil selbst, nicht in den Aufrufstellen.</p>
<div class="logo-demo">
<div class="on-white">
<span class="freiraum">
<img src="../public/brand/manner-logo.svg" alt="Manner" width="150" height="60"
style="padding:20px;box-sizing:content-box;display:block">
</span>
<div style="font-size:11px;color:var(--ink-muted);margin-top:8px">auf Weiss</div>
</div>
<div class="on-blue">
<span class="freiraum">
<img src="../public/brand/manner-logo.svg" alt="Manner" width="150" height="60"
style="padding:20px;box-sizing:content-box;display:block">
</span>
<div style="font-size:11px;color:#ffffffaa;margin-top:8px">auf dem Panel der Anmeldeseite</div>
</div>
</div>
<p style="font-size:12px;color:var(--ink-muted)">
§1.2: der Schriftzug darf ausschliesslich zur Gaenze auf Rosa stehen. Die Datei
bringt ihr rosa Feld mit, das Logo ist damit auf jedem Untergrund regelkonform.</p>
<!-- ══════════════════════════════════════════════════════════ -->
<h2>6 · Was hier noch nicht entschieden ist</h2>
<div class="card">
<p style="margin-top:0"><b>Das Logo ist das Markenlogo, nicht das Unternehmenslogo.</b>
Nach §3.2 waere fuer eine Anwendung, hinter der „Josef Manner &amp; Comp. AG" als
Absender steht, das Unternehmenslogo mit dem Stephansdom-Medaillon das richtige. Es
liegt nicht vor. Die offiziellen Dateien gaebe es bei der Kreativdirektion — der
Kontakt steht auf Seite 7 des Manuals.</p>
<p><b>Die Vorlage ist aus dem PDF gezogen, nicht das Original.</b> Fuer die Testphase
traegt das; fuer die Uebergabe gehoert die offizielle Datei hinein.</p>
<p><b>Die Schrift ist Barlow, nicht DIN.</b> Nachgezaehlt ist das Manual zu 95 % in
DIN gesetzt (DIN-Regular 79 %, DIN-Bold 16 %); Helvetica Neue steht nur in den
Visitenkarten-Mustern und im Claim. DIN ist aber lizenzpflichtig, und eine
Drucklizenz deckt die Auslieferung als Webfont nicht ab. Die freien Nachbauten der
DIN 1451 sind Schilderschriften und bei 14 px in einer langen Tabelle schlechter
lesbar als das, was sie ersetzen. Barlow ist dieselbe Grotesk-Familie mit schmalen
Proportionen und geraden Abschluessen. Liegt eine Web-Lizenz fuer DIN vor, ist der
Wechsel eine Zeile in <code>app/layout.tsx</code>.</p>
<p style="margin-bottom:0"><b>Die Symbole sind nachgerechnet, nicht neu exportiert.</b>
Favicon, Apple-Symbol und die Manifest-Symbole kamen als 24-Bit-RGB und trugen
<code>#034DA1</code> als Blau. Sie liegen jetzt als RGBA vor — das verlangt der
Bildpfad des Bauwerkzeugs — und ihr Blau ist auf <code>#164194</code> gezogen,
inklusive der kantengeglaetteten Raender: je Pixel wurde bestimmt, wieviel Blau
darin steckt, und derselbe Anteil neu gemischt. Betroffen waren 5 bis 10 Prozent
der Pixel je Datei. Ein Neuexport aus der korrigierten Vorlage bliebe trotzdem der
sauberere Weg, sobald die offiziellen Dateien da sind.</p>
</div>
</div>
</body>
</html>

View File

@@ -1,14 +1,19 @@
import type { EmploymentStatus, NoteCategory } from "./types"; import type { EmploymentStatus, NoteCategory } from "./types";
// Die Initialen stehen in Weiss darauf, jeder Ton braucht also 4.5:1 gegen
// Weiss — nachgerechnet liegt der schwaechste bei 6.11:1. Die Reihe beginnt
// bei den beiden Markenfarben (Blau direkt, Rosa in der dunklen Stufe, weil
// #F69686 selbst nur 2.18:1 traegt) und faechert von dort auf, damit zwei
// Personen nebeneinander nicht denselben Kreis bekommen.
const AVATAR_PALETTE = [ const AVATAR_PALETTE = [
"#d6046e", "#164194", // Manner Blau
"#00666d", "#0d2857",
"#a8402e", // dunkles Manner Rosa
"#5c2e91", "#5c2e91",
"#835b00",
"#0e700e", "#0e700e",
"#b0035a", "#835b00",
"#2b6cb0", "#00666d",
"#a30354", "#8a2740",
]; ];
// Stable per-employee avatar color when employees.avatar_color isn't set. // Stable per-employee avatar color when employees.avatar_color isn't set.
@@ -24,7 +29,10 @@ export function avatarColorFor(seed: string): string {
export const STATUS_STYLES: Record<EmploymentStatus, string> = { export const STATUS_STYLES: Record<EmploymentStatus, string> = {
Aktiv: "bg-success-bg text-success-text", Aktiv: "bg-success-bg text-success-text",
Karenz: "bg-warning-bg text-warning-text", Karenz: "bg-warning-bg text-warning-text",
Geplant: "bg-brand-100 text-brand-700", // Rosa statt Blau: „Geplant" ist kein Hinweis und keine Warnung, sondern
// der Normalfall ohne besondere Faerbung — und dafuer ist die Markenfarbe
// die richtige. Blau wuerde hier nach Bedienelement aussehen.
Geplant: "bg-accent-200 text-brand-700",
Ausgetreten: "bg-danger-bg text-danger-text", Ausgetreten: "bg-danger-bg text-danger-text",
}; };
@@ -36,7 +44,9 @@ export const CATEGORY_STYLES: Record<ColorCategory, string> = {
warning: "bg-warning-bg text-warning-text", warning: "bg-warning-bg text-warning-text",
info: "bg-info-bg text-info-text", info: "bg-info-bg text-info-text",
purple: "bg-purple-bg text-purple-text", purple: "bg-purple-bg text-purple-text",
brand: "bg-brand-100 text-brand-700", // Der Chip ohne eigene Bedeutung. Auf accent-200 statt accent-100, weil
// die hellere Stufe dem Fehler-Chip zu nahe kommt (dE 5.6 gegen 13.7).
brand: "bg-accent-200 text-brand-700",
}; };
// Audit-log / activity-feed action -> badge color, per the action list in // Audit-log / activity-feed action -> badge color, per the action list in

BIN
public/brand/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/brand/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { actionBadgeStyle, avatarColorFor, STATUS_STYLES } from "@/lib/colors"; import { actionBadgeStyle, avatarColorFor, CATEGORY_STYLES, STATUS_STYLES } from "@/lib/colors";
describe("avatarColorFor", () => { describe("avatarColorFor", () => {
it("is deterministic for the same seed", () => { it("is deterministic for the same seed", () => {
@@ -23,7 +23,11 @@ describe("actionBadgeStyle", () => {
expect(actionBadgeStyle("Neueinstellung")).toBe(STATUS_STYLES.Aktiv); expect(actionBadgeStyle("Neueinstellung")).toBe(STATUS_STYLES.Aktiv);
}); });
it("falls back to the brand style for an unknown action", () => { // Gegen CATEGORY_STYLES.brand geprüft und nicht gegen einen abgetippten
expect(actionBadgeStyle("Irgendwas Unbekanntes")).toContain("bg-brand-100"); // Klassennamen: welche Farbe der Chip ohne eigene Bedeutung trägt, ist eine
// Frage der Palette und hat sich mit der Umstellung auf das Manner-CD schon
// einmal geändert. Der Test soll die Zuordnung sichern, nicht den Farbton.
it("falls back to the neutral category style for an unknown action", () => {
expect(actionBadgeStyle("Irgendwas Unbekanntes")).toBe(CATEGORY_STYLES.brand);
}); });
}); });