Die Oberflaeche auf das Manner-CD umstellen
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>
@@ -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
|
After Width: | Height: | Size: 8.0 KiB |
BIN
app/favicon.ico
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -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,
|
||||||
|
|||||||
@@ -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
|
After Width: | Height: | Size: 73 KiB |
@@ -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
@@ -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": "/"
|
||||||
|
}
|
||||||
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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" ? (
|
||||||
|
|||||||
@@ -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 }) {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
@@ -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 & 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>
|
||||||
@@ -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
|
After Width: | Height: | Size: 9.4 KiB |
BIN
public/brand/icon-512-maskable.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/brand/icon-512.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
312
public/brand/manner-logo.svg
Normal file
|
After Width: | Height: | Size: 73 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||