Messanzeige
Ein Wert gegen seine Skala, mit schwingender Nadel, benannten Bereichen, aufleuchtenden Stufen, beziffertem Zifferblatt oder als Reihe kleiner Anzeigen. Zeiger und Pfeiltasten lesen jeden Punkt der Skala ab.
Eine von 6 Components in KPI-, Live- und Karten-Charts
Variants
Details
Was Messanzeige mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- KPI-, Live- und Karten-Charts
- Bereich
- Elements
- Variants
- 5
- Version
- 1.0.0 · seit 1.1.0
- Datei
- components/ui/gauge.tsx
- Bringt mit
- utilskiticonchartchart-figure
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 11 eigene Texte
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle GaugeProps aus components/ui/gauge.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von Omit<ChartA11y, 'label'>, Omit<ChartFigureProps, 'value'> an.
labeloptionalstringAccessible name of the chart. Default: the title.
headlineoptionalstringThe big number in the header, formatted. The gauge shows its own value, so most gauges leave it out.
variantoptionalGaugeVariantneedlezonessegmenteddialrow
valueoptionalnumberThe value. row: unused, see items.
minoptionalnumbermaxoptionalnumberformatoptionalIntl.NumberFormatOptionsValue format, e.g. { style: 'percent' } or { style: 'unit', unit: 'kilowatt' }
suffixoptionalstringUnit after the big value, e.g. " kW". Intl knows few units, this works for every one.
unitLabeloptionalstringName of the measure, shown under the value
zonesoptionalGaugeZone[]zones: the named ranges
targetoptionalnumberA target mark on the scale
targetLabeloptionalstringsegmentsoptionalnumbersegmented: number of steps. Default 10.
itemsoptionalGaugeItem[]row: one small gauge per item
Eingebaute Texte
Diese Texte schreibt Messanzeige selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 11 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| high | Englisch Running high | Deutsch Fast ausgelastet |
| span | Englisch {a} to {b} | Deutsch {a} bis {b} |
| reading | Englisch Reading | Deutsch Ablesen |
| target | Englisch Target | Deutsch Ziel |
| of | Englisch {value} of {max} | Deutsch {value} von {max} |
| range | Englisch Range | Deutsch Bereich |
| zone | Englisch In range {zone} | Deutsch Im Bereich {zone} |
| item | Englisch Measure | Deutsch Messgröße |
| value | Englisch Value | Deutsch Wert |
| scale | Englisch Scale from {min} to {max} | Deutsch Skala von {min} bis {max} |
| steps | Englisch {n} of {total} steps | Deutsch {n} von {total} Stufen |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Messanzeige reagiert selbst auf die Tasten Escape, Pfeil hoch, Pfeil runter, Pfeil links, Pfeil rechts, Pos1 und Ende.
- Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.
Bewegung
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 1 Stellen im Code schalten Bewegung dann ab.
- Ohne Animationen, sie steht sofort ruhig da.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 398 Zeilen
'use client';
import { useState } from 'react';
import { cn, round, useTexts } from '@/lib/kit';
import { Icon } from '@/components/ui/icon';
import { ChartRoot, ChartSurface, ChartTable, CountUp, arcPath, fill, numberFormatter, polar, tickFormatter, useEntrance, useIntlLocale, useWidth, type ChartA11y } from '@/components/ui/chart';
import { ChartFigure, type ChartFigureProps } from '@/components/ui/chart-figure';
/*
* One value against its scale.
* needle a half circle, the arc fills and a needle swings to the value
* zones the scale in named ranges (good, watch, critical), the needle
* names the range it stands in, with an icon, not color alone
* segmented the arc in equal steps that light up one after another
* dial three quarters of a circle with a numbered scale
* row several small gauges side by side, one value each
*
* The pointer reads the scale: a ghost mark follows it along the arc and
* names the value there. Arrow keys move that reading in steps, Escape
* clears it. The final state is rendered on the server.
*/
export type GaugeVariant = 'needle' | 'zones' | 'segmented' | 'dial' | 'row';
export interface GaugeZone {
/** Upper end of the range, the last one should end at max */
to: number;
label: string;
/** good, watch or critical take the status colors, neutral the grey */
tone: 'good' | 'watch' | 'critical' | 'neutral';
}
export interface GaugeItem {
label: string;
value: number;
min?: number;
max?: number;
/** Note below the value, e.g. "of 16 GB" */
note?: string;
}
export interface GaugeProps extends Omit<ChartA11y, 'label'>, Omit<ChartFigureProps, 'value'> {
/** Accessible name of the chart. Default: the title. */
label?: string;
/** The big number in the header, formatted. The gauge shows its own value, so most gauges leave it out. */
headline?: string;
variant?: GaugeVariant;
/** The value. row: unused, see items. */
value?: number;
min?: number;
max?: number;
/** Value format, e.g. { style: 'percent' } or { style: 'unit', unit: 'kilowatt' } */
format?: Intl.NumberFormatOptions;
/** Unit after the big value, e.g. " kW". Intl knows few units, this works for every one. */
suffix?: string;
/** Name of the measure, shown under the value */
unitLabel?: string;
/** zones: the named ranges */
zones?: GaugeZone[];
/** A target mark on the scale */
target?: number;
targetLabel?: string;
/** segmented: number of steps. Default 10. */
segments?: number;
/** row: one small gauge per item */
items?: GaugeItem[];
className?: string;
}
const TEXTS = {
en: { high: 'Running high', span: '{a} to {b}', reading: 'Reading', target: 'Target', of: '{value} of {max}', range: 'Range', zone: 'In range {zone}', item: 'Measure', value: 'Value', scale: 'Scale from {min} to {max}', steps: '{n} of {total} steps' },
de: { high: 'Fast ausgelastet', span: '{a} bis {b}', reading: 'Ablesen', target: 'Ziel', of: '{value} von {max}', range: 'Bereich', zone: 'Im Bereich {zone}', item: 'Messgröße', value: 'Wert', scale: 'Skala von {min} bis {max}', steps: '{n} von {total} Stufen' },
};
const TONE: Record<GaugeZone['tone'], string> = {
good: 'var(--success)',
watch: 'var(--warning)',
critical: 'var(--destructive)',
neutral: 'var(--muted-foreground)',
};
const TONE_ICON = { good: 'check-circle', watch: 'alert-circle', critical: 'x-circle', neutral: null } as const;
interface ArcGeo {
cx: number;
cy: number;
r: number;
a0: number;
a1: number;
w: number;
h: number;
stroke: number;
}
const arcGeo = (width: number, sweep: 180 | 270, stroke: number, maxR = 150): ArcGeo => {
const r = Math.max(48, Math.min(maxR, (width - stroke - 8) / 2));
if (sweep === 180) {
const h = r + stroke / 2 + 4;
return { cx: round(width / 2), cy: round(r + stroke / 2 + 4), r, a0: -90, a1: 90, w: width, h: round(h + 4), stroke };
}
// 270: the open part at the bottom, the box ends a little below the center
const h = r + stroke / 2 + 4 + r * 0.72;
return { cx: round(width / 2), cy: round(r + stroke / 2 + 4), r, a0: -135, a1: 135, w: width, h: round(h), stroke };
};
/**
* One value against its scale: a needle, named zones, steps, a numbered dial or a row of small gauges.
* <Gauge variant="zones" value={72} zones={[{ to: 60, label: 'Calm', tone: 'good' }, …]} />
*/
export function Gauge(props: GaugeProps) {
const { variant = 'needle', value = 0, min = 0, max = 100, format, suffix = '', unitLabel, zones = [], target, targetLabel, segments = 10, items = [], label, summary, labels, animate = true, title, headline: headValue, delta, meta, source, framed, className } = props;
const t = useTexts(TEXTS, undefined);
const intl = useIntlLocale();
const fmtN = numberFormatter(intl, format);
const fmt = (n: number) => fmtN(n) + suffix;
const [wrapRef, width] = useWidth<HTMLDivElement>(480);
const [rootRef, anim] = useEntrance<HTMLDivElement>(animate);
const [reading, setReading] = useState<number | null>(null);
const name = label ?? title ?? unitLabel ?? '';
const clamp = (v: number, lo = min, hi = max) => Math.max(lo, Math.min(hi, v));
const span = max - min || 1;
/* ---------- row: several small gauges ---------- */
if (variant === 'row') {
const cols = width >= 720 ? Math.min(4, items.length) : width >= 420 ? 2 : 1;
return (
<ChartFigure title={title} value={headValue} delta={delta} meta={meta} source={source} framed={framed} className={className}>
<ChartRoot anim={anim} rootRef={rootRef} className="[--gauge-track:color-mix(in_oklab,var(--foreground)_9%,var(--card))]">
<div ref={wrapRef}>
<ul className="grid gap-px overflow-hidden rounded-lg border border-border bg-border" style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}>
{items.map((it, i) => {
const lo = it.min ?? 0;
const hi = it.max ?? 100;
const share = Math.max(0, Math.min(1, (it.value - lo) / (hi - lo || 1)));
const cell = (width - (cols - 1)) / cols;
const g = arcGeo(Math.min(cell - 32, 200), 180, 10, 80);
const angle = g.a0 + share * (g.a1 - g.a0);
const tone = share >= 0.9 ? 'var(--destructive)' : share >= 0.75 ? 'var(--warning)' : 'var(--primary)';
return (
<li key={it.label} className="group flex flex-col items-center bg-card px-4 pb-4 pt-5">
<div className="relative w-full" style={{ maxWidth: g.w }}>
<svg width="100%" viewBox={`0 0 ${g.w} ${g.h}`} aria-hidden className="block overflow-visible">
<path d={arcPath(g.cx, g.cy, g.r, g.a0, g.a1)} fill="none" stroke="var(--gauge-track)" strokeWidth={g.stroke} strokeLinecap="round" />
<path d={arcPath(g.cx, g.cy, g.r, g.a0, Math.max(g.a0 + 0.5, angle))} pathLength={1} fill="none" stroke={tone} strokeWidth={g.stroke} strokeLinecap="round" className={anim ? 'kitch-draw' : undefined} style={anim ? ({ '--kitch-d': `${150 + i * 120}ms`, '--kitch-t': '1.1s' } as React.CSSProperties) : undefined} />
</svg>
<p className="absolute inset-x-0 bottom-0 text-center text-[22px] font-semibold leading-none tracking-[-0.02em] text-foreground">
<CountUp value={round(share * 100, 0)} format={(n) => `${n}${intl === 'de-DE' ? ' %' : '%'}`} anim={anim} delay={150 + i * 120} />
</p>
</div>
<p className="mt-3 text-center text-[13px] font-medium text-foreground">{it.label}</p>
{it.note && <p className="mt-0.5 text-center text-[12px] tabular-nums text-muted-foreground">{it.note}</p>}
{share >= 0.75 && (
<p className="mt-2 inline-flex items-center gap-1 text-[12px] font-medium text-warning-foreground">
<Icon name="alert-circle" size={13} strokeWidth={2} />
{t.high}
</p>
)}
</li>
);
})}
{/* Empty cells close the last row, so no hairline colour shows through as a grey block */}
{Array.from({ length: (cols - (items.length % cols)) % cols }, (_, k) => (
<li key={`fill${k}`} aria-hidden className="bg-card" />
))}
</ul>
</div>
<ChartTable caption={name} head={[t.item, t.value]} rows={items.map((it) => [it.label, fill(t.of, { value: fmt(it.value), max: fmt(it.max ?? 100) })])} />
</ChartRoot>
</ChartFigure>
);
}
/* ---------- one gauge ---------- */
const sweep = variant === 'dial' ? 270 : 180;
const stroke = variant === 'segmented' ? 18 : variant === 'dial' ? 12 : 16;
const g = arcGeo(Math.min(width, 420), sweep, stroke, variant === 'dial' ? 140 : 160);
const angleOf = (v: number) => g.a0 + ((clamp(v) - min) / span) * (g.a1 - g.a0);
const angle = angleOf(value);
const zone = zones.find((z) => value <= z.to) ?? zones[zones.length - 1];
const zoneStart = (i: number) => (i === 0 ? min : zones[i - 1].to);
const needleAngle = anim === 'wait' ? g.a0 : angle;
const share = (clamp(value) - min) / span;
const lit = Math.round(share * segments);
// Dial ticks: a nice step with 5 to 10 labels
const tickStep = (() => {
const raw = span / 8;
const mag = 10 ** Math.floor(Math.log10(raw));
const n = raw / mag;
return (n <= 1 ? 1 : n <= 2 ? 2 : n <= 2.5 ? 2.5 : n <= 5 ? 5 : 10) * mag;
})();
const ticks: number[] = [];
for (let v = min; v <= max + tickStep / 1000; v += tickStep) ticks.push(round(v, 6));
const readAt = (clientX: number, clientY: number, el: HTMLElement) => {
const svg = el.querySelector('svg');
if (!svg) return;
const b = svg.getBoundingClientRect();
const k = g.w / (b.width || g.w);
const dx = (clientX - b.left) * k - g.cx;
const dy = (clientY - b.top) * k - g.cy;
const dist = Math.hypot(dx, dy);
if (dist < g.r * 0.45 || dist > g.r + stroke * 1.6) return setReading(null);
let deg = (Math.atan2(dy, dx) * 180) / Math.PI + 90;
if (deg > 180) deg -= 360;
if (deg < g.a0 - 8 || deg > g.a1 + 8) return setReading(null);
const v = min + ((Math.max(g.a0, Math.min(g.a1, deg)) - g.a0) / (g.a1 - g.a0)) * span;
setReading(round(v, 6));
};
const step = span / (variant === 'segmented' ? segments : 20);
const onKeyDown = (e: React.KeyboardEvent) => {
const cur = reading ?? value;
let next: number | null = cur;
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') next = clamp(cur + step);
else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') next = clamp(cur - step);
else if (e.key === 'Home') next = min;
else if (e.key === 'End') next = max;
else if (e.key === 'Escape') next = null;
else return;
e.preventDefault();
setReading(next === null ? null : round(next, 6));
};
const readZone = reading !== null ? zones.find((z) => reading <= z.to) : undefined;
const live = reading !== null ? `${t.reading} ${fmt(reading)}${readZone ? `, ${readZone.label}` : ''}` : '';
const auto = `${fmt(value)}. ${fill(t.scale, { min: fmt(min), max: fmt(max) })}${zone ? `. ${fill(t.zone, { zone: zone.label })}` : ''}${target !== undefined ? `. ${targetLabel ?? t.target} ${fmt(target)}` : ''}`;
const table = <ChartTable caption={name} head={[t.range, t.value]} rows={[[unitLabel ?? name, fmt(value)], ...(target !== undefined ? [[targetLabel ?? t.target, fmt(target)]] : []), ...zones.map((z, i) => [z.label, fill(t.span, { a: fmt(zoneStart(i)), b: fmt(z.to) })])]} />;
const mainColor = variant === 'zones' && zone ? TONE[zone.tone] : 'var(--primary)';
const readAngle = reading !== null ? angleOf(reading) : null;
const [rx, ry] = readAngle !== null ? polar(g.cx, g.cy, g.r + stroke / 2 + 10, readAngle) : [0, 0];
return (
<ChartFigure title={title} value={headValue} delta={delta} meta={meta} source={source} framed={framed} className={className}>
<ChartRoot anim={anim} rootRef={rootRef} className="[--gauge-track:color-mix(in_oklab,var(--foreground)_9%,var(--card))]">
<div ref={wrapRef} className="flex flex-col items-center">
<ChartSurface
label={name}
summary={summary ?? auto}
labels={labels}
live={live}
table={table}
onKeyDown={onKeyDown}
onBlur={() => setReading(null)}
onPointerMove={(e) => readAt(e.clientX, e.clientY, e.currentTarget)}
onPointerDown={(e) => readAt(e.clientX, e.clientY, e.currentTarget)}
onPointerLeave={(e) => e.pointerType === 'mouse' && setReading(null)}
className="w-full"
style={{ maxWidth: g.w }}
>
<svg width="100%" viewBox={`0 0 ${g.w} ${g.h}`} aria-hidden className="block overflow-visible">
{/* Track */}
{variant !== 'segmented' && <path d={arcPath(g.cx, g.cy, g.r, g.a0, g.a1)} fill="none" stroke="var(--gauge-track)" strokeWidth={stroke} strokeLinecap="round" />}
{/* Zones as quiet bands on the inside */}
{variant === 'zones' &&
zones.map((z, i) => {
const a = angleOf(zoneStart(i));
const b = angleOf(z.to);
const isIn = zone === z;
return (
<path
key={z.label}
d={arcPath(g.cx, g.cy, g.r - stroke / 2 - 9, a + (i ? 0.8 : 0), b - (i < zones.length - 1 ? 0.8 : 0))}
fill="none"
stroke={TONE[z.tone]}
strokeOpacity={isIn ? 1 : 0.45}
strokeWidth={isIn ? 6 : 4}
strokeLinecap="butt"
className={cn('transition-[stroke-opacity,stroke-width] duration-300', anim && 'kitch-fade')}
style={anim ? ({ '--kitch-d': `${200 + i * 120}ms` } as React.CSSProperties) : undefined}
/>
);
})}
{/* Value arc */}
{variant === 'segmented'
? Array.from({ length: segments }, (_, i) => {
const gap = 2.4;
const a = g.a0 + (i * (g.a1 - g.a0)) / segments + gap / 2;
const b = g.a0 + ((i + 1) * (g.a1 - g.a0)) / segments - gap / 2;
const on = i < lit;
return (
<path
key={i}
d={arcPath(g.cx, g.cy, g.r, a, b)}
fill="none"
stroke={on ? 'var(--primary)' : 'var(--gauge-track)'}
strokeWidth={stroke}
className={cn(on && anim && 'kitch-fade')}
style={on && anim ? ({ '--kitch-d': `${200 + i * 90}ms` } as React.CSSProperties) : undefined}
/>
);
})
: value > min && (
<path
d={arcPath(g.cx, g.cy, g.r, g.a0, Math.max(g.a0 + 0.5, angle))}
pathLength={1}
fill="none"
stroke={mainColor}
strokeWidth={stroke}
strokeLinecap="round"
className={anim ? 'kitch-draw' : undefined}
style={anim ? ({ '--kitch-d': '150ms', '--kitch-t': '1.2s' } as React.CSSProperties) : undefined}
/>
)}
{/* Dial scale */}
{variant === 'dial' &&
ticks.map((v, i) => {
const a = angleOf(v);
const [x1, y1] = polar(g.cx, g.cy, g.r - stroke / 2 - 6, a);
const [x2, y2] = polar(g.cx, g.cy, g.r - stroke / 2 - 12, a);
const [tx, ty] = polar(g.cx, g.cy, g.r - stroke / 2 - 26, a);
const minor = polar(g.cx, g.cy, g.r - stroke / 2 - 6, angleOf(v + tickStep / 2));
const minor2 = polar(g.cx, g.cy, g.r - stroke / 2 - 10, angleOf(v + tickStep / 2));
return (
<g key={v}>
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke="var(--kitch-axis)" strokeWidth="1.5" />
{i < ticks.length - 1 && <line x1={minor[0]} y1={minor[1]} x2={minor2[0]} y2={minor2[1]} stroke="var(--kitch-grid)" strokeWidth="1" />}
<text x={tx} y={ty} dy="0.34em" textAnchor="middle" className={cn('text-[11px] tabular-nums', value >= v ? 'fill-foreground' : 'fill-muted-foreground')}>
{tickFormatter(intl, format)(v)}
</text>
</g>
);
})}
{/* Target */}
{target !== undefined &&
(() => {
const a = angleOf(target);
const [x1, y1] = polar(g.cx, g.cy, g.r - stroke / 2 - 3, a);
const [x2, y2] = polar(g.cx, g.cy, g.r + stroke / 2 + 5, a);
return <line x1={x1} y1={y1} x2={x2} y2={y2} stroke="var(--foreground)" strokeWidth="2.5" strokeLinecap="round" className={anim ? 'kitch-fade' : undefined} style={anim ? ({ '--kitch-d': '900ms' } as React.CSSProperties) : undefined} />;
})()}
{/* Needle */}
{(variant === 'needle' || variant === 'zones' || variant === 'dial') && (
<g style={{ transform: `rotate(${round(needleAngle)}deg)`, transformOrigin: `${g.cx}px ${g.cy}px`, transition: anim ? 'transform 1.3s cubic-bezier(.3,1.35,.5,1) .15s' : undefined }} className="motion-reduce:!transition-none">
<path d={`M${g.cx - 3.5} ${g.cy}L${g.cx} ${round(g.cy - g.r + stroke / 2 + 6)}L${g.cx + 3.5} ${g.cy}Z`} fill="var(--foreground)" />
</g>
)}
{(variant === 'needle' || variant === 'zones' || variant === 'dial') && <circle cx={g.cx} cy={g.cy} r={8} fill="var(--foreground)" stroke="var(--kitch-surface)" strokeWidth="3" />}
{/* Reading: a ghost mark that follows the pointer */}
{readAngle !== null && (
<g>
<line x1={polar(g.cx, g.cy, g.r - stroke / 2 - 2, readAngle)[0]} y1={polar(g.cx, g.cy, g.r - stroke / 2 - 2, readAngle)[1]} x2={polar(g.cx, g.cy, g.r + stroke / 2 + 2, readAngle)[0]} y2={polar(g.cx, g.cy, g.r + stroke / 2 + 2, readAngle)[1]} stroke="var(--foreground)" strokeOpacity="0.6" strokeWidth="1.5" />
<text x={rx} y={ry} dy="0.34em" textAnchor={readAngle < -10 ? 'end' : readAngle > 10 ? 'start' : 'middle'} className="text-[12px] font-semibold tabular-nums" fill="var(--foreground)" style={{ paintOrder: 'stroke', stroke: 'var(--kitch-surface)', strokeWidth: 4 }}>
{fmt(reading!)}
</text>
</g>
)}
{/* Scale ends of the half circle */}
{sweep === 180 && (
<g className="text-[11px] tabular-nums" fill="var(--muted-foreground)">
<text x={g.cx - g.r} y={g.cy + 18} textAnchor="middle">
{fmt(min)}
</text>
<text x={g.cx + g.r} y={g.cy + 18} textAnchor="middle">
{fmt(max)}
</text>
</g>
)}
</svg>
</ChartSurface>
{/* The value under the arc */}
<div className={cn('flex flex-col items-center text-center', sweep === 180 ? 'mt-6' : '-mt-10')}>
<p className="text-[34px] font-semibold leading-none tracking-[-0.03em] text-foreground sm:text-[40px]">
<CountUp value={value} format={fmt} anim={anim} duration={1200} delay={150} />
</p>
{unitLabel && <p className="mt-2 text-[13px] text-muted-foreground">{unitLabel}</p>}
{variant === 'segmented' && !unitLabel && <p className="mt-1 text-[12px] tabular-nums text-muted-foreground">{fill(t.steps, { n: String(lit), total: String(segments) })}</p>}
{variant === 'zones' && zone && (
<p className="mt-3 inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1 text-[12px] font-medium text-foreground">
{TONE_ICON[zone.tone] && <Icon name={TONE_ICON[zone.tone]!} size={14} strokeWidth={2} style={{ color: TONE[zone.tone] }} />}
{zone.label}
</p>
)}
{target !== undefined && (
<p className="mt-2 inline-flex items-center gap-1.5 text-[12px] text-muted-foreground">
<span aria-hidden className="h-3 w-[2.5px] rounded-full bg-foreground" />
{targetLabel ?? t.target} <span className="tabular-nums text-foreground">{fmt(target)}</span>
</p>
)}
</div>
{variant === 'zones' && zones.length > 0 && (
<ul className="mt-6 grid w-full max-w-md grid-cols-1 gap-1 sm:grid-cols-3">
{zones.map((z, i) => {
const isIn = zone === z;
return (
<li key={z.label} className={cn('flex items-center gap-2 rounded-md px-2.5 py-2 text-[12px] transition-colors duration-300', isIn ? 'bg-accent text-foreground' : 'text-muted-foreground')}>
<span aria-hidden className="h-2 w-4 shrink-0 rounded-full" style={{ background: TONE[z.tone], opacity: isIn ? 1 : 0.55 }} />
<span className="min-w-0 flex-1 truncate font-medium">{z.label}</span>
<span className="tabular-nums">{fill(t.span, { a: fmt(zoneStart(i)), b: fmt(z.to) })}</span>
</li>
);
})}
</ul>
)}
</div>
</ChartRoot>
</ChartFigure>
);
}Häufige Fragen
Wie installiere ich Messanzeige?
Mit aiw add @ai-website/gauge. Der Befehl legt die Datei components/ui/gauge.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon, chart und chart-figure. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Messanzeige?
5 Varianten, Nadel, Bereiche, Stufen, Zifferblatt und Reihe von Anzeigen. Du wählst sie mit der Prop variant, zum Beispiel variant="zones".
Kann ich die Texte von Messanzeige übersetzen?
Ja. Messanzeige bringt 11 eigene Texte auf Deutsch und Englisch mit, etwa „Fast ausgelastet“, „Ablesen“ und „Ziel“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Messanzeige einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Messanzeige mit aiw_components und installieren sie mit aiw_add @ai-website/gauge. Nenn im Prompt die Variante, etwa variant="needle", dann schreibt der Assistent deine Inhalte direkt in die Props.
Kann ich Messanzeige ohne Code anpassen?
Ja, im Studio. Alle 5 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden Gauge-Code zum Kopieren.