Files
Dashboard/features/dashboard/components/cards/AlertList.tsx
T
dejanandClaude Opus 5 351a26efa4 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
2026-08-29 19:32:55 +02:00

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>
);
}