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
108 lines
4.0 KiB
TypeScript
108 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import {
|
|
FeatherAlertTriangle,
|
|
FeatherArrowRight,
|
|
FeatherCheckCircle,
|
|
FeatherInfo,
|
|
} from "@subframe/core";
|
|
import { STATUS_FARBEN } from "../../utils/chartTheme";
|
|
import { formatEuro } from "../../utils/formatters";
|
|
import { useToast } from "../../context/ToastContext";
|
|
import type { AlertEintrag } from "../../data/calculations";
|
|
|
|
/**
|
|
* Handlungsbedarf-Liste (COMP-08).
|
|
*
|
|
* Jede Zeile nennt drei Dinge: was los ist, um wie viel Geld es geht und was
|
|
* zu tun ist. Ohne den Betrag lassen sich die Zeilen nicht gegeneinander
|
|
* abwägen, ohne die Handlung ist die Liste ein Bericht statt einer Arbeitsliste.
|
|
* Der Absprung in die jeweilige Arbeitsliste ist im Prototyp eine Attrappe.
|
|
*/
|
|
export function AlertList({
|
|
eintraege,
|
|
leerText = "Keine offenen Punkte",
|
|
/** Zwei Spalten, wenn die Karte über die volle Seitenbreite läuft. */
|
|
spalten = 1,
|
|
}: {
|
|
eintraege: AlertEintrag[];
|
|
leerText?: string;
|
|
spalten?: 1 | 2;
|
|
}) {
|
|
const { zeigeToast } = useToast();
|
|
const rang = { kritisch: 0, warnung: 1, hinweis: 2 } as const;
|
|
const sortiert = [...eintraege].sort((a, b) => rang[a.art] - rang[b.art]);
|
|
|
|
if (!sortiert.length) {
|
|
return (
|
|
<div className="flex h-full min-h-[180px] flex-col items-center justify-center gap-2 text-center">
|
|
<FeatherCheckCircle className="text-[22px]" style={{ color: STATUS_FARBEN.positiv }} />
|
|
<span className="text-[13px] text-text-neutral-tertiary">{leerText}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
// Container-Query statt Viewport: dieselbe Liste steht mal über die volle
|
|
// Seitenbreite und mal in einer Drittelkarte. Der Viewport weiß davon nichts.
|
|
<ul
|
|
className={`@container ${
|
|
spalten === 2 ? "grid grid-cols-1 gap-x-8 xl:grid-cols-2" : "flex flex-col"
|
|
}`}
|
|
>
|
|
{sortiert.map((eintrag) => {
|
|
const farbe =
|
|
eintrag.art === "kritisch"
|
|
? STATUS_FARBEN.negativ
|
|
: eintrag.art === "warnung"
|
|
? STATUS_FARBEN.warnung
|
|
: STATUS_FARBEN.info;
|
|
|
|
return (
|
|
<li key={eintrag.id}>
|
|
<button
|
|
onClick={() =>
|
|
zeigeToast(eintrag.aktion, `${eintrag.text} — Absprung folgt in der Umsetzung`)
|
|
}
|
|
className="group flex w-full items-center gap-3 border-b border-solid border-border-default py-3 text-left last:border-b-0 hover:bg-neutral-50"
|
|
>
|
|
<span className="flex-none" style={{ color: farbe }}>
|
|
{eintrag.art === "hinweis" ? (
|
|
<FeatherInfo className="text-[16px]" />
|
|
) : (
|
|
<FeatherAlertTriangle className="text-[16px]" />
|
|
)}
|
|
</span>
|
|
|
|
<span className="flex min-w-0 grow flex-col">
|
|
<span className="min-w-0 break-words text-[13px] font-medium leading-snug text-text-neutral-primary">
|
|
{eintrag.text}
|
|
</span>
|
|
<span className="min-w-0 break-words text-[12px] leading-snug text-text-neutral-tertiary">
|
|
{eintrag.detail}
|
|
</span>
|
|
</span>
|
|
|
|
{eintrag.betrag !== undefined ? (
|
|
<span className="flex-none whitespace-nowrap text-[14px] font-semibold tabular-nums text-text-neutral-primary">
|
|
{formatEuro(eintrag.betrag)}
|
|
</span>
|
|
) : null}
|
|
|
|
{/* Schmale Karte: nur der Pfeil, die Handlung steht im Tooltip. */}
|
|
<span
|
|
title={eintrag.aktion}
|
|
className="flex flex-none items-center justify-end gap-1 whitespace-nowrap text-[12px] font-medium text-text-neutral-tertiary group-hover:text-text-neutral-primary @md:w-[150px]"
|
|
>
|
|
<span className="hidden @md:inline">{eintrag.aktion}</span>
|
|
<FeatherArrowRight className="text-[13px]" />
|
|
</span>
|
|
</button>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
);
|
|
}
|