feat(web): Modul-Identitaet mit App-Kacheln und deutschen Kategorienamen
Jedes Modul bekommt Farbe und Symbol (tender-radar Ocker, dkv-fleet Petrol, cert-manager Violett, domaincheck Blau, proxmox Rotorange, unbekannte stabil per Hash). Kategorien in Alltagssprache; dazu die Texte fuer die neue Anmeldeseite. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { getModuleIdentity, type ModuleIconId } from '@/lib/module-identity';
|
||||||
|
|
||||||
|
const GLYPHS: Record<ModuleIconId, ReactNode> = {
|
||||||
|
radar: (
|
||||||
|
<>
|
||||||
|
<path d="M19.07 4.93A10 10 0 0 0 6.99 3.34" />
|
||||||
|
<path d="M4 6h.01" />
|
||||||
|
<path d="M2.29 9.62A10 10 0 1 0 21.31 8.35" />
|
||||||
|
<path d="M16.24 7.76A6 6 0 1 0 8.23 16.67" />
|
||||||
|
<path d="M12 18h.01" />
|
||||||
|
<path d="M17.99 11.66A6 6 0 0 1 15.77 16.67" />
|
||||||
|
<circle cx="12" cy="12" r="2" />
|
||||||
|
<path d="m13.41 10.59 5.66-5.66" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
fuel: (
|
||||||
|
<>
|
||||||
|
<line x1="3" x2="15" y1="22" y2="22" />
|
||||||
|
<line x1="4" x2="14" y1="9" y2="9" />
|
||||||
|
<path d="M14 22V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v18" />
|
||||||
|
<path d="M14 13h2a2 2 0 0 1 2 2v2a2 2 0 0 0 2 2a2 2 0 0 0 2-2V9.83a2 2 0 0 0-.59-1.42L18 5" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
certificate: (
|
||||||
|
<>
|
||||||
|
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
|
||||||
|
<path d="m9 12 2 2 4-4" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
globe: (
|
||||||
|
<>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
||||||
|
<path d="M2 12h20" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
server: (
|
||||||
|
<>
|
||||||
|
<rect x="2" y="2" width="20" height="8" rx="2" />
|
||||||
|
<rect x="2" y="14" width="20" height="8" rx="2" />
|
||||||
|
<line x1="6" x2="6.01" y1="6" y2="6" />
|
||||||
|
<line x1="6" x2="6.01" y1="18" y2="18" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
tile: (
|
||||||
|
<>
|
||||||
|
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
|
||||||
|
<path d="m3.3 7 8.7 5 8.7-5" />
|
||||||
|
<path d="M12 22V12" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ModuleTileProps {
|
||||||
|
slug: string;
|
||||||
|
/** Kantenlaenge in Pixeln (Seitenleiste 20, Karten 40, Seitenkopf 36). */
|
||||||
|
size?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App-Kachel eines Moduls: abgerundetes Quadrat in der Modulfarbe mit weissem
|
||||||
|
* Symbol (siehe lib/module-identity.ts). Rein dekorativ — der Modulname steht
|
||||||
|
* immer als Text daneben.
|
||||||
|
*/
|
||||||
|
export function ModuleTile({ slug, size = 20, className = '' }: ModuleTileProps) {
|
||||||
|
const { color, icon } = getModuleIdentity(slug);
|
||||||
|
const glyph = Math.round(size * 0.6);
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
data-module-tile={slug}
|
||||||
|
className={`inline-flex shrink-0 items-center justify-center text-white ${className}`}
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: Math.max(4, Math.round(size * 0.24)),
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={glyph}
|
||||||
|
height={glyph}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={size <= 24 ? 2.4 : 2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
{GLYPHS[icon]}
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Modul-Identitaet (Design „Mosaik“): jedes Modul bekommt eine feste Farbe
|
||||||
|
* und ein Symbol — wie die App-Kacheln bei Microsoft 365. Die Kachel wird in
|
||||||
|
* der Seitenleiste (20 px), auf den Marktplatz-Karten (40 px) und im Kopf der
|
||||||
|
* Modulseiten (36 px) gezeigt, immer mit weissem Symbol.
|
||||||
|
*
|
||||||
|
* Kontrast weisses Symbol gegen Kachel (WCAG, gemessen): tender-radar 3,8:1
|
||||||
|
* (Ockerton gegenueber dem Entwurf oklch(0.68 0.14 70) = 2,96:1 abgedunkelt),
|
||||||
|
* dkv-fleet 4,5:1, cert-manager 5,1:1, domaincheck 4,3:1, proxmox 4,6:1,
|
||||||
|
* Reserve gruen 4,1:1, Reserve pink 4,3:1 — alle ueber den geforderten 3:1
|
||||||
|
* fuer grafische Elemente.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ModuleIconId =
|
||||||
|
| 'radar'
|
||||||
|
| 'fuel'
|
||||||
|
| 'certificate'
|
||||||
|
| 'globe'
|
||||||
|
| 'server'
|
||||||
|
| 'tile';
|
||||||
|
|
||||||
|
export interface ModuleIdentity {
|
||||||
|
/** Kachelfarbe als OKLCH-Farbwert. */
|
||||||
|
color: string;
|
||||||
|
/** Symbol auf der Kachel. */
|
||||||
|
icon: ModuleIconId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KNOWN: Record<string, ModuleIdentity> = {
|
||||||
|
'tender-radar': { color: 'oklch(0.62 0.135 68)', icon: 'radar' },
|
||||||
|
'dkv-fleet': { color: 'oklch(0.56 0.09 195)', icon: 'fuel' },
|
||||||
|
'cert-manager': { color: 'oklch(0.55 0.14 292)', icon: 'certificate' },
|
||||||
|
domaincheck: { color: 'oklch(0.58 0.14 250)', icon: 'globe' },
|
||||||
|
proxmox: { color: 'oklch(0.58 0.16 35)', icon: 'server' },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Reservefarben fuer unbekannte Module (stabil per Kennungs-Hash). */
|
||||||
|
const RESERVE = ['oklch(0.58 0.12 150)', 'oklch(0.60 0.15 350)'] as const;
|
||||||
|
|
||||||
|
function stableHash(value: string): number {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
hash = (hash * 31 + value.charCodeAt(i)) >>> 0;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Farbe und Symbol eines Moduls; unbekannte Kennungen bekommen stabil eine Reservefarbe. */
|
||||||
|
export function getModuleIdentity(slug: string): ModuleIdentity {
|
||||||
|
const known = KNOWN[slug];
|
||||||
|
if (known) return known;
|
||||||
|
return { color: RESERVE[stableHash(slug) % RESERVE.length], icon: 'tile' };
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert den Anzeigenamen einer Modulkategorie in Alltagssprache
|
||||||
|
* (`domain-tools` → „Domains“). Unbekannte Kategorien behalten ihre Kennung.
|
||||||
|
*/
|
||||||
|
export function useCategoryLabel(): (slug: string) => string {
|
||||||
|
const t = useTranslations('moduleCategories');
|
||||||
|
return useCallback(
|
||||||
|
// `t.has` fehlt in einfachen Test-Attrappen von next-intl — dann Kennung.
|
||||||
|
(slug: string) => (typeof t.has === 'function' && t.has(slug) ? t(slug) : slug),
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,7 +32,8 @@
|
|||||||
"networkError": "Verbindungsfehler. Bitte versuchen Sie es erneut."
|
"networkError": "Verbindungsfehler. Bitte versuchen Sie es erneut."
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"tagline": "Modulare Workflow-Plattform für Ihr Unternehmen"
|
"tagline": "Modulare Workflow-Plattform für Ihr Unternehmen",
|
||||||
|
"claim": "Ihre Werkzeuge an einem Ort."
|
||||||
},
|
},
|
||||||
"desktopDownload": {
|
"desktopDownload": {
|
||||||
"windows": "Desktop-App herunterladen (Windows)",
|
"windows": "Desktop-App herunterladen (Windows)",
|
||||||
@@ -68,7 +69,8 @@
|
|||||||
"wrongCurrentPassword": "Das aktuelle Passwort ist falsch.",
|
"wrongCurrentPassword": "Das aktuelle Passwort ist falsch.",
|
||||||
"networkError": "Verbindungsfehler. Bitte versuchen Sie es erneut.",
|
"networkError": "Verbindungsfehler. Bitte versuchen Sie es erneut.",
|
||||||
"passwordMismatch": "Die Passwörter stimmen nicht überein."
|
"passwordMismatch": "Die Passwörter stimmen nicht überein."
|
||||||
}
|
},
|
||||||
|
"loginSubtitle": "Melden Sie sich mit Ihrem Benutzerkonto an."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Suchen...",
|
"search": "Suchen...",
|
||||||
@@ -1306,5 +1308,12 @@
|
|||||||
"confirm": "Verstanden",
|
"confirm": "Verstanden",
|
||||||
"close": "Fenster schließen",
|
"close": "Fenster schließen",
|
||||||
"contentLabel": "Änderungen"
|
"contentLabel": "Änderungen"
|
||||||
|
},
|
||||||
|
"moduleCategories": {
|
||||||
|
"domain-tools": "Domains",
|
||||||
|
"security-tools": "Sicherheit",
|
||||||
|
"fleet": "Fuhrpark",
|
||||||
|
"infrastructure": "Infrastruktur",
|
||||||
|
"procurement": "Beschaffung"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@
|
|||||||
"networkError": "Connection error. Please try again."
|
"networkError": "Connection error. Please try again."
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
"tagline": "Modular workflow platform for your organization"
|
"tagline": "Modular workflow platform for your organization",
|
||||||
|
"claim": "Your tools in one place."
|
||||||
},
|
},
|
||||||
"desktopDownload": {
|
"desktopDownload": {
|
||||||
"windows": "Download desktop app (Windows)",
|
"windows": "Download desktop app (Windows)",
|
||||||
@@ -68,7 +69,8 @@
|
|||||||
"wrongCurrentPassword": "The current password is incorrect.",
|
"wrongCurrentPassword": "The current password is incorrect.",
|
||||||
"networkError": "Connection error. Please try again.",
|
"networkError": "Connection error. Please try again.",
|
||||||
"passwordMismatch": "Passwords do not match."
|
"passwordMismatch": "Passwords do not match."
|
||||||
}
|
},
|
||||||
|
"loginSubtitle": "Sign in with your user account."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Search...",
|
"search": "Search...",
|
||||||
@@ -1306,5 +1308,12 @@
|
|||||||
"confirm": "Got it",
|
"confirm": "Got it",
|
||||||
"close": "Close window",
|
"close": "Close window",
|
||||||
"contentLabel": "Changes"
|
"contentLabel": "Changes"
|
||||||
|
},
|
||||||
|
"moduleCategories": {
|
||||||
|
"domain-tools": "Domains",
|
||||||
|
"security-tools": "Security",
|
||||||
|
"fleet": "Fleet",
|
||||||
|
"infrastructure": "Infrastructure",
|
||||||
|
"procurement": "Procurement"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user