fix(layout): kollidierenden und überlaufenden Text beseitigen
Gemeldeter Fehler: In der Ringlegende klebten Bezeichnung und Wert aneinander
("Gestellt189,1 T€"). Ursache war eine eigene Regression — beim Beheben der
abgeschnittenen Legendennamen war das truncate entfallen, ohne Ersatz. Damit
schrumpft die Namensspalte durch min-w-0 + grow auf null Breite, und der Text
läuft sichtbar über seinen Kasten hinaus:
1280 px Kastenbreite 0 px, Text 43 px darüber hinaus
1440 px Kastenbreite 25 px, Text 18 px darüber hinaus
1600 px unauffällig
Genau deshalb war es auf den Screenshots nicht zu sehen: Der Fehler tritt nur in
bestimmten Fensterbreiten auf.
Neu: scripts/verify-layout.mjs + npm run verify:layout. Lädt alle sechs Seiten in
sieben Breiten (1024–1920) und misst jede Textzeile über Range.getClientRects.
Gemessen wird auf Textzeilen, nicht auf Elementkästen — sonst melden benachbarte
Tabellenzellen mit Innenabstand falsch positiv. Mindestabstand 8 px = Carbon
$spacing-03. Der Lauf fand 110 Fehler; alle behoben:
- Ringlegende: Container-Query statt fester Zweispaltigkeit. Legende sitzt rechts
vom Ring, wenn Platz ist, sonst darunter — Carbon: "positioned to the right of
the chart when space is plentiful". Namen brechen um statt überzulaufen.
- Auslastungs-Achse: Beschriftungsdichte staffelt sich über drei Container-Stufen,
vorher überlappten die Zahlen in schmalen Karten um bis zu 16 px.
- Handlungsbedarf und Kennzahl-Labels: break-words statt Überlauf.
- Kartentitel und -untertitel: break-words, weil Dateipfade keine Leerzeichen
zum Umbrechen haben.
Dokumentiert als DESIGN-SYSTEM.md §9 mit Begründung, warum das ein gemessener
Prüfer sein muss und keine Sichtprüfung.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
This commit is contained in:
1 parent
0875ae2688
commit
351a26efa4
10 files changed
+232
-23
No files matched your search
@@ -76,10 +76,10 @@ export function AlertList({
|
||||
</span>
|
||||
|
||||
<span className="flex min-w-0 grow flex-col">
|
||||
<span className="text-[13px] font-medium leading-snug text-text-neutral-primary">
|
||||
<span className="min-w-0 break-words text-[13px] font-medium leading-snug text-text-neutral-primary">
|
||||
{eintrag.text}
|
||||
</span>
|
||||
<span className="text-[12px] leading-snug text-text-neutral-tertiary">
|
||||
<span className="min-w-0 break-words text-[12px] leading-snug text-text-neutral-tertiary">
|
||||
{eintrag.detail}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -74,7 +74,7 @@ export function ChartCard({
|
||||
<header className="flex flex-none items-start justify-between gap-3 px-4 pb-3 pt-4">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<h2 className="truncate text-[14px] font-semibold leading-tight text-text-neutral-primary">
|
||||
<h2 className="min-w-0 break-words text-[14px] font-semibold leading-tight text-text-neutral-primary">
|
||||
{titel}
|
||||
</h2>
|
||||
{hinweis ? (
|
||||
@@ -87,7 +87,7 @@ export function ChartCard({
|
||||
) : null}
|
||||
</div>
|
||||
{untertitel ? (
|
||||
<p className="line-clamp-2 text-[12px] leading-snug text-text-neutral-tertiary">
|
||||
<p className="line-clamp-2 break-words text-[12px] leading-snug text-text-neutral-tertiary">
|
||||
{untertitel}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function StatZeile({
|
||||
>
|
||||
{eintraege.map((eintrag) => (
|
||||
<div key={eintrag.label} className="flex flex-col gap-0.5">
|
||||
<span className="text-[11px] uppercase tracking-[0.06em] text-text-neutral-tertiary">
|
||||
<span className="break-words text-[11px] uppercase tracking-[0.06em] text-text-neutral-tertiary">
|
||||
{eintrag.label}
|
||||
</span>
|
||||
<span className="text-[14px] font-semibold tabular-nums text-text-neutral-primary">
|
||||
|
||||
@@ -43,7 +43,7 @@ export function AuslastungsBalken({
|
||||
for (let t = min; t <= max; t += 10) ticks.push(t);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="@container flex flex-col gap-2">
|
||||
{sichtbar.map((zeile) => {
|
||||
const farbe =
|
||||
zeile.wert < SCHWELLEN.frei
|
||||
@@ -113,15 +113,29 @@ export function AuslastungsBalken({
|
||||
<div className="grid grid-cols-[minmax(96px,24%)_minmax(0,1fr)_48px] items-center gap-3 pt-1">
|
||||
<span />
|
||||
<span className="relative block h-4">
|
||||
{ticks.map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
className="absolute top-0 -translate-x-1/2 text-[11px] tabular-nums text-text-neutral-tertiary"
|
||||
style={{ left: `${pos(t)}%` }}
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
{/* Beschriftungsdichte staffelt sich mit der Containerbreite: ganz
|
||||
schmal nur die Enden, dann die 100-%-Marke, dann jede zweite, dann
|
||||
alle. Container-Query, weil dieselbe Karte mal ein Drittel und mal
|
||||
die volle Seitenbreite einnimmt — der Viewport weiß davon nichts. */}
|
||||
{ticks.map((t, index) => {
|
||||
const stufe =
|
||||
t === min || t === max
|
||||
? ""
|
||||
: t === 100
|
||||
? "hidden @sm:inline"
|
||||
: index % 2 === 1
|
||||
? "hidden @lg:inline"
|
||||
: "hidden @md:inline";
|
||||
return (
|
||||
<span
|
||||
key={t}
|
||||
className={`absolute top-0 -translate-x-1/2 text-[11px] tabular-nums text-text-neutral-tertiary ${stufe}`}
|
||||
style={{ left: `${pos(t)}%` }}
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
<span className="text-right text-[11px] text-text-neutral-tertiary">%</span>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,11 @@ import { DONUT, farbeFuer, gruppiereRest } from "../../utils/chartTheme";
|
||||
* - maximal 5 Segmente, Rest als "Sonstige" (gruppiereRest)
|
||||
* - größtes Segment startet oben, dann im Uhrzeigersinn absteigend
|
||||
* - Gesamtwert steht in der Mitte, sonst muss man die Segmente addieren
|
||||
* - Legende rechts mit Wert, einzeilig — nicht im Chart
|
||||
* - Legende rechts mit Wert — Carbon: „positioned to the right of the chart
|
||||
* when space is plentiful". Ist der Platz nicht reichlich, rutscht sie unter
|
||||
* den Ring, statt Namen und Werte ineinanderlaufen zu lassen. Gesteuert über
|
||||
* eine Container-Query, weil dieselbe Karte mal ein Drittel und mal die halbe
|
||||
* Seitenbreite einnimmt — der Viewport weiß davon nichts.
|
||||
* - Klick auf einen Legendeneintrag hebt das Segment hervor
|
||||
*/
|
||||
export function Donut({
|
||||
@@ -45,8 +49,9 @@ export function Donut({
|
||||
const gesamt = segmente.reduce((summe, s) => summe + s.value, 0);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[168px_minmax(0,1fr)] items-center gap-5">
|
||||
<div className="relative" style={{ height: hoehe }}>
|
||||
<div className="@container">
|
||||
<div className="flex flex-col items-center gap-4 @md:grid @md:grid-cols-[168px_minmax(0,1fr)] @md:items-center @md:gap-5">
|
||||
<div className="relative w-full max-w-[220px] @md:max-w-none" style={{ height: hoehe }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
@@ -84,7 +89,7 @@ export function Donut({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="flex flex-col gap-1.5">
|
||||
<ul className="flex w-full flex-col gap-1.5">
|
||||
{segmente.map((segment, index) => {
|
||||
const anteil = gesamt > 0 ? (segment.value / gesamt) * 100 : 0;
|
||||
return (
|
||||
@@ -99,7 +104,7 @@ export function Donut({
|
||||
className="mt-1 h-2.5 w-2.5 flex-none rounded-sm"
|
||||
style={{ background: farbeFuer(segment.name, index) }}
|
||||
/>
|
||||
<span className="min-w-0 grow text-[12px] leading-snug text-text-neutral-secondary">
|
||||
<span className="min-w-0 grow break-words text-[12px] leading-snug text-text-neutral-secondary">
|
||||
{segment.name}
|
||||
</span>
|
||||
<span className="flex-none whitespace-nowrap text-[12px] tabular-nums text-text-neutral-primary">
|
||||
@@ -113,6 +118,7 @@ export function Donut({
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user