Files
mission-control-v2/frontend/src/components/diagramme/Messring.tsx
T
HitonabiandClaude Opus 5.5 948e55e4f2 design v5: Übersicht als Vorschau, Grundbausteine im neuen Look
Tiefblauer Grund mit Lichtflecken in den Bereichsfarben, Glasflächen mit
Lichtkante (flaeche), ruhigere Schrift (Überschriften normal geschrieben,
Großbuchstaben nur als kleines Etikett) und dezente Bewegung.

- Startseite neu: Geräte-Ring mit Gesamtlage, drei Eckwerte, Instrumente
  (8 Messringe), Bereichskarten mit Kacheln je Modell, Baustein und Gerät
  (Symbol, Zustandspunkt, Update direkt an der Kachel), Speicher/NAS,
  Zeitleisten „Als Nächstes“ und „Zuletzt eingespielt“.
- Panel, Schildchen, Kennzahl, Verlaufslinie, Seitenleiste und Kopf im
  neuen Look — wirkt schon auf allen Seiten.
- Symbole je Gerät (lib/geraetebild): Farbe des Bereichs, Zustände nur als
  Punkt. Vorbilder: Heimdall, Dashy, Homepage, umbrelOS, Pulse.

Noch nicht ausgerollt (kein dist): erst nach dem Ja des Users.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:22:39 +02:00

83 lines
4.0 KiB
TypeScript

import type { CSSProperties } from "react"
import { cn } from "cn"
import type { Bereich } from "@/lib/navigation"
import { type Einheit, wertText } from "@/lib/messung"
import type { Messpunkt } from "@/lib/typen"
import { Sparkline } from "./Sparkline"
// Ein Messwert als Ring (Design v5, 25.09.2026): der Bogen füllt sich bis zum Wert, im Normalfall in der Farbe des
// Bereichs, ab der gelben Grenze bernstein, ab der roten rot — dazu das Wort („hoch“, „kritisch“), damit es nicht
// nur an der Farbe hängt. Zwei feine Striche am Rand zeigen, wo die Grenzen liegen; darunter die Linie der Stunde.
const ZUSTAND = {
ruhig: { wert: "text-foreground", marke: null },
gelb: { wert: "text-bernstein", marke: "hoch" },
rot: { wert: "text-rot-text", marke: "kritisch" },
} as const
const MITTE = 48
const RADIUS = 37
const UMFANG = 2 * Math.PI * RADIUS
const BOGEN = UMFANG * 0.75
/** Punkt auf dem Rand für einen Anteil 0…1 des 270°-Bogens (Beginn unten links, im Uhrzeigersinn). */
function randpunkt(anteil: number, r: number) {
const winkel = ((135 + 270 * anteil) * Math.PI) / 180
return { x: MITTE + r * Math.cos(winkel), y: MITTE + r * Math.sin(winkel) }
}
export function Messring({ label, wert, einheit, punkte, zustand = "ruhig", bereich, grenzen, max = 100, zeitraumText, i = 0 }: {
label: string
wert: number | null | undefined
einheit: Einheit
punkte?: Messpunkt[]
zustand?: keyof typeof ZUSTAND
bereich: Bereich
/** Gelbe und rote Grenze in der Einheit des Werts. */
grenzen: [number, number]
max?: number
zeitraumText: string
/** Reihenfolge für das Auftauchen. */
i?: number
}) {
const z = ZUSTAND[zustand]
const farbe = zustand === "rot" ? "var(--rot)" : zustand === "gelb" ? "var(--bernstein)" : bereich === "box" ? "var(--box)" : "var(--lab)"
const anteil = wert == null ? 0 : Math.min(Math.max(wert / max, 0), 1)
const [zahl, einheitText] = wertText(wert, einheit).split(" ")
return (
<div className="flaeche-innen flex min-w-0 flex-col items-center gap-1 rounded-2xl px-2 pt-2.5 pb-2">
<div className="relative size-[88px]">
<svg viewBox="0 0 96 96" className="size-full" aria-hidden>
<circle cx={MITTE} cy={MITTE} r={RADIUS} fill="none" stroke="rgb(255 255 255 / 0.07)" strokeWidth={7}
strokeLinecap="round" strokeDasharray={`${BOGEN} ${UMFANG}`} transform={`rotate(135 ${MITTE} ${MITTE})`} />
{grenzen.map((g, n) => {
const a = randpunkt(Math.min(g / max, 1), RADIUS + 6)
const b = randpunkt(Math.min(g / max, 1), RADIUS + 9.5)
return <line key={g} x1={a.x} y1={a.y} x2={b.x} y2={b.y} strokeWidth={1.5} strokeLinecap="round"
stroke={n === 0 ? "var(--bernstein)" : "var(--rot)"} opacity={0.55} />
})}
{wert != null && anteil > 0 && (
<circle cx={MITTE} cy={MITTE} r={RADIUS} fill="none" stroke={farbe} strokeWidth={7} strokeLinecap="round"
strokeDasharray={`${Math.max(BOGEN * anteil, 0.5)} ${UMFANG}`} transform={`rotate(135 ${MITTE} ${MITTE})`}
className="ring-fuellt transition-[stroke-dasharray] duration-700"
style={{ "--i": i, filter: `drop-shadow(0 0 5px color-mix(in srgb, ${farbe} 65%, transparent))` } as CSSProperties} />
)}
</svg>
<span className="absolute inset-0 flex items-center justify-center pt-1">
<span className={cn("font-anzeige text-[26px] leading-none font-semibold tabular-nums", z.wert)}>{zahl}</span>
{einheitText && <span className="ml-0.5 self-center pt-1.5 text-[12px] font-medium text-text-3">{einheitText}</span>}
</span>
</div>
<span className="etikett -mt-1 flex items-center gap-1.5 text-[10.5px] text-text-3">
{label}
{z.marke && <span className={z.wert}>· {z.marke}</span>}
</span>
<div className="w-full px-1">
{punkte && punkte.length > 1
? <Sparkline punkte={punkte} max={max} farbe={farbe} hoehe={20} beschriftung={`${label}, ${zeitraumText}`} />
: <span className="block h-5" aria-hidden />}
</div>
</div>
)
}