From 7192be223ababe6a8fbbad1dd713420aa58d0d9c Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 25 Sep 2026 14:32:37 +0200 Subject: [PATCH] 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) --- .../src/components/modules/module-tile.tsx | 97 +++++++++++++++++++ apps/web/src/lib/module-identity.ts | 53 ++++++++++ apps/web/src/lib/use-category-label.ts | 17 ++++ apps/web/src/messages/de.json | 13 ++- apps/web/src/messages/en.json | 13 ++- 5 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/components/modules/module-tile.tsx create mode 100644 apps/web/src/lib/module-identity.ts create mode 100644 apps/web/src/lib/use-category-label.ts diff --git a/apps/web/src/components/modules/module-tile.tsx b/apps/web/src/components/modules/module-tile.tsx new file mode 100644 index 0000000..68dff7c --- /dev/null +++ b/apps/web/src/components/modules/module-tile.tsx @@ -0,0 +1,97 @@ +import type { ReactNode } from 'react'; +import { getModuleIdentity, type ModuleIconId } from '@/lib/module-identity'; + +const GLYPHS: Record = { + radar: ( + <> + + + + + + + + + + ), + fuel: ( + <> + + + + + + ), + certificate: ( + <> + + + + ), + globe: ( + <> + + + + + ), + server: ( + <> + + + + + + ), + tile: ( + <> + + + + + ), +}; + +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 ( + + ); +} diff --git a/apps/web/src/lib/module-identity.ts b/apps/web/src/lib/module-identity.ts new file mode 100644 index 0000000..f4dbf65 --- /dev/null +++ b/apps/web/src/lib/module-identity.ts @@ -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 = { + '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' }; +} diff --git a/apps/web/src/lib/use-category-label.ts b/apps/web/src/lib/use-category-label.ts new file mode 100644 index 0000000..a8786f7 --- /dev/null +++ b/apps/web/src/lib/use-category-label.ts @@ -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], + ); +} diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index f64e870..52658a7 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -32,7 +32,8 @@ "networkError": "Verbindungsfehler. Bitte versuchen Sie es erneut." }, "branding": { - "tagline": "Modulare Workflow-Plattform für Ihr Unternehmen" + "tagline": "Modulare Workflow-Plattform für Ihr Unternehmen", + "claim": "Ihre Werkzeuge an einem Ort." }, "desktopDownload": { "windows": "Desktop-App herunterladen (Windows)", @@ -68,7 +69,8 @@ "wrongCurrentPassword": "Das aktuelle Passwort ist falsch.", "networkError": "Verbindungsfehler. Bitte versuchen Sie es erneut.", "passwordMismatch": "Die Passwörter stimmen nicht überein." - } + }, + "loginSubtitle": "Melden Sie sich mit Ihrem Benutzerkonto an." }, "header": { "search": "Suchen...", @@ -1306,5 +1308,12 @@ "confirm": "Verstanden", "close": "Fenster schließen", "contentLabel": "Änderungen" + }, + "moduleCategories": { + "domain-tools": "Domains", + "security-tools": "Sicherheit", + "fleet": "Fuhrpark", + "infrastructure": "Infrastruktur", + "procurement": "Beschaffung" } } diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 4ba9647..6361430 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -32,7 +32,8 @@ "networkError": "Connection error. Please try again." }, "branding": { - "tagline": "Modular workflow platform for your organization" + "tagline": "Modular workflow platform for your organization", + "claim": "Your tools in one place." }, "desktopDownload": { "windows": "Download desktop app (Windows)", @@ -68,7 +69,8 @@ "wrongCurrentPassword": "The current password is incorrect.", "networkError": "Connection error. Please try again.", "passwordMismatch": "Passwords do not match." - } + }, + "loginSubtitle": "Sign in with your user account." }, "header": { "search": "Search...", @@ -1306,5 +1308,12 @@ "confirm": "Got it", "close": "Close window", "contentLabel": "Changes" + }, + "moduleCategories": { + "domain-tools": "Domains", + "security-tools": "Security", + "fleet": "Fleet", + "infrastructure": "Infrastructure", + "procurement": "Procurement" } }