Fundament: Branding Craftvia
- Farb-Tokens laut Brandbook §11.4 (Lotsenblau, Signalorange, Graphit, Hafengrau, Stahlgrau, Zink, Funktionsfarben) in globals.css + src/lib/brand.ts; helles Theme - Inline-SVG-Logo src/components/brand/craftvia-logo.tsx (horizontal/signet, color/mono/reversed, App-Icon-Kachel, optionale Tagline); Certvia-/GEFIM-Logos entfernt - Favicon/PWA-Icons aus dem Signet erzeugt (scripts/generate-brand-icons.ts), site.webmanifest (Craftvia, theme_color #082E5B) - Inter selbst gehostet (next/font/local, lokale OFL-Datei), Open Sans/Poppins entfernt - Metadaten, WebAuthn-RP-Name, Mail-/Dokument-CD auf Craftvia umgestellt - docs/craftvia/BRANDING.md ersetzt docs/BRANDING-CERTVIA.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@@ -1,149 +0,0 @@
|
||||
# Branding-Umstellung auf Certvia — Inventar & Umsetzung
|
||||
|
||||
> Branch `dev-branding-certvia` (Basis `dev`) · Quelle: `Aufgabenpaket-Branding-Certvia.md` + `Certvia-Assets/`
|
||||
> Leitplanke: **GEFIM bleibt Dachmarke** („Ein Produkt von GEFIM"), nur die **Produktmarke** wird Certvia.
|
||||
> Reines Branding — **keine Funktionsänderung**, kein Umbau von Logik oder Datenmodell.
|
||||
|
||||
---
|
||||
|
||||
## 1. Design-Tokens (verbindliche Referenz)
|
||||
|
||||
**Markenfarben** (Logo, Print, Verläufe — `--brand-*`):
|
||||
|
||||
| Rolle | Token | Hex |
|
||||
|---|---|---|
|
||||
| Violett (Marke) | `--brand-violet` | `#5d52a3` |
|
||||
| Magenta (Akzent „via", Haken) | `--brand-magenta` | `#812d80` |
|
||||
| Hellblau | `--brand-blue` | `#8dc4e0` |
|
||||
| Anthrazit (Text/Headlines) | `--brand-anthracite` | `#3b3b3a` |
|
||||
| Violett auf Dunkel | `--brand-violet-on-dark` | `#7d6fd6` |
|
||||
| Magenta auf Dunkel | `--brand-magenta-on-dark` | `#d17bcf` |
|
||||
|
||||
**UI Dark-Theme** (unverändert — **ist** die Certvia-Produktpalette, nicht „korrigieren"):
|
||||
`--ui-primary #7d6fd6` · `--ui-primary-deep #5d52a3` · `--ui-blue #8dc4e0` · `--ui-magenta #b45bb0` ·
|
||||
`--bg-0 #0e1220` · `--bg-1 #141a2e` · `--elevated #1a2138` · `--panel rgba(30,38,64,.72)` ·
|
||||
`--panel-brd rgba(120,135,180,.18)` · `--foreground #e8ecf7` · `--muted-foreground #8b93ad` ·
|
||||
`--ok #39c07f` · `--warn #f0ad4e` · `--risk #ff6b6b` · Primär-Verlauf `linear-gradient(135deg,#5d52a3,#7d6fd6)`
|
||||
· Fokus-Ring `rgba(125,111,214,.35)`.
|
||||
|
||||
Single Source of Truth: **`src/app/globals.css`** (CSS) und **`src/lib/brand.ts`** (JS/TS — für alles,
|
||||
was CSS-Variablen nicht auflösen kann: Canvas-/PNG-Export, Metadaten, künftige Dokument-Exporte).
|
||||
|
||||
**Typografie:** Headlines/Wortmarke **Poppins**, Fließtext **Open Sans** — self-hosted (`src/app/fonts/`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Inventar (Story S0)
|
||||
|
||||
### 2.1 Produktmarke → Certvia
|
||||
|
||||
| Datei | Fundstelle | Maßnahme | Story |
|
||||
|---|---|---|---|
|
||||
| `src/app/globals.css` | `--gefim-violet/-deep/-blue/-magenta`, Kommentare „GEFIM-Theme" | Umbenannt auf `--brand-*` / `--ui-*`, Kommentare auf Certvia | S1/S9 |
|
||||
| `src/app/globals.css` | Flächenfarben direkt als Hex (`#0e1220`, `#141a2e`, …) | Über `--bg-0`/`--bg-1`/`--elevated` geführt, semantische Tokens referenzieren sie | S1 |
|
||||
| `src/components/risk-modals.tsx` | `bg-[#0e1220]` (Rest-Risiko-Marker, Legende) | → `bg-[var(--bg-0)]` | S1 |
|
||||
| `src/app/(app)/risks/page.tsx` | `bg-[#0e1220]/75`, `hover:bg-[#0e1220]` (Heatmap-Chip) | → Token `--chip-overlay` / `var(--bg-0)` | S1 |
|
||||
| `src/components/dependency-graph.tsx` | `#8dc4e0`, `#ff6b6b`, `#141a2e`, `#0e1220`, `#4a5372`, `#8b93ad` | CSS-Vars wo möglich; PNG-Export nutzt `BRAND.ui` aus `src/lib/brand.ts` (html-to-image löst keine CSS-Vars auf) | S1 |
|
||||
| `public/gefim-logo.png` | Produktlogo in Sidebar + Login | Verschoben nach `public/assets/logo/gefim-logo.png`, dient nur noch der **Dachmarke** | S2 |
|
||||
| `src/app/(app)/layout.tsx` | Sidebar-Logo `/gefim-logo.png`, Alt-Text „GEFIM …" | `<CertviaLogo variant="lockup" theme="dark">` über den Branding-Resolver | S2/S8 |
|
||||
| `src/app/login/page.tsx` | Logo `/gefim-logo.png`, Alt-Text | Certvia-Lockup + Tagline + Dachmarken-Fußnote | S2/S7 |
|
||||
| `src/app/platform/login/page.tsx` | kein Logo | Certvia-Lockup + „Plattform-Administration" | S2/S7 |
|
||||
| `src/app/(platform)/layout.tsx` | `ShieldCheck` + „ISMS · Plattform-Administration" | Certvia-Mark + „Certvia · Plattform-Administration" | S2/S3 |
|
||||
| `src/app/layout.tsx` | `title: "ISMS-Tool"`, keine Icons/OG/Manifest | Vollständige Certvia-Metadaten inkl. Icons, Manifest, `themeColor` | S3 |
|
||||
| `src/app/favicon.ico` | Next-Default-Favicon | Entfernt; Icons laufen über `public/favicon/` + `metadata.icons` | S2 |
|
||||
| `messages/de.json`, `messages/en.json` | `common.appName` = „ISMS-Tool"/„ISMS Tool" | → „Certvia" (+ `appTagline`, `appByline`) | S3 |
|
||||
| `src/server/mfa.ts` | TOTP-Issuer `"ISMS-Tool · Plattform"` | → `"Certvia · Plattform"` | S3 |
|
||||
| `src/components/ui/button.tsx` | Kommentar „GEFIM-Verlauf" | → „Certvia-Verlauf" | S9 |
|
||||
| `src/components/mockup-ui.tsx` | Kommentar-Referenz auf Mockup-Datei | Formulierung entschärft (Dateiname bleibt, s. u.) | S9 |
|
||||
| `src/lib/control-titles.ts` | Kommentar „aus dem GEFIM-Mockup übernommen" | → neutral „aus dem UI-Mockup" | S9 |
|
||||
| `src/app/(app)/settings/page.tsx` | Hinweis „Logo-Upload folgt in Phase 2" | Ergänzt um Certvia-Default-Aussage | S8 |
|
||||
| `src/proxy.ts` | Matcher schloss `.webmanifest` nicht aus | `/site.webmanifest` lieferte die Login-HTML statt JSON → Endung ergänzt (siehe §5) | S3 |
|
||||
|
||||
### 2.2 Dachmarke GEFIM → bleibt bewusst erhalten
|
||||
|
||||
| Ort | Grund |
|
||||
|---|---|
|
||||
| `public/assets/logo/gefim-logo.png` | Dachmarken-Logo für „Ein Produkt von GEFIM" |
|
||||
| `src/components/brand/powered-by-gefim.tsx` | Neue Komponente, die genau diesen Hinweis rendert (Login, Plattform-Login, Print-/Export-Fußzeile, Mail-Footer) |
|
||||
| `docs/ISMS-*-GEFIM.html`, Verweise in `README.md`/`AGENTS.md`/`docs/SPEC.md` | Historische Mockups/Spezifikation — Projektartefakte, keine Produkt-UI |
|
||||
| `prisma/seed.ts` (`ORG_NAME` „GEFIM Demo GmbH") | **Demo-Mandantendaten** (fiktiver Kunde), nicht der Produktname |
|
||||
| `prisma/import-managed.ts` (`portal.gefim.example`) | Demo-Registerzeile, nicht der Produktname |
|
||||
|
||||
Nach der Umstellung liefert `rg -i "gefim"` ausschließlich diese Kategorien.
|
||||
|
||||
---
|
||||
|
||||
## 3. Umgesetzte Stories
|
||||
|
||||
| Story | Inhalt | Status |
|
||||
|---|---|---|
|
||||
| **S0** | Inventar (dieses Dokument) | ✅ |
|
||||
| **S1** | Token-Zentralisierung `--brand-*` / `--ui-*`, `src/lib/brand.ts`, hartkodierte Farben ersetzt | ✅ |
|
||||
| **S2** | Assets eingebunden, `<CertviaLogo>`, Favicon/PWA-Icons, Sidebar + beide Logins | ✅ |
|
||||
| **S3** | App-Name/Meta/OG/Manifest/i18n/MFA-Issuer auf Certvia | ✅ |
|
||||
| **S4** | Poppins + Open Sans self-hosted bestätigt (waren es bereits), Fallback-Stack + `swap` geprüft | ✅ |
|
||||
| **S7** | Login, Plattform-Login, Change-Password, Enroll-MFA, Konto-deaktiviert, 404, 500 gebranded | ✅ |
|
||||
| **S9** | Produkt-Referenzen bereinigt, Dachmarke belassen | ✅ |
|
||||
| **S5** | Branding-Layer für Dokument-/Export-CD (`src/lib/brand.ts` + Print-Stylesheet) | ✅ (Andockpunkt) |
|
||||
| **S8** | Branding-Resolver mit Certvia-Default, Custom überschreibt nur wenn gesetzt | ✅ |
|
||||
| **S6** | Brandfähige E-Mail-Template-Basis (SMTP selbst folgt in Paket 4) | ✅ (Basis) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Wo liegt was (neu)
|
||||
|
||||
| Pfad | Inhalt |
|
||||
|---|---|
|
||||
| `src/lib/brand.ts` | Marken-/UI-Farben als JS-Konstanten, Produktname/Tagline/Byline, Asset-Pfade, `resolveTenantBranding()` |
|
||||
| `src/lib/document-brand.ts` | Dokument-CD (helles Theme, Kopf-/Fußzeilen-Bausteine) — Andockpunkt für DOCX/PDF |
|
||||
| `src/lib/email-brand.ts` | Brandfähige HTML-/Text-Mail-Basis für Paket 4 |
|
||||
| `src/components/brand/certvia-logo.tsx` | `<CertviaLogo variant="lockup\|mark" theme="light\|dark" />` als Inline-SVG |
|
||||
| `src/components/brand/tenant-brand.tsx` | Mandanten-Marke mit Certvia-Fallback |
|
||||
| `src/components/brand/powered-by-gefim.tsx` | Dachmarken-Hinweis inkl. GEFIM-Logo |
|
||||
| `public/assets/logo/` | Certvia-SVGs/PNGs + GEFIM-Dachmarkenlogo |
|
||||
| `public/favicon/`, `public/favicon.ico`, `public/site.webmanifest` | App-Icons und PWA-Manifest |
|
||||
| `docs/branding/` | Aufgabenpaket + Logo-README aus dem Design-Paket |
|
||||
| `src/app/globals.css` (`@media print`) | Druck-/Dokument-CD |
|
||||
|
||||
---
|
||||
|
||||
## 5. Beim Einbau aufgefallen (Abweichungen vom Aufgabenpaket)
|
||||
|
||||
- **Lockup-SVG trägt Poppins jetzt eingebettet.** `public/assets/logo/certvia-lockup-positiv.svg`
|
||||
enthält Poppins 300 + 700 als woff2-Data-URI (≈22 KB). Damit erscheint die Wortmarke auch
|
||||
außerhalb der App korrekt (Druck, Export, Mail) — der offene Punkt „Text in Pfade wandeln" aus
|
||||
`docs/branding/README-Logo.md` ist damit erledigt. Geometrie und Farben sind unverändert.
|
||||
In der App rendert `<CertviaLogo>` das Zeichen ohnehin inline und nutzt die App-Schrift direkt.
|
||||
- **`/site.webmanifest` war nicht erreichbar.** Der Route-Gate-Matcher in `src/proxy.ts` nahm nur
|
||||
`svg|png|jpg|ico` aus — für `.webmanifest` lieferte der Server die Login-Seite statt JSON, das
|
||||
PWA-Manifest wäre also wirkungslos geblieben. Endung ergänzt. Bewusst **keine** Verzeichnis-
|
||||
Ausnahme für `assets/`: das ist zugleich die App-Route des Asset-Inventars und bleibt hinter dem
|
||||
Gate (nachgeprüft: `/assets`, `/assets/xyz` → 307 auf `/login`).
|
||||
- **`src/app/favicon.ico` entfernt.** Die Datei-Konvention von Next.js und eine `public/favicon.ico`
|
||||
schließen sich gegenseitig aus; die Icons laufen jetzt vollständig über `metadata.icons`.
|
||||
- **404-/500-Seiten neu angelegt** (`src/app/not-found.tsx`, `src/app/error.tsx`) — es gab bisher
|
||||
keine. Sie erscheinen für angemeldete Nutzer; anonyme Aufrufe fängt weiterhin das Login-Gate ab.
|
||||
|
||||
### Kontrast (WCAG AA, geprüft)
|
||||
|
||||
| Kombination | Kontrast | Bewertung |
|
||||
|---|---|---|
|
||||
| Magenta `#812d80` auf Weiß (Wortmarke/Print) | 8,0 : 1 | AA + AAA |
|
||||
| Magenta `#d17bcf` auf Karte `#141a2e` | 6,1 : 1 | AA |
|
||||
| Magenta `#d17bcf` auf Seitengrund `#0e1220` | 6,6 : 1 | AA |
|
||||
|
||||
---
|
||||
|
||||
## 6. Bewusste Abgrenzungen
|
||||
|
||||
- **UI-Palette unverändert.** Die Dark-Theme-Farben wurden nur konsolidiert/umbenannt, nicht geändert.
|
||||
`--brand-violet` (`#5d52a3`) gilt für Logo/Print/Verläufe, `--ui-primary` (`#7d6fd6`) für Interaktion.
|
||||
- **Kein Light-Mode.** Die App ist bewusst dark-only; `<CertviaLogo theme="light">` existiert für
|
||||
Dokument-/Druck-/Mail-Kontexte, wird in der UI aber nicht verwendet.
|
||||
- **Poppins 400** ist nicht als woff2 im Repo (vorhanden: 300/500/600/700). Aktuell nutzt nichts
|
||||
Poppins 400 — Headlines laufen auf 600/700, die Wortmarke auf 300/700. Sollte 400 gebraucht werden,
|
||||
muss die woff2 ergänzt werden; ein CDN-Fetch bleibt ausgeschlossen.
|
||||
- **Logo-Upload je Mandant** braucht Objektspeicher (Admin Phase 2). Der Resolver ist bereits darauf
|
||||
vorbereitet (`logoUrl` optional), das Feld existiert im Schema noch nicht — bewusst keine Migration.
|
||||
- **DOCX/PDF-Export** existiert noch nicht. S5 liefert den Branding-Layer (Konstanten + Kopf-/Fußzeile
|
||||
+ Print-CSS), an den das Export-Feature andockt.
|
||||
@@ -1,107 +0,0 @@
|
||||
# Umsetzungspaket — Vollständige Branding-Umstellung auf **Certvia**
|
||||
|
||||
> Claude-Code-Prompt für **einen** Entwickler. Basis-Branch **`dev`**, Feature-Branch **`dev/branding-certvia`** (PR-Ziel `dev`).
|
||||
> Quelle der Wahrheit: **Certvia Brand-Board** (`Certvia-Brand-Board-final.html`) + Logo-SVGs (`certvia-mark-positiv/negativ/grau/anthrazit.svg`, `certvia-favicon.svg`, `certvia-lockup-positiv.svg`) aus `Certvia-Design-Uebergabe.zip`.
|
||||
|
||||
## 0. Ziel & Leitplanken
|
||||
Die App vollständig auf den **Certvia-Styleguide** umstellen — Logo, Wortmarke, App-Name, Favicon/Icons, Typografie, Marken-/UI-Farben, Dokument-/Export-Design und alle Texte.
|
||||
**Wichtige Leitplanken:**
|
||||
- **GEFIM bleibt Dachmarke.** „**Ein Produkt von GEFIM**" (inkl. GEFIM-Logo) bleibt im Footer/Impressum/Export-Fußzeile erhalten. Nur die **Produktmarke** wird Certvia.
|
||||
- **UI-Farben bleiben wie initial festgelegt** (Dark-Theme-Primär `#7d6fd6` etc.) — sie **sind** die Certvia-UI-Palette. Nicht „korrigieren". Marken-Violett `#5d52a3` gilt für Logo/Print/Verläufe, UI-Interaktiv bleibt `#7d6fd6`.
|
||||
- **Keine Funktionsänderung** — reines Branding/Design. Kein Umbau von Logik/Datenmodell.
|
||||
|
||||
---
|
||||
|
||||
## 1. Design-Tokens (verbindliche Referenz)
|
||||
|
||||
**Markenfarben (Logo, Print, Verläufe, Akzent):**
|
||||
| Rolle | Hex |
|
||||
|---|---|
|
||||
| Violett (Marke) | `#5d52a3` |
|
||||
| Magenta (Akzent: „via", Haken, Highlights) | `#812d80` |
|
||||
| Hellblau | `#8dc4e0` |
|
||||
| Anthrazit (Text/Headlines) | `#3b3b3a` |
|
||||
|
||||
**UI Dark-Theme (unverändert, = Certvia-Produktpalette):**
|
||||
`--violet:#7d6fd6` · `--violet-deep:#5d52a3` · `--blue:#8dc4e0` · `--magenta:#b45bb0` · `--bg0:#0e1220` · `--bg1:#141a2e` · `--panel:rgba(30,38,64,.72)` · `--panel-b:rgba(120,135,180,.18)` · `--elev:#1a2138` · `--txt:#e8ecf7` · `--muted:#8b93ad` · `--ok:#39c07f` · `--warn:#f0ad4e` · `--risk:#ff6b6b` · Primär-Verlauf `linear-gradient(135deg,#5d52a3,#7d6fd6)` · Fokus-Ring `rgba(125,111,214,.35)`.
|
||||
|
||||
**Typografie:** Headlines/Wortmarke **Poppins**, Fließtext **Open Sans** (bereits im Einsatz — bestätigen, **selbst hosten**, kein ungefragtes Google-Fonts-CDN).
|
||||
|
||||
**Logo/Marke:** C-Monogramm (offenes „C" Violett `#5d52a3` + Haken Magenta `#812d80`, Haken schließt bündig an die C-Öffnung). Wortmarke „Cert" (Poppins Light, Anthrazit) + „**via**" (Poppins Bold, Magenta). Auf dunklem Grund: C `#7d6fd6`, Haken `#d17bcf`, Text weiß.
|
||||
|
||||
---
|
||||
|
||||
## 2. Story B0 — Bestandsaufnahme / Branding-Inventar (zuerst!) (S)
|
||||
**Als** Entwickler **möchte ich** alle Branding-Berührungspunkte finden, **damit** nichts übersehen wird.
|
||||
- Repo-weit suchen und als kurze Inventarliste (Datei → Fundstelle → Maßnahme) dokumentieren:
|
||||
```bash
|
||||
rg -i "gefim" -l # Produktname vs. Dachmarke unterscheiden
|
||||
rg -i "shield|logo|brand|favicon|wordmark|app[_-]?name|siteName|title" src public
|
||||
rg -i "#5d52a3|#7d6fd6|#812d80|#8dc4e0|#b45bb0|#0e1220" src # hartkodierte Farben außerhalb der Tokens
|
||||
rg -i "poppins|open sans|fonts.googleapis" src public
|
||||
```
|
||||
- **AK:** Inventarliste liegt vor; jede Fundstelle ist einer der Stories S1–S9 zugeordnet; klar getrennt „Produktmarke → Certvia" vs. „Dachmarke → GEFIM bleibt".
|
||||
|
||||
---
|
||||
|
||||
## 3. Stories
|
||||
|
||||
### S1 — Design-Tokens zentralisieren & Certvia-Palette verankern (M)
|
||||
- Eine **zentrale Token-Quelle** (CSS-Variablen/`theme`/Tailwind-Config) als Single Source of Truth; hartkodierte Farben aus S0 durch Tokens ersetzen.
|
||||
- Marken- vs. UI-Tokens klar benennen (`--brand-violet` `#5d52a3` vs. `--ui-primary` `#7d6fd6`), damit Logo/Print und UI nicht vermischt werden.
|
||||
- **AK:** keine hartkodierten Markenfarben mehr in Komponenten; Umschalten zentral möglich; UI optisch unverändert (nur konsolidiert).
|
||||
|
||||
### S2 — Logo, Wortmarke & Icons (M)
|
||||
- **App-Header/Sidebar**, **Login** (`/login`) und **Plattform-Login** (`/platform/login`): GEFIM-Schild → **Certvia-Logo** (Lockup C-Monogramm + „Certvia"). Dark-Variante nutzen.
|
||||
- **Favicon** + **PWA/Manifest-Icons** (`public/`, `manifest.webmanifest`, `apple-touch-icon`) auf `certvia-favicon.svg` (C auf Violett-Tile) und generierte PNGs (32/180/192/512).
|
||||
- `meta-theme-color` = `#5d52a3` (schon so auf der Website).
|
||||
- SVG-Assets aus dem Design-Paket in `public/assets/logo/` bzw. `src/components/brand/` einbinden; **Logo als React-Komponente** `<CertviaLogo variant="lockup|mark" theme="light|dark" />`.
|
||||
- **AK:** in allen App-Bereichen erscheint das Certvia-Logo; Favicon/Tab-Icon = Certvia; keine GEFIM-Schild-Reste in Produkt-UI.
|
||||
|
||||
### S3 — App-Name, Titel, Meta & i18n-Texte (S–M)
|
||||
- App-Name/`<title>`/OG-/Twitter-Meta, PWA-Name, Sidebar-Kopf, „Über"/Version-Info → **Certvia**.
|
||||
- i18n (`de`/`en`): alle Produktname-Strings auf „Certvia"; Tagline „Informationssicherheit. Endlich einfach." wo passend.
|
||||
- **AK:** Browser-Tab, Meta, Manifest und sichtbare Produktnamen zeigen „Certvia"; „ein Produkt von GEFIM" bleibt erhalten.
|
||||
|
||||
### S4 — Typografie bestätigen & selbst hosten (S)
|
||||
- Poppins (300/400/500/600/700) + Open Sans (400/600/700) **self-hosted** (woff2) statt CDN; Fallback-Stack; `font-display: swap`.
|
||||
- **AK:** keine externen Font-Requests; Wortmarke „Cert**via**" korrekt (Light + Bold), Headlines Poppins, Fließtext Open Sans.
|
||||
|
||||
### S5 — Dokument-/Export-Corporate-Design (M–L)
|
||||
- **DOCX/PDF/Print-Export** (Richtlinien, VDA-ISA-Katalog, Reports) auf **Certvia-Corporate-Design**: Deckblatt/Kopf mit Certvia-Logo + Farben, Fußzeile „Certvia — ein Produkt von GEFIM" + GEFIM-Logo, Poppins/Open Sans.
|
||||
- Betrifft den (offenen) DOCX/PDF-Export — Branding-Layer so bauen, dass er beim Export-Feature andockt.
|
||||
- **AK:** exportierte Dokumente tragen Certvia-Branding; GEFIM als Dachmarke in der Fußzeile.
|
||||
|
||||
### S6 — E-Mail-/Benachrichtigungs-Templates (S–M, falls vorhanden)
|
||||
- Sobald SMTP/E-Mail (Paket 4) existiert bzw. für Einladungs-/Reset-Mails: Header/Footer/Farben/Logo auf Certvia; Absender-/Signatur-Text „Certvia — ein Produkt von GEFIM".
|
||||
- Falls E-Mail noch nicht aktiv: nur die **Template-Basis** brandfähig vorbereiten.
|
||||
- **AK:** vorhandene Mail-Templates gebranded oder brandfähige Basis vorbereitet.
|
||||
|
||||
### S7 — Auth-, Fehler- & Systemseiten (S)
|
||||
- Login, Change-Password, deaktiviertes Konto, 404/500, Wartungs-/Status-Banner, Loading-/Empty-States: Certvia-Logo + Palette.
|
||||
- **AK:** keine ungebrandeten/gemischten Screens mehr.
|
||||
|
||||
### S8 — Mandanten-Branding-Default = Certvia (S–M)
|
||||
- Der Whitelabel-/Branding-Fallback je Mandant (Admin Phase 2 „Logo-Upload") ist **Certvia** als Default; Custom-Logo überschreibt nur, wenn gesetzt.
|
||||
- **AK:** ohne Custom-Logo zeigt jeder Mandant Certvia; „ein Produkt von GEFIM" bleibt unabhängig davon.
|
||||
|
||||
### S9 — Restliche GEFIM-Referenzen bereinigen (S)
|
||||
- Alle in S0 gefundenen **Produkt**-Referenzen „GEFIM" → „Certvia" (Kommentare, Alt-Texte, aria-labels, Klassennamen unkritisch). **Dachmarken-Hinweise bewusst belassen.**
|
||||
- **AK:** `rg -i "gefim"` liefert nur noch Dachmarken-Kontext (Footer/Impressum/Export-Fußzeile/Assets `gefim/`).
|
||||
|
||||
---
|
||||
|
||||
## 4. QA / Definition of Done
|
||||
- **Visuelles Review** der Kernscreens (Dashboard, Richtlinien, Assets, Risiken, Lieferanten, Aufgaben, Admin, beide Logins, Export, Fehlerseiten) hell **und** dunkel — Screenshots im PR.
|
||||
- **Kontrast prüfen:** Magenta `#812d80` auf Weiß und `#d17bcf` auf Dunkel (WCAG AA für Text/aktive Elemente).
|
||||
- `rg -i "gefim"` = nur Dachmarken-Kontext; keine externen Font-/Logo-Requests; keine hartkodierten Markenfarben.
|
||||
- `npx tsc --noEmit` → `npm run lint` → `npm run build` grün; Demo-Umgebung lauffähig.
|
||||
- **Reines Branding-PR** — keine funktionalen Diffs.
|
||||
|
||||
## 5. Reihenfolge
|
||||
S0 (Inventar) → S1 (Tokens) → S2 (Logo/Icons) → S3 (Name/Meta/i18n) → S4 (Fonts) → S7 (Auth/Fehler) → S9 (Cleanup) → S5 (Export) → S8 (Mandanten-Default) → S6 (Mail, falls vorhanden).
|
||||
|
||||
## 6. Assets (mitliefern an den Entwickler)
|
||||
Aus `Certvia-Design-Uebergabe.zip`: `logo/certvia-mark-positiv.svg`, `-negativ.svg`, `-grau.svg`, `-anthrazit.svg`, `certvia-favicon.svg`, `certvia-lockup-positiv.svg`, `logo/README-Logo.md`, `Brand-Board.html` (Farb-/Typo-Referenz) sowie `gefim-dachmarke/` (GEFIM-Logo für den „ein Produkt von GEFIM"-Hinweis).
|
||||
|
||||
> Hinweis: Falls PNG-Rastergrößen (Favicon/Apple-Touch/PWA 32/180/192/512) benötigt werden, kann ich sie aus dem SVG erzeugen und beilegen — sag kurz Bescheid.
|
||||
@@ -1,29 +0,0 @@
|
||||
# Certvia — Logo-Dateien
|
||||
|
||||
Logo = **C-Monogramm + Haken** (Entwurf C) in GEFIM-Farben. Alle Marken (außer Lockup) sind quadratisch (viewBox 64×64), vektoriell und frei skalierbar.
|
||||
|
||||
## Dateien
|
||||
| Datei | Verwendung |
|
||||
|---|---|
|
||||
| `certvia-mark-positiv.svg` | Standard auf hellem Grund (C Violett, Haken Magenta) |
|
||||
| `certvia-mark-negativ.svg` | Auf farbigem/dunklem Grund (weiß) |
|
||||
| `certvia-mark-grau.svg` | Graustufen |
|
||||
| `certvia-mark-anthrazit.svg` | Einfarbig Anthrazit (Dokumente/Druck s/w) |
|
||||
| `certvia-favicon.svg` | App-Icon/Favicon (Monogramm auf violettem Tile) |
|
||||
| `certvia-lockup-positiv.svg` | Wortmarke horizontal (Zeichen + „Certvia") |
|
||||
|
||||
## Farben
|
||||
- **Violett (C):** `#5d52a3`
|
||||
- **Magenta (Haken & „via"):** `#812d80`
|
||||
- **Anthrazit (Wortmarke „Cert"):** `#3b3b3a`
|
||||
- Auf dunklem Grund/UI: C `#7d6fd6`, Haken `#d17bcf`.
|
||||
|
||||
## Konstruktion / Regeln
|
||||
- „C" ist **offen nach rechts**; der **Haken** liegt in der Öffnung („cert").
|
||||
- **Kein Verlauf** im Zeichen. „C" nicht schließen, nicht verzerren, Seitenverhältnis wahren.
|
||||
- **Schutzraum:** mind. Höhe des Haken-Elements rundum frei.
|
||||
- Wortmarke „Cert" (Poppins Light/300, Anthrazit) + „**via**" (Poppins Bold/700, Magenta).
|
||||
- Hinweis: SVG-Lockup nutzt Poppins per `font-family` — beim finalen Export ggf. **Text in Pfade wandeln**, damit die Schrift überall korrekt erscheint.
|
||||
|
||||
## Schrift
|
||||
- **Poppins** (Wortmarke/Headlines), **Open Sans** (Fließtext).
|
||||
@@ -0,0 +1,68 @@
|
||||
# Craftvia — Branding in der Anwendung
|
||||
|
||||
Maßgeblich ist das Brandbook: [BRANDBOOK.md](BRANDBOOK.md) (§11.4 Farben, §11.5 Typografie).
|
||||
Referenzbild: [assets/craftvia-brand-identity-v1.png](assets/craftvia-brand-identity-v1.png).
|
||||
Dieses Dokument beschreibt, **wo** die Marke im Code verankert ist.
|
||||
|
||||
## Quellen der Wahrheit
|
||||
|
||||
| Was | Datei |
|
||||
|---|---|
|
||||
| CSS-Tokens (Browser) | `src/app/globals.css` — `--brand-*` (Kernfarben), `--ui-*` (UI-Rollen), shadcn-Tokens abgeleitet |
|
||||
| JS-Konstanten (Metadaten, Mail, Dokumente) | `src/lib/brand.ts` — `BRAND`, `BRAND_COLORS`, `STATUS_COLORS`, `BRAND_FONTS` |
|
||||
| Logo (Inline-SVG) | `src/components/brand/craftvia-logo.tsx` |
|
||||
| Mandanten-Logo-Override | `src/components/brand/tenant-brand.tsx` + `resolveTenantBranding()` |
|
||||
| E-Mail-Layout | `src/lib/email-brand.ts` |
|
||||
| Dokument-/Druck-CD | `src/lib/document-brand.ts`, `@media print` in `globals.css` |
|
||||
| Favicon/PWA-Icons | `scripts/generate-brand-icons.ts` → `public/favicon/*`, `public/favicon.ico` |
|
||||
| Manifest | `public/site.webmanifest` (`theme_color` #082E5B) |
|
||||
|
||||
## Farben
|
||||
|
||||
| Rolle | Name | Hex | Token |
|
||||
|---|---|---|---|
|
||||
| Primär | Lotsenblau | `#082E5B` | `--brand-lotsenblau` → `--primary` |
|
||||
| Akzent/CTA | Signalorange | `#FF6A00` | `--brand-signalorange` → `--ui-accent` (`bg-cta`) |
|
||||
| Text | Graphit | `#34383D` | `--foreground` |
|
||||
| Fläche | Hafengrau | `#F3F5F7` | `--background` |
|
||||
| Sekundärtext | Stahlgrau | `#66727D` | `--muted-foreground` |
|
||||
| Linien | Zink | `#D9E0E5` | `--border`, `--input` |
|
||||
| Erfolg / Warnung / Fehler / Info | | `#23845D` / `#C87912` / `#C64242` / `#2F6FA3` | `--ok` / `--warn` / `--risk` / `--info` |
|
||||
|
||||
Regeln: Signalorange sparsam (eine Hauptaktion je Ansicht, aktive Elemente). Farbe nie
|
||||
als einziges Statussignal — immer mit Text oder Icon. Keine Verläufe, keine Schlagschatten
|
||||
(`--shadow-card` ist eine feine Kontur); die Utility-Namen `bg-grad*` zeigen Vollfarbe.
|
||||
|
||||
## Logo
|
||||
|
||||
`<CraftviaLogo />` — Props:
|
||||
|
||||
- `variant`: `"horizontal"` (Signet + Wortmarke, Default) | `"signet"`
|
||||
- `tone`: `"color"` (Default) | `"mono"` (einfarbig Lotsenblau) | `"reversed"` (weiß, für dunkle Flächen)
|
||||
- `tile` (nur Signet): App-Icon-Kachel in Lotsenblau mit weißem C und orangem Haken
|
||||
- `tagline`: „Handwerk. Digital auf Kurs." unter der Wortmarke
|
||||
- `height`: Renderhöhe in px
|
||||
|
||||
Konstruktion (viewBox 64×64, exportiert als `SIGNET`): kräftiges C (Kreisbogen r=19,
|
||||
Strichstärke 11) mit Öffnung rechts oben; oranger Haken startet links außerhalb, durchquert
|
||||
das C (dort per Maske freigestellt) und endet als Pfeil nach rechts oben. Wortmarke
|
||||
„Craft" Lotsenblau + „via" Signalorange, Gewicht 800.
|
||||
|
||||
Nach Änderungen an der Geometrie Icons neu erzeugen:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/generate-brand-icons.ts
|
||||
```
|
||||
|
||||
## Typografie
|
||||
|
||||
Inter (Brandbook §11.5), selbst gehostet über `next/font/local`
|
||||
(`src/app/fonts/inter-variable.ttf`, SIL Open Font License) — kein Laden aus dem Netz.
|
||||
TODO: auf woff2-Subsets umstellen (Dateigröße).
|
||||
|
||||
## Produktidentität
|
||||
|
||||
- Name: **Craftvia**, Tagline: **Handwerk. Digital auf Kurs.**
|
||||
- In-Product-Assistent: **Lotse** (eigene Figur, getrennt vom Logo; Modul `lotse`)
|
||||
- TOTP-Issuer: `Craftvia · Plattform` (`src/server/mfa.ts`), WebAuthn-RP-Name: `Craftvia`
|
||||
- Mail-Absender-Default: `MAIL_FROM_NAME` → `Craftvia`
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Certvia">
|
||||
<rect x="0" y="0" width="64" height="64" rx="14" fill="#5d52a3"/>
|
||||
<path d="M46 20 A17 17 0 1 0 46 44" fill="none" stroke="#ffffff" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M27 32 l6 6 l12 -15" fill="none" stroke="#f2a7ef" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 427 B |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="256" height="256" role="img" aria-label="Certvia">
|
||||
<path d="M46 20 A17 17 0 1 0 46 44" fill="none" stroke="#3b3b3a" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M27 32 l6 6 l12 -15" fill="none" stroke="#3b3b3a" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 361 B |
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="256" height="256" role="img" aria-label="Certvia">
|
||||
<path d="M46 20 A17 17 0 1 0 46 44" fill="none" stroke="#8a8a90" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M27 32 l6 6 l12 -15" fill="none" stroke="#8a8a90" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 361 B |
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="256" height="256" role="img" aria-label="Certvia">
|
||||
<path d="M46 20 A17 17 0 1 0 46 44" fill="none" stroke="#ffffff" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M27 32 l6 6 l12 -15" fill="none" stroke="#ffffff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 361 B |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="256" height="256" role="img" aria-label="Certvia">
|
||||
<path d="M46 20 A17 17 0 1 0 46 44" fill="none" stroke="#5d52a3" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M27 32 l6 6 l12 -15" fill="none" stroke="#812d80" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 361 B |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 576 B After Width: | Height: | Size: 915 B |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
||||
<defs>
|
||||
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="64" height="64">
|
||||
<rect x="0" y="0" width="64" height="64" fill="#fff"/>
|
||||
<path d="M6 28 L22 42 L44 21" fill="none" stroke="#000" stroke-width="13" stroke-linejoin="miter"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="64" height="64" rx="14" fill="#082E5B"/>
|
||||
<g transform="translate(9.6 9.6) scale(0.7)">
|
||||
<path d="M36.5 16.14 A19 19 0 1 0 48.71 37.3" fill="none" stroke="#FFFFFF" stroke-width="11" stroke-linecap="butt" mask="url(#cut)"/>
|
||||
<path d="M6 28 L22 42 L44 21" fill="none" stroke="#FF6A00" stroke-width="7" stroke-linejoin="miter"/>
|
||||
<path d="M52.7 12.7 L47.73 27.8 L37.37 16.96 Z" fill="#FF6A00"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 807 B |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
||||
<defs>
|
||||
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="64" height="64">
|
||||
<rect x="0" y="0" width="64" height="64" fill="#fff"/>
|
||||
<path d="M6 28 L22 42 L44 21" fill="none" stroke="#000" stroke-width="13" stroke-linejoin="miter"/>
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<g transform="translate(2 2) scale(0.9375)">
|
||||
<path d="M36.5 16.14 A19 19 0 1 0 48.71 37.3" fill="none" stroke="#082E5B" stroke-width="11" stroke-linecap="butt" mask="url(#cut)"/>
|
||||
<path d="M6 28 L22 42 L44 21" fill="none" stroke="#FF6A00" stroke-width="7" stroke-linejoin="miter"/>
|
||||
<path d="M52.7 12.7 L47.73 27.8 L37.37 16.96 Z" fill="#FF6A00"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 741 B |
|
Before Width: | Height: | Size: 572 B After Width: | Height: | Size: 464 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 893 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"name": "Certvia",
|
||||
"short_name": "Certvia",
|
||||
"description": "Informationssicherheit. Endlich einfach.",
|
||||
"theme_color": "#5d52a3",
|
||||
"background_color": "#0e1220",
|
||||
"name": "Craftvia",
|
||||
"short_name": "Craftvia",
|
||||
"description": "Handwerk. Digital auf Kurs.",
|
||||
"lang": "de",
|
||||
"start_url": "/dashboard",
|
||||
"scope": "/",
|
||||
"theme_color": "#082E5B",
|
||||
"background_color": "#F3F5F7",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{ "src": "/favicon/craftvia-app-icon.svg", "sizes": "any", "type": "image/svg+xml" },
|
||||
{ "src": "/favicon/pwa-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/favicon/pwa-512.png", "sizes": "512x512", "type": "image/png" },
|
||||
{ "src": "/favicon/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Erzeugt Favicon/PWA-Icons aus dem Craftvia-Signet (src/components/brand/craftvia-logo.tsx,
|
||||
* Geometrie `SIGNET`). Keine Netzwerkzugriffe; PNGs via `sharp` (liegt über Next.js bei).
|
||||
*
|
||||
* Lauf: npx tsx scripts/generate-brand-icons.ts
|
||||
* Ausgabe: public/favicon/*.svg|png, public/favicon.ico (PNG-in-ICO, 32×32)
|
||||
*/
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import sharp from "sharp";
|
||||
import { BRAND_COLORS } from "../src/lib/brand";
|
||||
import { SIGNET } from "../src/components/brand/craftvia-logo";
|
||||
|
||||
const OUT = join(process.cwd(), "public", "favicon");
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
|
||||
function signetSvg(opts: { tile: boolean; padding?: number; radius?: number }): string {
|
||||
const arc = opts.tile ? BRAND_COLORS.white : BRAND_COLORS.lotsenblau;
|
||||
const check = BRAND_COLORS.signalorange;
|
||||
const pad = opts.padding ?? 9.6;
|
||||
const scale = (64 - 2 * pad) / 64;
|
||||
const bg = opts.tile
|
||||
? `<rect x="0" y="0" width="64" height="64" rx="${opts.radius ?? 14}" fill="${BRAND_COLORS.lotsenblau}"/>`
|
||||
: "";
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
||||
<defs>
|
||||
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="64" height="64">
|
||||
<rect x="0" y="0" width="64" height="64" fill="#fff"/>
|
||||
<path d="${SIGNET.check}" fill="none" stroke="#000" stroke-width="${SIGNET.checkWidth + 6}" stroke-linejoin="miter"/>
|
||||
</mask>
|
||||
</defs>
|
||||
${bg}
|
||||
<g transform="translate(${pad} ${pad}) scale(${scale})">
|
||||
<path d="${SIGNET.arc}" fill="none" stroke="${arc}" stroke-width="${SIGNET.arcWidth}" stroke-linecap="butt" mask="url(#cut)"/>
|
||||
<path d="${SIGNET.check}" fill="none" stroke="${check}" stroke-width="${SIGNET.checkWidth}" stroke-linejoin="miter"/>
|
||||
<path d="${SIGNET.arrow}" fill="${check}"/>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
async function png(svg: string, size: number, file: string) {
|
||||
await sharp(Buffer.from(svg), { density: 72 * (size / 64) * 2 })
|
||||
.resize(size, size)
|
||||
.png()
|
||||
.toFile(join(OUT, file));
|
||||
}
|
||||
|
||||
/** Minimaler ICO-Container mit einem eingebetteten PNG (von allen Browsern unterstützt). */
|
||||
function icoFromPng(pngBuf: Buffer, size: number): Buffer {
|
||||
const header = Buffer.alloc(6);
|
||||
header.writeUInt16LE(0, 0);
|
||||
header.writeUInt16LE(1, 2);
|
||||
header.writeUInt16LE(1, 4);
|
||||
const entry = Buffer.alloc(16);
|
||||
entry.writeUInt8(size >= 256 ? 0 : size, 0);
|
||||
entry.writeUInt8(size >= 256 ? 0 : size, 1);
|
||||
entry.writeUInt8(0, 2);
|
||||
entry.writeUInt8(0, 3);
|
||||
entry.writeUInt16LE(1, 4);
|
||||
entry.writeUInt16LE(32, 6);
|
||||
entry.writeUInt32LE(pngBuf.length, 8);
|
||||
entry.writeUInt32LE(6 + 16, 12);
|
||||
return Buffer.concat([header, entry, pngBuf]);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Favicon: freistehendes Signet (transparent) als SVG — skaliert verlustfrei.
|
||||
const plain = signetSvg({ tile: false, padding: 2 });
|
||||
writeFileSync(join(OUT, "craftvia-signet.svg"), plain);
|
||||
// App-Icon-Kachel (Lotsenblau) für PWA/Apple/OG.
|
||||
const tile = signetSvg({ tile: true });
|
||||
writeFileSync(join(OUT, "craftvia-app-icon.svg"), tile);
|
||||
// Maskable: volle Fläche ohne Rundung, Signet in der Safe-Zone (80 %).
|
||||
const maskable = signetSvg({ tile: true, padding: 14, radius: 0 });
|
||||
|
||||
await png(plain, 16, "icon-16.png");
|
||||
await png(plain, 32, "icon-32.png");
|
||||
await png(plain, 48, "icon-48.png");
|
||||
await png(tile, 180, "apple-touch-icon-180.png");
|
||||
await png(tile, 192, "pwa-192.png");
|
||||
await png(tile, 512, "pwa-512.png");
|
||||
await png(maskable, 512, "maskable-512.png");
|
||||
|
||||
const ico32 = await sharp(Buffer.from(plain), { density: 144 }).resize(32, 32).png().toBuffer();
|
||||
writeFileSync(join(process.cwd(), "public", "favicon.ico"), icoFromPng(ico32, 32));
|
||||
console.log("✓ Craftvia-Icons erzeugt in public/favicon (+ public/favicon.ico)");
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { auth } from "@/server/auth";
|
||||
import { dbForTenant, prisma } from "@/server/db";
|
||||
import { resolvePasswordPolicy, describePasswordPolicy } from "@/lib/password-policy";
|
||||
import { ForcePasswordChangeForm } from "@/components/force-password-change-form";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
|
||||
/**
|
||||
* Erzwungener Passwortwechsel beim ersten Login (Force-Change). Liegt außerhalb der
|
||||
@@ -29,7 +29,7 @@ export default async function ChangePasswordPage() {
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={34} />
|
||||
<CraftviaLogo variant="horizontal" height={34} />
|
||||
<p className="mt-5 font-heading text-lg font-semibold">Passwort ändern</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Ihr Konto wurde mit einem Initialpasswort angelegt. Bitte vergeben Sie jetzt ein eigenes Passwort, um fortzufahren.
|
||||
|
||||
@@ -7,7 +7,7 @@ import { newTotpSecret, totpUri } from "@/server/mfa";
|
||||
import { encryptSecret, decryptSecret } from "@/server/secret-crypto";
|
||||
import { TenantMfaEnrollForm } from "@/components/tenant-mfa-enroll-form";
|
||||
import { PasskeyManager } from "@/components/passkey-manager";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
|
||||
/**
|
||||
* SEC3-a: Pflicht-MFA-Einrichtung für Mandanten-Nutzer, wenn der Mandant MFA verlangt
|
||||
@@ -37,7 +37,7 @@ export default async function EnrollMfaPage() {
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-md rounded-2xl border bg-card p-8">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={34} />
|
||||
<CraftviaLogo variant="horizontal" height={34} />
|
||||
<p className="mt-5 font-heading text-lg font-semibold">Zwei-Faktor-Authentifizierung einrichten</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Ihre Organisation verlangt eine Zwei-Faktor-Authentifizierung. Scannen Sie den QR-Code mit
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
|
||||
/**
|
||||
* Gebrandete Fehlerseite (Story S7). Zeigt bewusst keine technischen Details —
|
||||
@@ -19,7 +18,7 @@ export default function Error({
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8 text-center">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={34} className="mx-auto" />
|
||||
<CraftviaLogo variant="horizontal" height={34} className="mx-auto" />
|
||||
<p className="mt-6 font-heading text-lg font-semibold">Es ist ein Fehler aufgetreten</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Die Aktion konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut oder wenden
|
||||
@@ -31,7 +30,6 @@ export default function Error({
|
||||
<Button onClick={reset} className="mt-6 w-full justify-center">
|
||||
Erneut versuchen
|
||||
</Button>
|
||||
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
import { ForgotPasswordForm } from "@/components/auth-recovery-forms";
|
||||
import { loginPathOf } from "@/server/auth-selfservice";
|
||||
|
||||
@@ -25,7 +24,7 @@ export default async function ForgotPasswordPage({
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={36} />
|
||||
<CraftviaLogo variant="horizontal" height={36} />
|
||||
<p className="mt-5 font-heading text-lg font-semibold">Passwort vergessen</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{domain === "platform"
|
||||
@@ -40,8 +39,6 @@ export default async function ForgotPasswordPage({
|
||||
Zurück zur Anmeldung
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<PoweredByGefim className="mt-6 border-t border-[var(--panel-brd)] pt-4" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-open-sans);
|
||||
--font-sans: var(--font-inter);
|
||||
--font-mono: ui-monospace, monospace;
|
||||
--font-heading: var(--font-poppins);
|
||||
--font-heading: var(--font-inter);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
@@ -39,7 +39,7 @@
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
/* Semantische Zusatzflächen (dunkles Certvia-Theme) */
|
||||
/* Semantische Zusatzflächen */
|
||||
--color-panel: var(--panel);
|
||||
--color-panel-brd: var(--panel-brd);
|
||||
--color-elevated: var(--elevated);
|
||||
@@ -47,6 +47,8 @@
|
||||
--color-band: var(--band);
|
||||
--color-band-text: var(--band-text);
|
||||
--color-band-brd: var(--band-brd);
|
||||
--color-cta: var(--ui-accent);
|
||||
--color-cta-foreground: var(--ui-accent-foreground);
|
||||
--color-ok: var(--ok);
|
||||
--color-warn: var(--warn);
|
||||
--color-risk: var(--risk);
|
||||
@@ -61,133 +63,109 @@
|
||||
}
|
||||
|
||||
/*
|
||||
* Certvia — zentrale Token-Definition (Single Source of Truth für Farben).
|
||||
* Craftvia — zentrale Token-Definition (Single Source of Truth für Farben).
|
||||
* Quelle: docs/craftvia/BRANDBOOK.md §11.4. JS-Pendant: src/lib/brand.ts.
|
||||
*
|
||||
* Zwei bewusst getrennte Ebenen:
|
||||
* --brand-* Markenfarben aus dem Certvia-Styleguide (Logo, Print, Verläufe,
|
||||
* Dokument-/Export-CD). NICHT für UI-Flächen verwenden.
|
||||
* --ui-* Produktpalette des dunklen App-Themes (Interaktion, Flächen).
|
||||
* Sie ist die festgelegte Certvia-UI-Palette und wird nicht an die
|
||||
* Markenfarben „angeglichen" — Marken-Violett #5d52a3 gilt fürs Logo,
|
||||
* UI-Interaktiv bleibt #7d6fd6.
|
||||
* Zwei Ebenen:
|
||||
* --brand-* Kernfarben der Marke (Logo, Print, Dokumente).
|
||||
* --ui-* Rollen im hellen App-Theme (Interaktion, Flächen), abgeleitet aus --brand-*.
|
||||
*
|
||||
* Die semantischen shadcn-Tokens (--primary, --background, …) leiten sich unten
|
||||
* ausschließlich aus --ui-* / --bg-* ab; Hex-Werte stehen nur noch hier.
|
||||
* Ein späterer Light-Mode kann über dieselben Token-Namen unter .light ergänzt werden.
|
||||
* JS-seitiges Pendant (Canvas-/PNG-Export, Metadaten): src/lib/brand.ts.
|
||||
* ausschließlich aus --brand-* / --ui-* ab; Hex-Werte stehen nur in diesem Block.
|
||||
* Farbe nie als einziges Statussignal — immer mit Text/Icon kombinieren.
|
||||
*/
|
||||
:root {
|
||||
/* ---- Marke (Logo / Print / Export) ---- */
|
||||
--brand-violet: #5d52a3;
|
||||
--brand-magenta: #812d80;
|
||||
--brand-blue: #8dc4e0;
|
||||
--brand-anthracite: #3b3b3a;
|
||||
/* Marken-Varianten auf dunklem Grund (Styleguide) */
|
||||
--brand-violet-on-dark: #7d6fd6;
|
||||
--brand-magenta-on-dark: #d17bcf;
|
||||
/* ---- Marke ---- */
|
||||
--brand-lotsenblau: #082e5b;
|
||||
--brand-signalorange: #ff6a00;
|
||||
--brand-graphit: #34383d;
|
||||
--brand-hafengrau: #f3f5f7;
|
||||
--brand-stahlgrau: #66727d;
|
||||
--brand-zink: #d9e0e5;
|
||||
--brand-white: #ffffff;
|
||||
|
||||
/* ---- UI-Palette (dunkles Produkt-Theme) ---- */
|
||||
--ui-primary: #7d6fd6;
|
||||
--ui-primary-deep: #5d52a3;
|
||||
--ui-blue: #8dc4e0;
|
||||
--ui-magenta: #b45bb0;
|
||||
--ui-primary-soft: rgba(125, 111, 214, 0.18);
|
||||
--ui-primary-ring: rgba(125, 111, 214, 0.35);
|
||||
/* ---- Funktionsfarben ---- */
|
||||
--ok: #23845d;
|
||||
--warn: #c87912;
|
||||
--risk: #c64242;
|
||||
--info: #2f6fa3;
|
||||
|
||||
/* ---- UI-Rollen (helles Produkt-Theme) ---- */
|
||||
--ui-primary: var(--brand-lotsenblau);
|
||||
--ui-primary-hover: #0b3c75;
|
||||
--ui-primary-soft: rgba(8, 46, 91, 0.08);
|
||||
--ui-primary-ring: rgba(8, 46, 91, 0.3);
|
||||
/* Akzent/CTA: Signalorange — sparsam für die wichtigste Aktion je Ansicht. */
|
||||
--ui-accent: var(--brand-signalorange);
|
||||
--ui-accent-foreground: #ffffff;
|
||||
|
||||
/* Grundflächen */
|
||||
--bg-0: #0e1220;
|
||||
--bg-1: #141a2e;
|
||||
--bg-2: #1a2138;
|
||||
--bg-3: #1e2640;
|
||||
--txt: #e8ecf7;
|
||||
--txt-muted: #8b93ad;
|
||||
--line: rgba(120, 135, 180, 0.18);
|
||||
--bg-0: var(--brand-hafengrau);
|
||||
--bg-1: var(--brand-white);
|
||||
--bg-2: #eef1f4;
|
||||
--txt: var(--brand-graphit);
|
||||
--txt-muted: var(--brand-stahlgrau);
|
||||
--line: var(--brand-zink);
|
||||
|
||||
/* Verläufe */
|
||||
--grad: var(--grad-primary);
|
||||
--grad-primary: linear-gradient(
|
||||
135deg,
|
||||
var(--ui-primary-deep) 0%,
|
||||
var(--ui-primary) 100%
|
||||
);
|
||||
--grad-soft: var(--grad-primary);
|
||||
--grad-critical: linear-gradient(90deg, var(--risk) 0%, var(--ui-magenta) 100%);
|
||||
/* Keine Verläufe im Craftvia-CD: die Utility-Namen bleiben, zeigen aber Vollfarbe. */
|
||||
--grad-primary: var(--ui-primary);
|
||||
|
||||
/* Status */
|
||||
--ok: #39c07f;
|
||||
--warn: #f0ad4e;
|
||||
--risk: #ff6b6b;
|
||||
--info: #5aa9e6;
|
||||
|
||||
/* Flächen (semantisch → leiten aus den Tokens oben ab) */
|
||||
/* Flächen (semantisch) */
|
||||
--background: var(--bg-0);
|
||||
--foreground: var(--txt);
|
||||
--card: var(--bg-1);
|
||||
--card-foreground: var(--txt);
|
||||
--popover: var(--bg-2);
|
||||
--popover: var(--bg-1);
|
||||
--popover-foreground: var(--txt);
|
||||
--primary: var(--ui-primary);
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: var(--bg-2);
|
||||
--secondary-foreground: #cfc9ee;
|
||||
--secondary-foreground: var(--brand-lotsenblau);
|
||||
--muted: var(--bg-2);
|
||||
--muted-foreground: var(--txt-muted);
|
||||
--accent: var(--bg-3);
|
||||
--accent-foreground: #cfc9ee;
|
||||
--accent: var(--ui-primary-soft);
|
||||
--accent-foreground: var(--brand-lotsenblau);
|
||||
--destructive: var(--risk);
|
||||
--border: var(--line);
|
||||
--input: rgba(120, 135, 180, 0.22);
|
||||
--input: var(--line);
|
||||
--ring: var(--ui-primary);
|
||||
--chart-1: var(--ui-primary);
|
||||
--chart-2: var(--ui-blue);
|
||||
--chart-3: var(--ui-magenta);
|
||||
--chart-1: var(--brand-lotsenblau);
|
||||
--chart-2: var(--brand-signalorange);
|
||||
--chart-3: var(--info);
|
||||
--chart-4: var(--ok);
|
||||
--chart-5: var(--warn);
|
||||
--radius: 0.7rem;
|
||||
--radius: 0.6rem;
|
||||
|
||||
/* Panels / Glas / erhöhte Flächen */
|
||||
--panel: rgba(30, 38, 64, 0.72);
|
||||
/* Panels / erhöhte Flächen */
|
||||
--panel: rgba(255, 255, 255, 0.92);
|
||||
--panel-brd: var(--line);
|
||||
--elevated: var(--bg-2);
|
||||
/* weiche Akzentkarte (früher #faf9fd) */
|
||||
--surface-soft: rgba(125, 111, 214, 0.08);
|
||||
/* Info-Band (früher #f2f0f9 / #e6e2f3 / #5a4e86) */
|
||||
--band: rgba(125, 111, 214, 0.1);
|
||||
--band-text: #c3bdec;
|
||||
--band-brd: rgba(125, 111, 214, 0.22);
|
||||
/* Chip auf farbiger Fläche (Risiko-Heatmap) — dunkler Grund, halbtransparent */
|
||||
--chip-overlay: rgba(14, 18, 32, 0.75);
|
||||
|
||||
/* Graph (React Flow) — Linien/Labels, die als SVG-Attribute gesetzt werden */
|
||||
--graph-edge: #4a5372;
|
||||
--graph-edge-critical: var(--risk);
|
||||
--surface-soft: var(--ui-primary-soft);
|
||||
--band: rgba(47, 111, 163, 0.08);
|
||||
--band-text: var(--info);
|
||||
--band-brd: rgba(47, 111, 163, 0.25);
|
||||
|
||||
/* Sidebar / Topbar */
|
||||
--sidebar: var(--bg-1);
|
||||
--sidebar-foreground: #b6bdd4;
|
||||
--sidebar-foreground: var(--brand-graphit);
|
||||
--sidebar-primary: var(--ui-primary);
|
||||
--sidebar-primary-foreground: #ffffff;
|
||||
--sidebar-accent: var(--ui-primary-soft);
|
||||
--sidebar-accent-foreground: #cfc9ee;
|
||||
--sidebar-accent-foreground: var(--brand-lotsenblau);
|
||||
--sidebar-border: var(--line);
|
||||
--sidebar-ring: var(--ui-primary);
|
||||
|
||||
--focus-ring: 0 0 0 3px var(--ui-primary-ring);
|
||||
--radius-card: 16px;
|
||||
--radius-ctrl: 10px;
|
||||
--dot-grid: radial-gradient(rgba(140, 155, 200, 0.1) 1px, transparent 1px);
|
||||
--glow:
|
||||
radial-gradient(1200px 700px at 20% -10%, #1c2340 0%, transparent 60%),
|
||||
radial-gradient(900px 600px at 100% 110%, #1a2138 0%, transparent 55%);
|
||||
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 28px rgba(0, 0, 0, 0.28);
|
||||
--radius-card: 12px;
|
||||
--radius-ctrl: 8px;
|
||||
/* Flaches CD: nur eine feine Kontur statt Schlagschatten. */
|
||||
--shadow-card: 0 1px 2px rgba(8, 46, 91, 0.06);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans), "Open Sans", system-ui, sans-serif;
|
||||
font-family: var(--font-sans), Inter, system-ui, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
background-color: var(--background);
|
||||
background-image: var(--glow);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -204,149 +182,29 @@ body {
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
font-family: var(--font-heading), "Poppins", sans-serif;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-heading), Inter, system-ui, sans-serif;
|
||||
font-weight: 700;
|
||||
color: var(--brand-lotsenblau);
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.bg-grad {
|
||||
background: var(--grad-primary);
|
||||
}
|
||||
.bg-grad,
|
||||
.bg-grad-soft {
|
||||
background: var(--grad-primary);
|
||||
}
|
||||
.bg-grad-critical {
|
||||
background: var(--grad-critical);
|
||||
}
|
||||
.shadow-card {
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
.dot-grid {
|
||||
background-image: var(--dot-grid);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
}
|
||||
|
||||
/* React Flow — Dark-Anpassungen + Glow für kritische Kanten (Teil B) */
|
||||
.react-flow__edge.critical-edge .react-flow__edge-path {
|
||||
filter: drop-shadow(0 0 4px rgba(255, 107, 107, 0.7));
|
||||
}
|
||||
.react-flow__controls-button {
|
||||
background: var(--elevated) !important;
|
||||
border-color: var(--panel-brd) !important;
|
||||
color: var(--muted-foreground) !important;
|
||||
}
|
||||
.react-flow__controls-button:hover {
|
||||
background: var(--accent) !important;
|
||||
}
|
||||
.react-flow__controls-button svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
.react-flow__edge-text {
|
||||
fill: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* Gerenderte Richtlinien/Verfahren (Lesemodus) — Typografie ohne Plugin */
|
||||
.policy-prose {
|
||||
font-size: 13.5px;
|
||||
line-height: 1.7;
|
||||
color: var(--foreground);
|
||||
}
|
||||
.policy-prose h2 {
|
||||
font-family: var(--font-heading, inherit);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin: 1.6em 0 0.5em;
|
||||
padding-top: 0.9em;
|
||||
border-top: 1px solid var(--border);
|
||||
scroll-margin-top: 1rem;
|
||||
}
|
||||
.policy-prose h2:first-child {
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
.policy-prose h3 {
|
||||
font-family: var(--font-heading, inherit);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin: 1.3em 0 0.4em;
|
||||
}
|
||||
.policy-prose h4 {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin: 1em 0 0.3em;
|
||||
}
|
||||
.policy-prose p {
|
||||
margin: 0.55em 0;
|
||||
}
|
||||
.policy-prose ul,
|
||||
.policy-prose ol {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 1.3em;
|
||||
list-style: revert;
|
||||
}
|
||||
.policy-prose li {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
.policy-prose strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
.policy-prose a {
|
||||
color: var(--info);
|
||||
text-decoration: none;
|
||||
}
|
||||
.policy-prose a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.policy-prose blockquote {
|
||||
border-left: 3px solid var(--band-brd);
|
||||
background: var(--band);
|
||||
color: var(--band-text);
|
||||
padding: 0.5em 0.9em;
|
||||
margin: 0.8em 0;
|
||||
border-radius: 0 8px 8px 0;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.policy-prose table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0.9em 0;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.policy-prose th,
|
||||
.policy-prose td {
|
||||
border: 1px solid var(--border);
|
||||
padding: 6px 10px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
.policy-prose th {
|
||||
background: var(--elevated);
|
||||
font-weight: 700;
|
||||
}
|
||||
.policy-prose code {
|
||||
background: var(--elevated);
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
* Dokument-/Druck-Corporate-Design (Story S5)
|
||||
*
|
||||
* Ausgaben außerhalb der App laufen im hellen Certvia-CD: Marken-Violett/
|
||||
* Magenta auf Weiß, Poppins für Überschriften, Open Sans im Fließtext.
|
||||
* Kopfzeile = Certvia-Lockup (das SVG trägt Poppins eingebettet, siehe
|
||||
* public/assets/logo/certvia-lockup-positiv.svg), Fußzeile = Dachmarken-
|
||||
* Hinweis „Certvia — ein Produkt von GEFIM" auf jeder Seite.
|
||||
*
|
||||
* Farb-/Textreferenz für den späteren DOCX/PDF-Export: src/lib/document-brand.ts.
|
||||
* Dokument-/Druck-Corporate-Design (Einsatzberichte, Druckansicht)
|
||||
* Farb-/Textreferenz für den PDF-Export: src/lib/document-brand.ts.
|
||||
* ========================================================================= */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 24mm 16mm 22mm;
|
||||
margin: 20mm 16mm 20mm;
|
||||
}
|
||||
|
||||
@media print {
|
||||
@@ -361,82 +219,33 @@ body {
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: var(--brand-anthracite);
|
||||
--card: #ffffff;
|
||||
--card-foreground: var(--brand-anthracite);
|
||||
--muted-foreground: #6b6b70;
|
||||
--primary: var(--brand-violet);
|
||||
--border: #d9d7e4;
|
||||
--panel: #ffffff;
|
||||
--panel-brd: #d9d7e4;
|
||||
--elevated: #f4f2fa;
|
||||
--band: #f4f2fa;
|
||||
--band-text: var(--brand-violet);
|
||||
--band-brd: #d9d7e4;
|
||||
--info: var(--brand-violet);
|
||||
}
|
||||
|
||||
body {
|
||||
background: #ffffff !important;
|
||||
background-image: none !important;
|
||||
color: var(--brand-anthracite);
|
||||
color: var(--brand-graphit);
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Kopf: Certvia-Lockup, einmal am Dokumentanfang. */
|
||||
body::before {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 13mm;
|
||||
margin-bottom: 6mm;
|
||||
padding-bottom: 4mm;
|
||||
border-bottom: 1px solid #d9d7e4;
|
||||
background: url("/assets/logo/certvia-lockup-positiv.svg") left center / auto 11mm no-repeat;
|
||||
}
|
||||
|
||||
/* Fuß: Dachmarken-Hinweis, auf jeder Seite (fixed wiederholt beim Druck). */
|
||||
/* Fuß: Produkthinweis auf jeder Seite. */
|
||||
body::after {
|
||||
content: "Certvia — ein Produkt von GEFIM";
|
||||
content: "Erstellt mit Craftvia";
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-top: 2mm;
|
||||
border-top: 1px solid #d9d7e4;
|
||||
font-family: var(--font-sans), "Open Sans", sans-serif;
|
||||
border-top: 1px solid var(--brand-zink);
|
||||
font-size: 9pt;
|
||||
color: #6b6b70;
|
||||
color: var(--brand-stahlgrau);
|
||||
}
|
||||
|
||||
.shadow-card,
|
||||
.bg-grad,
|
||||
.bg-grad-soft {
|
||||
.shadow-card {
|
||||
box-shadow: none !important;
|
||||
background: #ffffff !important;
|
||||
color: var(--brand-anthracite) !important;
|
||||
}
|
||||
|
||||
.policy-prose {
|
||||
color: var(--brand-anthracite);
|
||||
font-size: 10.5pt;
|
||||
}
|
||||
.policy-prose h2,
|
||||
.policy-prose h3 {
|
||||
color: var(--brand-violet);
|
||||
break-after: avoid;
|
||||
}
|
||||
.policy-prose table,
|
||||
.policy-prose blockquote {
|
||||
break-inside: avoid;
|
||||
}
|
||||
.policy-prose th {
|
||||
background: #f4f2fa;
|
||||
}
|
||||
.policy-prose a {
|
||||
color: var(--brand-violet);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { prisma } from "@/server/db";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
import { ResetPasswordForm } from "@/components/auth-recovery-forms";
|
||||
import { checkInvitationToken } from "@/server/actions/auth-recovery";
|
||||
import { describePasswordPolicy, resolvePasswordPolicy } from "@/lib/password-policy";
|
||||
@@ -37,7 +36,7 @@ export default async function InvitePage({
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={36} />
|
||||
<CraftviaLogo variant="horizontal" height={36} />
|
||||
<p className="mt-5 font-heading text-lg font-semibold">Konto einrichten</p>
|
||||
|
||||
{valid ? (
|
||||
@@ -57,8 +56,6 @@ export default async function InvitePage({
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -5,30 +5,13 @@ import { getLocale } from "next-intl/server";
|
||||
import { BRAND, BRAND_ASSETS, BRAND_COLORS } from "@/lib/brand";
|
||||
import "./globals.css";
|
||||
|
||||
// Selbst gehostet über next/font/local (committete woff2 in ./fonts) — kein
|
||||
// Google-Fonts-Fetch zur Build- oder Laufzeit. DSGVO-konform und macht den
|
||||
// Produktiv-Build reproduzierbar/offline (next/font/google lud die Schriften
|
||||
// sonst zur Build-Zeit von Google, was flakig fehlschlagen kann).
|
||||
const openSans = localFont({
|
||||
variable: "--font-open-sans",
|
||||
// Inter (Brandbook §11.5), selbst gehostet über next/font/local — kein Google-Fonts-Fetch
|
||||
// zur Build- oder Laufzeit (DSGVO, reproduzierbarer Offline-Build). Variable Font
|
||||
// (Gewichte 100–900 in einer Datei). TODO(brand): auf woff2-Subsets umstellen (kleiner).
|
||||
const inter = localFont({
|
||||
variable: "--font-inter",
|
||||
display: "swap",
|
||||
src: [
|
||||
{ path: "./fonts/open-sans-300.woff2", weight: "300", style: "normal" },
|
||||
{ path: "./fonts/open-sans-400.woff2", weight: "400", style: "normal" },
|
||||
{ path: "./fonts/open-sans-600.woff2", weight: "600", style: "normal" },
|
||||
{ path: "./fonts/open-sans-700.woff2", weight: "700", style: "normal" },
|
||||
],
|
||||
});
|
||||
|
||||
const poppins = localFont({
|
||||
variable: "--font-poppins",
|
||||
display: "swap",
|
||||
src: [
|
||||
{ path: "./fonts/poppins-300.woff2", weight: "300", style: "normal" },
|
||||
{ path: "./fonts/poppins-500.woff2", weight: "500", style: "normal" },
|
||||
{ path: "./fonts/poppins-600.woff2", weight: "600", style: "normal" },
|
||||
{ path: "./fonts/poppins-700.woff2", weight: "700", style: "normal" },
|
||||
],
|
||||
src: [{ path: "./fonts/inter-variable.ttf", weight: "100 900", style: "normal" }],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -37,35 +20,27 @@ export const metadata: Metadata = {
|
||||
description: BRAND.description,
|
||||
applicationName: BRAND.name,
|
||||
manifest: "/site.webmanifest",
|
||||
// Alle Icons aus dem Certvia-Asset-Bundle (public/favicon + public/assets/logo).
|
||||
// Icons aus dem Craftvia-Signet erzeugt (scripts/generate-brand-icons.ts).
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicon.ico", sizes: "any" },
|
||||
{ url: BRAND_ASSETS.favicon, type: "image/svg+xml" },
|
||||
{ url: "/favicon/icon-16.png", type: "image/png", sizes: "16x16" },
|
||||
{ url: "/favicon/icon-32.png", type: "image/png", sizes: "32x32" },
|
||||
],
|
||||
apple: [{ url: "/favicon/apple-touch-icon-180.png", sizes: "180x180" }],
|
||||
apple: [{ url: BRAND_ASSETS.appleTouchIcon, sizes: "180x180" }],
|
||||
},
|
||||
appleWebApp: { capable: true, title: BRAND.name, statusBarStyle: "black-translucent" },
|
||||
appleWebApp: { capable: true, title: BRAND.name, statusBarStyle: "default" },
|
||||
openGraph: {
|
||||
type: "website",
|
||||
siteName: BRAND.name,
|
||||
title: `${BRAND.name} — ${BRAND.tagline}`,
|
||||
description: BRAND.description,
|
||||
images: [{ url: "/assets/logo/certvia-lockup-positiv-1024.png", width: 1024, height: 259 }],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: `${BRAND.name} — ${BRAND.tagline}`,
|
||||
description: BRAND.description,
|
||||
images: ["/assets/logo/certvia-lockup-positiv-1024.png"],
|
||||
images: [{ url: BRAND_ASSETS.ogImage, width: 512, height: 512 }],
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
// Marken-Violett (wie auf der Certvia-Website), nicht die UI-Primärfarbe.
|
||||
themeColor: BRAND_COLORS.violet,
|
||||
themeColor: BRAND_COLORS.lotsenblau,
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
@@ -76,10 +51,7 @@ export default async function RootLayout({
|
||||
const locale = await getLocale();
|
||||
|
||||
return (
|
||||
<html
|
||||
lang={locale}
|
||||
className={`${openSans.variable} ${poppins.variable} h-full antialiased`}
|
||||
>
|
||||
<html lang={locale} className={`${inter.variable} h-full antialiased`}>
|
||||
<body className="min-h-full flex flex-col bg-background text-foreground">
|
||||
<NextIntlClientProvider>{children}</NextIntlClientProvider>
|
||||
</body>
|
||||
|
||||
@@ -8,8 +8,7 @@ import { verifyMfaPending, signLoginTicket, MFA_PENDING_COOKIE } from "@/server/
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
|
||||
/**
|
||||
* WS5 (Option C) — Two-Step-Login, Schritt 2: MFA. Erreichbar NUR mit gültigem,
|
||||
@@ -57,7 +56,7 @@ export default async function LoginMfaPage({
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={40} />
|
||||
<CraftviaLogo variant="horizontal" height={40} />
|
||||
<p className="mt-5 font-heading text-lg font-semibold">Bestätigung (2. Schritt)</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App oder einen Recovery-Code ein.
|
||||
@@ -81,8 +80,6 @@ export default async function LoginMfaPage({
|
||||
<p className="mt-4 text-center text-[12.5px]">
|
||||
<Link href="/login" className="text-muted-foreground hover:text-foreground">Zurück zur Anmeldung</Link>
|
||||
</p>
|
||||
|
||||
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -9,8 +9,7 @@ import { signMfaPending, signLoginTicket, MFA_PENDING_COOKIE } from "@/server/lo
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
import { PasskeyLoginButton } from "@/components/passkey-login-button";
|
||||
|
||||
export default async function LoginPage({
|
||||
@@ -78,7 +77,7 @@ export default async function LoginPage({
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={42} />
|
||||
<CraftviaLogo variant="horizontal" height={42} />
|
||||
<p className="mt-3 font-heading text-[13px] font-medium text-[var(--band-text)]">
|
||||
{t("tagline")}
|
||||
</p>
|
||||
@@ -122,8 +121,6 @@ export default async function LoginPage({
|
||||
{t("forgotPassword")}
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
|
||||
/** Gebrandete 404-Seite (Story S7 — keine ungebrandeten Systemscreens). */
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8 text-center">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={34} className="mx-auto" />
|
||||
<CraftviaLogo variant="horizontal" height={34} className="mx-auto" />
|
||||
<p className="mt-6 font-heading text-4xl font-bold text-[var(--primary)]">404</p>
|
||||
<p className="mt-2 font-heading text-lg font-semibold">Seite nicht gefunden</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
@@ -21,7 +20,6 @@ export default function NotFound() {
|
||||
>
|
||||
Zum Dashboard
|
||||
</Button>
|
||||
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { prisma } from "@/server/db";
|
||||
import { newTotpSecret, totpUri } from "@/server/mfa";
|
||||
import { encryptSecret, decryptSecret } from "@/server/secret-crypto";
|
||||
import { PlatformEnrollForm } from "@/components/platform-enroll-form";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
|
||||
/**
|
||||
* MFA-Einrichtung für Plattform-Administratoren (Pflicht beim ersten Login).
|
||||
@@ -30,7 +30,7 @@ export default async function EnrollMfaPage() {
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-md rounded-2xl border bg-card p-8">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={34} />
|
||||
<CraftviaLogo variant="horizontal" height={34} />
|
||||
<p className="mt-5 font-heading text-lg font-semibold">Zwei-Faktor-Authentifizierung einrichten</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Für Plattform-Administratoren verpflichtend. Scannen Sie den QR-Code mit einer
|
||||
|
||||
@@ -5,8 +5,7 @@ import { platformAuth, platformSignIn } from "@/server/platform-auth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
|
||||
/**
|
||||
* Getrennter Login für Plattform-Administratoren (Phase-1-Härtung Paket 2).
|
||||
@@ -41,7 +40,7 @@ export default async function PlatformLoginPage({
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={38} />
|
||||
<CraftviaLogo variant="horizontal" height={38} />
|
||||
<p className="mt-4 font-heading text-lg font-semibold">Plattform-Administration</p>
|
||||
<h2 className="mt-1 text-sm font-normal text-muted-foreground">
|
||||
Betreiberzugang — getrennt vom Mandanten-Login
|
||||
@@ -84,8 +83,6 @@ export default async function PlatformLoginPage({
|
||||
Passwort vergessen?
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { prisma } from "@/server/db";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
import { ResetPasswordForm } from "@/components/auth-recovery-forms";
|
||||
import { checkResetToken } from "@/server/actions/auth-recovery";
|
||||
import { loginPathOf } from "@/server/auth-selfservice";
|
||||
@@ -43,7 +42,7 @@ export default async function ResetPage({
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={36} />
|
||||
<CraftviaLogo variant="horizontal" height={36} />
|
||||
<p className="mt-5 font-heading text-lg font-semibold">Neues Passwort vergeben</p>
|
||||
|
||||
{valid ? (
|
||||
@@ -74,8 +73,6 @@ export default async function ResetPage({
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,7 @@ import { redirect } from "next/navigation";
|
||||
import { auth, signOut } from "@/server/auth";
|
||||
import { setActiveTenant } from "@/server/actions/tenant-switch";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
|
||||
/**
|
||||
* WS2 (Option C) — Mandantenauswahl nach dem Login, wenn eine Person mehreren
|
||||
@@ -22,7 +21,7 @@ export default async function SelectTenantPage() {
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={36} />
|
||||
<CraftviaLogo variant="horizontal" height={36} />
|
||||
<p className="mt-5 font-heading text-lg font-semibold">Mandant wählen</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Ihr Konto gehört mehreren Mandanten an. Bitte wählen Sie, in welchem Sie arbeiten möchten.
|
||||
@@ -48,8 +47,6 @@ export default async function SelectTenantPage() {
|
||||
<form action={async () => { "use server"; await signOut({ redirectTo: "/login" }); }} className="mt-5">
|
||||
<Button type="submit" variant="ghost" className="w-full justify-center text-[13px]">Abmelden</Button>
|
||||
</form>
|
||||
|
||||
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
import { confirmEmailChange } from "@/server/actions/auth-recovery";
|
||||
import { loginPathOf } from "@/server/auth-selfservice";
|
||||
|
||||
@@ -32,7 +31,7 @@ export default async function VerifyEmailPage({
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={36} />
|
||||
<CraftviaLogo variant="horizontal" height={36} />
|
||||
<p className="mt-5 font-heading text-lg font-semibold">E-Mail-Adresse bestätigen</p>
|
||||
|
||||
<p
|
||||
@@ -52,8 +51,6 @@ export default async function VerifyEmailPage({
|
||||
>
|
||||
Zur Anmeldung →
|
||||
</Link>
|
||||
|
||||
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { BRAND, BRAND_COLORS } from "@/lib/brand";
|
||||
|
||||
/**
|
||||
* Certvia-Logo als Inline-SVG.
|
||||
*
|
||||
* Bewusst inline statt <Image src="…svg">: die Wortmarke setzt „Cert" in Poppins
|
||||
* Light und „via" in Poppins Bold. Ein extern geladenes SVG hat keinen Zugriff auf
|
||||
* die per next/font/local eingebundene Schrift (gehashter Family-Name) und würde
|
||||
* auf eine System-Schrift zurückfallen. Inline greift `var(--font-poppins)`.
|
||||
*
|
||||
* Zeichen-Konstruktion (unverändert aus dem Design-Paket, viewBox 64×64):
|
||||
* „C" nach rechts offen, der Haken liegt bündig in der Öffnung. Kein Verlauf.
|
||||
*
|
||||
* theme="light" → heller Grund (C Violett, Haken/„via" Magenta, „Cert" Anthrazit)
|
||||
* theme="dark" → dunkler Grund (C #7d6fd6, Haken #d17bcf, „Cert" weiß)
|
||||
*/
|
||||
|
||||
type LogoTheme = "light" | "dark";
|
||||
|
||||
const PALETTE: Record<LogoTheme, { arc: string; hook: string; word: string; accent: string }> = {
|
||||
light: {
|
||||
arc: BRAND_COLORS.violet,
|
||||
hook: BRAND_COLORS.magenta,
|
||||
word: BRAND_COLORS.anthracite,
|
||||
accent: BRAND_COLORS.magenta,
|
||||
},
|
||||
dark: {
|
||||
arc: BRAND_COLORS.violetOnDark,
|
||||
hook: BRAND_COLORS.magentaOnDark,
|
||||
word: BRAND_COLORS.white,
|
||||
accent: BRAND_COLORS.magentaOnDark,
|
||||
},
|
||||
};
|
||||
|
||||
/** Das Monogramm allein — Pfade aus `certvia-mark-*.svg`. */
|
||||
function Monogram({ arc, hook }: { arc: string; hook: string }) {
|
||||
return (
|
||||
<>
|
||||
<path
|
||||
d="M46 20 A17 17 0 1 0 46 44"
|
||||
fill="none"
|
||||
stroke={arc}
|
||||
strokeWidth={6}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M27 32 l6 6 l12 -15"
|
||||
fill="none"
|
||||
stroke={hook}
|
||||
strokeWidth={5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function CertviaLogo({
|
||||
variant = "lockup",
|
||||
theme = "dark",
|
||||
height = 40,
|
||||
className,
|
||||
title = BRAND.name,
|
||||
}: {
|
||||
variant?: "lockup" | "mark";
|
||||
theme?: LogoTheme;
|
||||
/** Renderhöhe in px; die Breite ergibt sich aus dem Seitenverhältnis. */
|
||||
height?: number;
|
||||
className?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
const c = PALETTE[theme];
|
||||
|
||||
if (variant === "mark") {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 64 64"
|
||||
width={height}
|
||||
height={height}
|
||||
role="img"
|
||||
aria-label={title}
|
||||
className={className}
|
||||
>
|
||||
<title>{title}</title>
|
||||
<Monogram arc={c.arc} hook={c.hook} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 380 96"
|
||||
width={(height * 380) / 96}
|
||||
height={height}
|
||||
role="img"
|
||||
aria-label={title}
|
||||
className={className}
|
||||
>
|
||||
<title>{title}</title>
|
||||
<g transform="translate(6,8) scale(1.25)">
|
||||
<Monogram arc={c.arc} hook={c.hook} />
|
||||
</g>
|
||||
<text
|
||||
x={112}
|
||||
y={62}
|
||||
fontSize={52}
|
||||
letterSpacing={-1}
|
||||
style={{ fontFamily: "var(--font-poppins), Poppins, 'Segoe UI', sans-serif" }}
|
||||
>
|
||||
<tspan fontWeight={300} fill={c.word}>
|
||||
Cert
|
||||
</tspan>
|
||||
<tspan fontWeight={700} fill={c.accent}>
|
||||
via
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { useId } from "react";
|
||||
import { BRAND, BRAND_COLORS } from "@/lib/brand";
|
||||
|
||||
/**
|
||||
* Craftvia-Logo als Inline-SVG (Referenz: docs/craftvia/assets/craftvia-brand-identity-v1.png).
|
||||
*
|
||||
* Signet: kräftiges „C" in Lotsenblau, rechts oben geöffnet. Ein Haken in Signalorange
|
||||
* startet links außerhalb, durchquert das C (das C ist dort freigestellt) und läuft als
|
||||
* Pfeil nach rechts oben durch die Öffnung hinaus („auf Kurs").
|
||||
* Wortmarke: „Craft" Lotsenblau + „via" Signalorange, fett; Tagline optional.
|
||||
* Keine Schatten, keine Verläufe.
|
||||
*
|
||||
* Inline statt <img>: die Wortmarke nutzt die per next/font eingebundene App-Schrift.
|
||||
*
|
||||
* Varianten:
|
||||
* variant="horizontal" (Default) | "signet"
|
||||
* tone="color" (Default) | "mono" (einfarbig Lotsenblau) | "reversed" (weiß auf dunklem Grund)
|
||||
* tile (nur signet): App-Icon — Lotsenblau-Kachel, weißes C, oranger Haken
|
||||
*/
|
||||
|
||||
export type CraftviaLogoTone = "color" | "mono" | "reversed";
|
||||
|
||||
/** Geometrie des Signets (viewBox 0 0 64 64) — auch von scripts/generate-brand-icons.ts genutzt. */
|
||||
export const SIGNET = {
|
||||
/** C: Kreisbogen um (30,34), r=19, von -70° gegen den Uhrzeigersinn bis +10° (Öffnung rechts oben). */
|
||||
arc: "M36.5 16.14 A19 19 0 1 0 48.71 37.3",
|
||||
arcWidth: 11,
|
||||
/** Haken: Schwanz links außerhalb → Tal → Richtung Öffnung. */
|
||||
check: "M6 28 L22 42 L44 21",
|
||||
checkWidth: 7,
|
||||
/** Pfeilspitze nach rechts oben. */
|
||||
arrow: "M52.7 12.7 L47.73 27.8 L37.37 16.96 Z",
|
||||
} as const;
|
||||
|
||||
function palette(tone: CraftviaLogoTone, tile: boolean) {
|
||||
if (tile) return { arc: BRAND_COLORS.white, check: BRAND_COLORS.signalorange, craft: BRAND_COLORS.white, via: BRAND_COLORS.signalorange, tagline: BRAND_COLORS.white };
|
||||
switch (tone) {
|
||||
case "mono":
|
||||
return { arc: BRAND_COLORS.lotsenblau, check: BRAND_COLORS.lotsenblau, craft: BRAND_COLORS.lotsenblau, via: BRAND_COLORS.lotsenblau, tagline: BRAND_COLORS.lotsenblau };
|
||||
case "reversed":
|
||||
return { arc: BRAND_COLORS.white, check: BRAND_COLORS.white, craft: BRAND_COLORS.white, via: BRAND_COLORS.white, tagline: BRAND_COLORS.white };
|
||||
default:
|
||||
return { arc: BRAND_COLORS.lotsenblau, check: BRAND_COLORS.signalorange, craft: BRAND_COLORS.lotsenblau, via: BRAND_COLORS.signalorange, tagline: BRAND_COLORS.lotsenblau };
|
||||
}
|
||||
}
|
||||
|
||||
function Signet({ maskId, arc, check }: { maskId: string; arc: string; check: string }) {
|
||||
return (
|
||||
<>
|
||||
<defs>
|
||||
{/* Freistellung: das C ist dort unterbrochen, wo der Haken es durchquert. */}
|
||||
<mask id={maskId} maskUnits="userSpaceOnUse" x="0" y="0" width="64" height="64">
|
||||
<rect x="0" y="0" width="64" height="64" fill="#fff" />
|
||||
<path d={SIGNET.check} fill="none" stroke="#000" strokeWidth={SIGNET.checkWidth + 6} strokeLinejoin="miter" />
|
||||
</mask>
|
||||
</defs>
|
||||
<path d={SIGNET.arc} fill="none" stroke={arc} strokeWidth={SIGNET.arcWidth} strokeLinecap="butt" mask={`url(#${maskId})`} />
|
||||
<path d={SIGNET.check} fill="none" stroke={check} strokeWidth={SIGNET.checkWidth} strokeLinejoin="miter" strokeLinecap="butt" />
|
||||
<path d={SIGNET.arrow} fill={check} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function CraftviaLogo({
|
||||
variant = "horizontal",
|
||||
tone = "color",
|
||||
tile = false,
|
||||
tagline = false,
|
||||
height = 36,
|
||||
className,
|
||||
title = BRAND.name,
|
||||
}: {
|
||||
variant?: "horizontal" | "signet";
|
||||
tone?: CraftviaLogoTone;
|
||||
/** Nur für variant="signet": App-Icon-Kachel in Lotsenblau. */
|
||||
tile?: boolean;
|
||||
/** Tagline „Handwerk. Digital auf Kurs." unter der Wortmarke (nur horizontal). */
|
||||
tagline?: boolean;
|
||||
/** Renderhöhe in px; die Breite ergibt sich aus dem Seitenverhältnis. */
|
||||
height?: number;
|
||||
className?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
const maskId = `cv-mask-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
||||
const isTile = variant === "signet" && tile;
|
||||
const c = palette(tone, isTile);
|
||||
|
||||
if (variant === "signet") {
|
||||
return (
|
||||
<svg viewBox="0 0 64 64" width={height} height={height} role="img" aria-label={title} className={className}>
|
||||
<title>{title}</title>
|
||||
{isTile ? (
|
||||
<>
|
||||
<rect x="0" y="0" width="64" height="64" rx="14" fill={BRAND_COLORS.lotsenblau} />
|
||||
<g transform="translate(9.6 9.6) scale(0.7)">
|
||||
<Signet maskId={maskId} arc={c.arc} check={c.check} />
|
||||
</g>
|
||||
</>
|
||||
) : (
|
||||
<Signet maskId={maskId} arc={c.arc} check={c.check} />
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const vbW = 292;
|
||||
const vbH = tagline ? 82 : 64;
|
||||
return (
|
||||
<svg viewBox={`0 0 ${vbW} ${vbH}`} width={(height * vbW) / vbH} height={height} role="img" aria-label={title} className={className}>
|
||||
<title>{title}</title>
|
||||
<Signet maskId={maskId} arc={c.arc} check={c.check} />
|
||||
<text
|
||||
x={70}
|
||||
y={48}
|
||||
fontSize={46}
|
||||
fontWeight={800}
|
||||
letterSpacing={-1.2}
|
||||
style={{ fontFamily: "var(--font-sans), Inter, system-ui, 'Segoe UI', Arial, sans-serif" }}
|
||||
>
|
||||
<tspan fill={c.craft}>Craft</tspan>
|
||||
<tspan fill={c.via}>via</tspan>
|
||||
</text>
|
||||
{tagline && (
|
||||
<text
|
||||
x={72}
|
||||
y={74}
|
||||
fontSize={15}
|
||||
fontWeight={600}
|
||||
fill={c.tagline}
|
||||
style={{ fontFamily: "var(--font-sans), Inter, system-ui, 'Segoe UI', Arial, sans-serif" }}
|
||||
>
|
||||
{BRAND.tagline}
|
||||
</text>
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import { BRAND, BRAND_ASSETS } from "@/lib/brand";
|
||||
|
||||
/**
|
||||
* Dachmarken-Hinweis „Ein Produkt von GEFIM".
|
||||
*
|
||||
* Bewusste Leitplanke der Branding-Umstellung: Certvia ist die Produktmarke,
|
||||
* GEFIM bleibt Dachmarke und erscheint weiterhin in Fußzeilen, auf den
|
||||
* Anmeldeseiten sowie später in Export- und Mail-Fußzeilen.
|
||||
* Siehe docs/BRANDING-CERTVIA.md.
|
||||
*/
|
||||
export function PoweredByGefim({
|
||||
withLogo = true,
|
||||
className,
|
||||
}: {
|
||||
withLogo?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<p
|
||||
className={`flex items-center justify-center gap-2 text-[11px] text-muted-foreground ${className ?? ""}`}
|
||||
>
|
||||
<span>{BRAND.byline}</span>
|
||||
{withLogo && (
|
||||
<Image
|
||||
src={BRAND_ASSETS.parentLogo}
|
||||
alt={BRAND.parentBrand}
|
||||
// Seitenverhältnis der Quelldatei (1000×353)
|
||||
width={76}
|
||||
height={27}
|
||||
// Negativdarstellung auf dunklem Grund
|
||||
className="opacity-80 brightness-0 invert"
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
import type { TenantBranding } from "@/lib/brand";
|
||||
|
||||
/**
|
||||
* Kopfbereichs-Marke eines Mandanten (Story S8).
|
||||
* Kopfbereichs-Marke eines Mandanten.
|
||||
*
|
||||
* Default ist **Certvia**; ein mandanteneigenes Logo überschreibt nur, wenn es
|
||||
* gesetzt ist. Der Upload je Mandant kommt mit Admin Phase 2 (Objektspeicher) —
|
||||
* bis dahin liefert `resolveTenantBranding` konstant `logoUrl: null`, die
|
||||
* Verzweigung ist aber bereits vorhanden und muss dann nicht nachgezogen werden.
|
||||
* Default ist **Craftvia**; ein mandanteneigenes Logo überschreibt nur, wenn es
|
||||
* gesetzt ist (TenantSettings.logoKey → Upload folgt). Bis dahin liefert
|
||||
* `resolveTenantBranding` konstant `logoUrl: null`.
|
||||
*/
|
||||
export function TenantBrand({
|
||||
branding,
|
||||
@@ -20,8 +19,7 @@ export function TenantBrand({
|
||||
}) {
|
||||
if (branding.logoUrl) {
|
||||
return (
|
||||
// Mandanten-Logos liegen später im Objektspeicher (beliebige Domain) —
|
||||
// daher bewusst <img> statt next/image ohne Remote-Pattern-Konfiguration.
|
||||
// Mandanten-Logos liegen im Objektspeicher — daher bewusst <img> statt next/image.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={branding.logoUrl}
|
||||
@@ -31,5 +29,5 @@ export function TenantBrand({
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <CertviaLogo variant="lockup" theme="dark" height={height} className={className} />;
|
||||
return <CraftviaLogo variant="horizontal" height={height} className={className} />;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Basisbausteine im Look des UI-Mockups (docs/ISMS-Prototyp-GEFIM.html —
|
||||
* historisches Projektartefakt, der Dateiname bleibt bewusst unverändert):
|
||||
* PageHead (Crumb/Titel/Sub + Aktionen), KPI-Kapsel, C/I/A-Quadrate,
|
||||
* Tag-Chip, Owner-Chip, Status-Pille.
|
||||
* UI-Basisbausteine (Craftvia-CD): PageHead (Crumb/Titel/Sub + Aktionen), KPI-Kapsel,
|
||||
* Tag-Chip, Owner-Chip, Status-Pille. Farbe nie als einziges Statussignal — Pillen
|
||||
* tragen immer Text.
|
||||
*/
|
||||
|
||||
export function PageHead({
|
||||
@@ -56,80 +55,9 @@ export function KpiCard({
|
||||
);
|
||||
}
|
||||
|
||||
// Segmentbalken-Farben je Stufe 1–4 als Ampel: Grün → Gelb → Orange → Rot
|
||||
const CIA_BAR_COLORS: Record<number, string> = {
|
||||
1: "bg-[#2e9e6b]",
|
||||
2: "bg-[#e5b000]",
|
||||
3: "bg-[#e07d2e]",
|
||||
4: "bg-[#d64c4c]",
|
||||
};
|
||||
|
||||
/** Ein Schutzziel als 4er-Segmentbalken, gefüllt bis `level`. */
|
||||
function CiaSegment({ level, label }: { level: number; label?: string }) {
|
||||
const color = CIA_BAR_COLORS[level] ?? CIA_BAR_COLORS[1];
|
||||
return (
|
||||
<span className="inline-flex flex-col items-center gap-1">
|
||||
<span className="flex gap-[2px]">
|
||||
{[1, 2, 3, 4].map((seg) => (
|
||||
<span
|
||||
key={seg}
|
||||
className={cn(
|
||||
"h-4 w-[7px] rounded-[2px]",
|
||||
seg <= level ? color : "bg-[rgba(120,135,180,0.2)]"
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
{label && (
|
||||
<small className="text-[9.5px] font-bold tracking-[.04em] text-muted-foreground">
|
||||
{label}
|
||||
</small>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schutzbedarf / Schadenshöhe als drei Segmentbalken (C/I/A). Mit `labels`
|
||||
* werden VER/INT/VFB unter den Balken gezeigt (Detailkarten); ohne für
|
||||
* kompakte Tabellenzellen.
|
||||
*/
|
||||
export function CiaBadge({
|
||||
c,
|
||||
i,
|
||||
a,
|
||||
labels = false,
|
||||
}: {
|
||||
c: number;
|
||||
i: number;
|
||||
a: number;
|
||||
labels?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-2.5" title={`C${c} · I${i} · A${a}`}>
|
||||
<CiaSegment level={c} label={labels ? "VER" : undefined} />
|
||||
<CiaSegment level={i} label={labels ? "INT" : undefined} />
|
||||
<CiaSegment level={a} label={labels ? "VFB" : undefined} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** VER/INT/VFB-Beschriftung passend zur Balkenbreite — einmal pro Tabelle als Überschrift. */
|
||||
export function CiaLegend() {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-2.5 text-[9.5px] font-bold tracking-[.04em] text-muted-foreground">
|
||||
{["VER", "INT", "VFB"].map((l) => (
|
||||
<span key={l} className="w-[34px] text-center">
|
||||
{l}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function Tag({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<span className="inline-block rounded-md bg-[rgba(139,147,173,0.14)] px-2 py-0.5 text-[11px] font-bold text-[#b7bdd0]">
|
||||
<span className="inline-block rounded-md bg-[var(--muted)] px-2 py-0.5 text-[11px] font-bold text-muted-foreground">
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
@@ -153,15 +81,15 @@ export function OwnerChip({ name, noOwnerLabel }: { name?: string | null; noOwne
|
||||
);
|
||||
}
|
||||
|
||||
// Status-Chips: ~16–20 % Alpha der Statusfarbe auf Dunkel (Referenz-Vorgabe)
|
||||
// Status-Chips: ~12 % Alpha der Funktionsfarbe auf hellem Grund (Brandbook §11.4).
|
||||
const PILL_TONES = {
|
||||
ok: "bg-[rgba(57,192,127,0.16)] text-[var(--ok)]",
|
||||
warn: "bg-[rgba(240,173,78,0.16)] text-[var(--warn)]",
|
||||
orange: "bg-[rgba(240,140,60,0.16)] text-[#f0a35a]",
|
||||
risk: "bg-[rgba(255,107,107,0.16)] text-[var(--risk)]",
|
||||
info: "bg-[rgba(90,169,230,0.16)] text-[var(--info)]",
|
||||
mut: "bg-[rgba(139,147,173,0.16)] text-muted-foreground",
|
||||
violet: "bg-[rgba(125,111,214,0.2)] text-[#c3bdec]",
|
||||
ok: "bg-[rgba(35,132,93,0.12)] text-[var(--ok)]",
|
||||
warn: "bg-[rgba(200,121,18,0.12)] text-[var(--warn)]",
|
||||
orange: "bg-[rgba(255,106,0,0.12)] text-[#b34a00]",
|
||||
risk: "bg-[rgba(198,66,66,0.12)] text-[var(--risk)]",
|
||||
info: "bg-[rgba(47,111,163,0.12)] text-[var(--info)]",
|
||||
mut: "bg-[rgba(102,114,125,0.12)] text-muted-foreground",
|
||||
violet: "bg-[rgba(8,46,91,0.1)] text-[var(--primary)]",
|
||||
} as const;
|
||||
|
||||
export function Pill({
|
||||
@@ -183,12 +111,6 @@ export function Pill({
|
||||
);
|
||||
}
|
||||
|
||||
/** Kritikalität 1–4 als Pille in Mockup-Farben (info/warn/risk). */
|
||||
export function CriticalityPill({ level, label }: { level: number; label: string }) {
|
||||
const tone = level >= 4 ? "risk" : level === 3 ? "warn" : level === 2 ? "info" : "ok";
|
||||
return <Pill tone={tone}>{label}</Pill>;
|
||||
}
|
||||
|
||||
export function SectTitle({ title, sub }: { title: string; sub?: string }) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,106 +1,88 @@
|
||||
/**
|
||||
* Certvia — Marken- und Produktkonstanten (JS-Pendant zu den CSS-Tokens in
|
||||
* `src/app/globals.css`).
|
||||
* Craftvia — Marken- und Produktkonstanten (JS-Pendant zu den CSS-Tokens in
|
||||
* `src/app/globals.css`). Quelle: docs/craftvia/BRANDBOOK.md §11.4/§11.5.
|
||||
*
|
||||
* Hier steht nur, was CSS-Variablen NICHT auflösen können:
|
||||
* - Metadaten (`<title>`, OG/Twitter, Manifest, TOTP-Issuer)
|
||||
* - Canvas-/PNG-Export (html-to-image rendert außerhalb des CSS-Kontexts)
|
||||
* - Dokument-/Export-Corporate-Design (DOCX/PDF, Print-Kopf/-Fußzeile)
|
||||
* - Metadaten (`<title>`, OG, Manifest, TOTP-Issuer, WebAuthn-RP-Name)
|
||||
* - Dokument-/Export-Corporate-Design (PDF-Berichte, Print)
|
||||
* - E-Mail-Templates (Inline-Styles, kein CSS-Var-Support in Mail-Clients)
|
||||
* - Inline-SVG-Logo (src/components/brand/craftvia-logo.tsx)
|
||||
*
|
||||
* Für alles, was im Browser gerendert wird, gelten die CSS-Tokens als Quelle —
|
||||
* dieses Modul spiegelt sie nur, damit beide Seiten nicht auseinanderlaufen.
|
||||
*
|
||||
* Leitplanke: GEFIM bleibt Dachmarke. Produktmarke = Certvia.
|
||||
*/
|
||||
|
||||
/** Markenfarben aus dem Certvia-Styleguide (Logo, Print, Verläufe). */
|
||||
/** Kernfarben aus dem Craftvia-Brandbook (§11.4). */
|
||||
export const BRAND_COLORS = {
|
||||
violet: "#5d52a3",
|
||||
magenta: "#812d80",
|
||||
blue: "#8dc4e0",
|
||||
anthracite: "#3b3b3a",
|
||||
/** Varianten auf dunklem Grund */
|
||||
violetOnDark: "#7d6fd6",
|
||||
magentaOnDark: "#d17bcf",
|
||||
white: "#ffffff",
|
||||
/** Primär — Logo, Navigation, Headlines */
|
||||
lotsenblau: "#082E5B",
|
||||
/** Sekundär — „via", Haken-/Weg-Signet, CTA, aktive Elemente */
|
||||
signalorange: "#FF6A00",
|
||||
/** Dunkel — Fließtext, Footer */
|
||||
graphit: "#34383D",
|
||||
/** Hell — Flächen, Seitenhintergrund */
|
||||
hafengrau: "#F3F5F7",
|
||||
/** Neutral — Sekundärtext, Icons */
|
||||
stahlgrau: "#66727D",
|
||||
/** Linie — Trennlinien, Eingabefelder */
|
||||
zink: "#D9E0E5",
|
||||
white: "#FFFFFF",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* UI-Palette des dunklen Produkt-Themes. Bewusst getrennt von BRAND_COLORS:
|
||||
* Marken-Violett #5d52a3 gilt fürs Logo, UI-Interaktiv bleibt #7d6fd6.
|
||||
*/
|
||||
export const UI_COLORS = {
|
||||
primary: "#7d6fd6",
|
||||
primaryDeep: "#5d52a3",
|
||||
blue: "#8dc4e0",
|
||||
magenta: "#b45bb0",
|
||||
bg0: "#0e1220",
|
||||
bg1: "#141a2e",
|
||||
bg2: "#1a2138",
|
||||
text: "#e8ecf7",
|
||||
textMuted: "#8b93ad",
|
||||
line: "rgba(120, 135, 180, 0.18)",
|
||||
graphEdge: "#4a5372",
|
||||
ok: "#39c07f",
|
||||
warn: "#f0ad4e",
|
||||
risk: "#ff6b6b",
|
||||
info: "#5aa9e6",
|
||||
/** Funktionsfarben (§11.4). Farbe nie als einziges Statussignal einsetzen. */
|
||||
export const STATUS_COLORS = {
|
||||
success: "#23845D",
|
||||
warning: "#C87912",
|
||||
error: "#C64242",
|
||||
info: "#2F6FA3",
|
||||
} as const;
|
||||
|
||||
/** Produktidentität — überall dort, wo der Name als Text erscheint. */
|
||||
export const BRAND = {
|
||||
/** Produktmarke */
|
||||
name: "Certvia",
|
||||
tagline: "Informationssicherheit. Endlich einfach.",
|
||||
name: "Craftvia",
|
||||
tagline: "Handwerk. Digital auf Kurs.",
|
||||
description:
|
||||
"Certvia — Informationssicherheits-Managementsystem für ISO/IEC 27001:2022 und TISAX/VDA-ISA.",
|
||||
/** Dachmarke (bleibt erhalten, siehe docs/BRANDING-CERTVIA.md) */
|
||||
parentBrand: "GEFIM",
|
||||
/** Fußzeilen-/Signaturhinweis für UI, Export und E-Mail */
|
||||
byline: "Ein Produkt von GEFIM",
|
||||
"Craftvia — die Einsatz-App für Handwerks- und Montagebetriebe: Aufträge, Teams, Einsätze und Berichte in einem System.",
|
||||
/** Name des In-Product-Assistenten (separat vom Logo, §Lotse) */
|
||||
assistantName: "Lotse",
|
||||
/** Kompakte Form für Dokument-Fußzeilen */
|
||||
documentByline: "Certvia — ein Produkt von GEFIM",
|
||||
documentByline: "Erstellt mit Craftvia",
|
||||
} as const;
|
||||
|
||||
/** Asset-Pfade unter `public/` (Logo-Bundle aus dem Design-Paket). */
|
||||
/** Asset-Pfade unter `public/` (aus dem Inline-SVG-Logo erzeugt). */
|
||||
export const BRAND_ASSETS = {
|
||||
markPositive: "/assets/logo/certvia-mark-positiv.svg",
|
||||
markNegative: "/assets/logo/certvia-mark-negativ.svg",
|
||||
markGrey: "/assets/logo/certvia-mark-grau.svg",
|
||||
markAnthracite: "/assets/logo/certvia-mark-anthrazit.svg",
|
||||
lockupPositive: "/assets/logo/certvia-lockup-positiv.svg",
|
||||
favicon: "/assets/logo/certvia-favicon.svg",
|
||||
/** Dachmarke */
|
||||
parentLogo: "/assets/logo/gefim-logo.png",
|
||||
} as const;
|
||||
|
||||
/** Schriftstapel für Kontexte ohne Tailwind/CSS-Vars (Export, E-Mail, Print). */
|
||||
export const BRAND_FONTS = {
|
||||
heading: "'Poppins', 'Segoe UI', system-ui, sans-serif",
|
||||
body: "'Open Sans', system-ui, -apple-system, sans-serif",
|
||||
favicon: "/favicon/craftvia-signet.svg",
|
||||
appleTouchIcon: "/favicon/apple-touch-icon-180.png",
|
||||
ogImage: "/favicon/pwa-512.png",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Standard-Branding eines Mandanten (Story S8). Ohne eigenes Logo/Akzent zeigt
|
||||
* jeder Mandant Certvia; ein späterer Logo-Upload (Admin Phase 2) überschreibt
|
||||
* ausschließlich die gesetzten Felder.
|
||||
* Schriftstapel für Kontexte ohne Tailwind/CSS-Vars (Export, E-Mail, Print).
|
||||
* Primärschrift laut Brandbook: Inter. TODO(brand): Inter selbst hosten (woff2 in
|
||||
* src/app/fonts) — bis dahin nutzt die App die vorhandenen selbst gehosteten Schriften.
|
||||
*/
|
||||
export const BRAND_FONTS = {
|
||||
heading: "Inter, system-ui, 'Segoe UI', Roboto, Arial, sans-serif",
|
||||
body: "Inter, system-ui, 'Segoe UI', Roboto, Arial, sans-serif",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Standard-Branding eines Mandanten. Ohne eigenes Logo/Akzent zeigt jeder Mandant
|
||||
* Craftvia; ein späterer Logo-Upload überschreibt ausschließlich die gesetzten Felder.
|
||||
*/
|
||||
export type TenantBranding = {
|
||||
/** Produktname im Kopfbereich */
|
||||
productName: string;
|
||||
/** Custom-Logo des Mandanten; `null` ⇒ Certvia-Lockup */
|
||||
/** Custom-Logo des Mandanten; `null` ⇒ Craftvia-Logo */
|
||||
logoUrl: string | null;
|
||||
/** Akzentfarbe; `null` ⇒ UI-Primärfarbe */
|
||||
accent: string | null;
|
||||
/** Dachmarken-Hinweis, unabhängig vom Mandanten-Branding */
|
||||
byline: string;
|
||||
};
|
||||
|
||||
const HEX_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
||||
|
||||
/**
|
||||
* Löst das effektive Branding eines Mandanten auf. Certvia ist der Default;
|
||||
* Löst das effektive Branding eines Mandanten auf. Craftvia ist der Default;
|
||||
* mandanteneigene Werte überschreiben nur, wenn sie gesetzt und plausibel sind.
|
||||
*/
|
||||
export function resolveTenantBranding(
|
||||
@@ -112,6 +94,5 @@ export function resolveTenantBranding(
|
||||
productName: BRAND.name,
|
||||
logoUrl: logoUrl ? logoUrl : null,
|
||||
accent: accent && HEX_RE.test(accent) ? accent : null,
|
||||
byline: BRAND.byline,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
import { BRAND, BRAND_COLORS, BRAND_FONTS } from "@/lib/brand";
|
||||
|
||||
/**
|
||||
* Branding-Layer für Dokument-Ausgaben (Story S5).
|
||||
* Branding-Layer für Dokument-Ausgaben (Einsatzberichte, Druckansicht, PDF-Export).
|
||||
*
|
||||
* Ausgaben außerhalb der App-UI — Druckansicht, künftiger DOCX-/PDF-Export von
|
||||
* Richtlinien, VDA-ISA-Katalog und Reports — laufen im **hellen** Corporate
|
||||
* Design: Marken-Violett/Magenta auf Weiß, Poppins für Überschriften, Open Sans
|
||||
* für Fließtext, Fußzeile mit dem Dachmarken-Hinweis.
|
||||
*
|
||||
* Bewusst ohne Abhängigkeit zu einer Export-Bibliothek: das Export-Feature
|
||||
* (DOCX/PDF) existiert noch nicht. Dieses Modul liefert die Kopf-/Fußzeilen-
|
||||
* Bausteine und die Farb-/Schriftreferenz, an die es dann andockt — damit das
|
||||
* CD nicht ein zweites Mal definiert werden muss.
|
||||
* Ausgaben außerhalb der App-UI laufen im hellen Craftvia-CD: Lotsenblau für
|
||||
* Überschriften, Signalorange als sparsamer Akzent, Graphit für Fließtext.
|
||||
* Bewusst ohne Abhängigkeit zu einer Export-Bibliothek — das Berichtsmodul dockt
|
||||
* an diese Farb-/Schriftreferenz an.
|
||||
*/
|
||||
|
||||
/** Farb-/Schriftreferenz für Dokumentausgaben (heller Grund). */
|
||||
export const DOCUMENT_THEME = {
|
||||
pageBackground: "#ffffff",
|
||||
text: BRAND_COLORS.anthracite,
|
||||
textMuted: "#6b6b70",
|
||||
accent: BRAND_COLORS.violet,
|
||||
accentStrong: BRAND_COLORS.magenta,
|
||||
rule: "#d9d7e4",
|
||||
tableHeaderBackground: "#f4f2fa",
|
||||
pageBackground: BRAND_COLORS.white,
|
||||
text: BRAND_COLORS.graphit,
|
||||
textMuted: BRAND_COLORS.stahlgrau,
|
||||
accent: BRAND_COLORS.lotsenblau,
|
||||
accentStrong: BRAND_COLORS.signalorange,
|
||||
rule: BRAND_COLORS.zink,
|
||||
tableHeaderBackground: BRAND_COLORS.hafengrau,
|
||||
headingFont: BRAND_FONTS.heading,
|
||||
bodyFont: BRAND_FONTS.body,
|
||||
} as const;
|
||||
@@ -30,7 +25,7 @@ export const DOCUMENT_THEME = {
|
||||
export type DocumentHeader = {
|
||||
/** Produktmarke — steht im Kopf jeder Ausgabe */
|
||||
brand: string;
|
||||
/** Dokumenttitel, z. B. „Richtlinie Zugriffskontrolle" */
|
||||
/** Dokumenttitel, z. B. „Einsatzbericht A-2026-0012" */
|
||||
title: string;
|
||||
/** Organisation des Mandanten */
|
||||
organisation?: string;
|
||||
@@ -41,10 +36,8 @@ export type DocumentHeader = {
|
||||
};
|
||||
|
||||
export type DocumentFooter = {
|
||||
/** „Certvia — ein Produkt von GEFIM" */
|
||||
/** „Erstellt mit Craftvia" */
|
||||
byline: string;
|
||||
/** Logo-Pfad der Dachmarke für Ausgaben, die Bilder einbetten können */
|
||||
parentLogo: string;
|
||||
/** Optionaler Vertraulichkeitsvermerk */
|
||||
confidentiality?: string;
|
||||
};
|
||||
@@ -54,18 +47,14 @@ export function documentHeader(input: Omit<DocumentHeader, "brand">): DocumentHe
|
||||
return { brand: BRAND.name, ...input };
|
||||
}
|
||||
|
||||
/** Fußzeilen-Daten — der Dachmarken-Hinweis ist nicht abschaltbar. */
|
||||
/** Fußzeilen-Daten einer Dokumentausgabe. */
|
||||
export function documentFooter(confidentiality?: string): DocumentFooter {
|
||||
return {
|
||||
byline: BRAND.documentByline,
|
||||
parentLogo: "/assets/logo/gefim-logo.png",
|
||||
confidentiality,
|
||||
};
|
||||
return { byline: BRAND.documentByline, confidentiality };
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzeiler für Ausgabeformate, die nur eine Textfußzeile kennen
|
||||
* (CSV-Kopfkommentar, Plaintext-Mail, DOCX-Footer ohne Bild).
|
||||
* (CSV-Kopfkommentar, Plaintext, PDF-Footer ohne Bild).
|
||||
*/
|
||||
export function documentFooterLine(confidentiality?: string): string {
|
||||
const f = documentFooter(confidentiality);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DOCUMENT_THEME } from "@/lib/document-brand";
|
||||
* Brandfähige E-Mail-Template-Basis (Story S6).
|
||||
*
|
||||
* SMTP und der Einladungs-/Reset-Flow kommen erst mit Paket 4 — dieses Modul
|
||||
* liefert vorab das Certvia-Gerüst, damit die Templates dann nur noch Inhalt
|
||||
* liefert das Craftvia-Gerüst, damit die Templates nur noch Inhalt
|
||||
* einsetzen müssen und das CD nicht neu erfunden wird.
|
||||
*
|
||||
* Bewusste Einschränkungen für E-Mail-Clients:
|
||||
@@ -39,7 +39,7 @@ export type EmailContent = {
|
||||
* E-Mail-Clients nicht.
|
||||
*/
|
||||
function baseUrl(): string {
|
||||
return (process.env.AUTH_URL ?? process.env.NEXTAUTH_URL ?? "https://app.certvia.de").replace(
|
||||
return (process.env.AUTH_URL ?? process.env.NEXTAUTH_URL ?? "http://localhost:3000").replace(
|
||||
/\/+$/,
|
||||
""
|
||||
);
|
||||
@@ -58,13 +58,13 @@ export function emailSender(address: string): string {
|
||||
return `${BRAND.name} <${address}>`;
|
||||
}
|
||||
|
||||
/** Signatur-/Fußzeilentext — enthält immer den Dachmarken-Hinweis. */
|
||||
/** Signatur-/Fußzeilentext. */
|
||||
export function emailSignature(): string {
|
||||
return `${BRAND.name} — ${BRAND.byline}`;
|
||||
return `${BRAND.name} — ${BRAND.tagline}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert eine Mail im Certvia-CD (heller Grund, Marken-Violett, Poppins-Kopf).
|
||||
* Rendert eine Mail im Craftvia-CD (Hafengrau-Grund, Lotsenblau, Signalorange-CTA).
|
||||
* Rückgabe ist ein vollständiges HTML-Dokument.
|
||||
*/
|
||||
export function renderHtmlEmail(content: EmailContent): string {
|
||||
@@ -80,7 +80,7 @@ export function renderHtmlEmail(content: EmailContent): string {
|
||||
|
||||
const action = content.action
|
||||
? `<table role="presentation" cellpadding="0" cellspacing="0" style="margin:24px 0;">
|
||||
<tr><td style="background:${BRAND_COLORS.violet};border-radius:8px;">
|
||||
<tr><td style="background:${BRAND_COLORS.signalorange};border-radius:8px;">
|
||||
<a href="${escapeHtml(content.action.url)}" style="display:inline-block;padding:12px 22px;font-family:${t.headingFont};font-size:15px;font-weight:600;color:#ffffff;text-decoration:none;">${escapeHtml(content.action.label)}</a>
|
||||
</td></tr>
|
||||
</table>`
|
||||
@@ -93,12 +93,13 @@ export function renderHtmlEmail(content: EmailContent): string {
|
||||
return `<!doctype html>
|
||||
<html lang="de">
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(content.subject)}</title></head>
|
||||
<body style="margin:0;padding:0;background:#f4f2fa;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f4f2fa;padding:28px 12px;">
|
||||
<body style="margin:0;padding:0;background:${BRAND_COLORS.hafengrau};">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:${BRAND_COLORS.hafengrau};padding:28px 12px;">
|
||||
<tr><td align="center">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:560px;background:${t.pageBackground};border-radius:14px;overflow:hidden;border:1px solid ${t.rule};">
|
||||
<tr><td style="padding:26px 30px 0;">
|
||||
<img src="${url}/assets/logo/certvia-lockup-positiv.svg" alt="${BRAND.name}" height="34" style="height:34px;width:auto;display:block;border:0;">
|
||||
<img src="${url}/favicon/pwa-192.png" alt="${BRAND.name}" height="34" width="34" style="height:34px;width:34px;display:inline-block;vertical-align:middle;border:0;border-radius:8px;">
|
||||
<span style="display:inline-block;vertical-align:middle;margin-left:10px;font-family:${t.headingFont};font-size:22px;font-weight:800;color:${BRAND_COLORS.lotsenblau};">Craft<span style="color:${BRAND_COLORS.signalorange};">via</span></span>
|
||||
</td></tr>
|
||||
<tr><td style="padding:22px 30px 4px;">
|
||||
<h1 style="margin:0 0 14px;font-family:${t.headingFont};font-size:20px;font-weight:600;color:${t.text};">${escapeHtml(content.heading)}</h1>
|
||||
|
||||
@@ -90,7 +90,7 @@ export function resetMailConfigCache(): void {
|
||||
cached = null;
|
||||
}
|
||||
|
||||
/** Absenderzeile `Certvia <no-reply@certvia.de>`. */
|
||||
/** Absenderzeile `Craftvia <no-reply@craftvia.de>`. */
|
||||
export function mailFrom(config: MailConfig): string {
|
||||
return `${config.fromName} <${config.from}>`;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
AuthenticationResponseJSON,
|
||||
AuthenticatorTransportFuture,
|
||||
} from "@simplewebauthn/types";
|
||||
import { BRAND } from "@/lib/brand";
|
||||
|
||||
/**
|
||||
* SEC3-b: WebAuthn/Passkey-Basis. Kapselt die RP-/Origin-Konfiguration (aus AUTH_URL bzw.
|
||||
@@ -17,7 +18,7 @@ import type {
|
||||
* (Cookie) und Persistenz.
|
||||
*/
|
||||
|
||||
export const RP_NAME = "Certvia";
|
||||
export const RP_NAME = BRAND.name;
|
||||
/** Cookie-Name der kurzlebigen Login-Challenge (pre-session Passkey-Login). */
|
||||
export const LOGIN_CHALLENGE_COOKIE = "wa_login_challenge";
|
||||
|
||||
|
||||