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>
83 lines
4.0 KiB
TypeScript
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>
|
|
)
|
|
}
|