Files
Dashboard/features/dashboard/components/cards/AlertList.tsx
T
dejanandClaude Opus 5 e209c33916 Drei Stellen im Hover- und Ausrichtungsverhalten nachbessern
Aus dem Sparring, alle drei auch in Analytics wirksam.

1. Vertriebstrichter: Zeilen-Hervorhebung statt Ausgrauen der uebrigen Balken.

   Bisher setzte das Ueberfahren einer Zeile alle anderen Balken auf 45 %
   Deckkraft. Das war aus drei Gruenden falsch: Es liess fuenf von sechs
   Balken verblassen und las sich dadurch wie eine Auswahl statt wie ein
   Ueberfahren; es war in sich widerspruechlich, weil die Balken verblassten
   und die Namen daneben schwarz blieben; und Deckkraft traegt in diesem
   Diagramm bereits Bedeutung — der Balken kodiert einen Anteil, ihn heller zu
   machen heisst dort "weniger" und nicht "gerade nicht beruehrt".

   Jetzt bekommt die beruehrte Zeile einen Untergrund, die uebrigen bleiben
   unveraendert. Derselbe Untergrund wie in der Handlungsbedarf-Liste und in
   den Tabellen. Die Spur des aktiven Balkens wird weiss: neutral-50 und
   neutral-100 liegen nur fuenf Stufen auseinander, grau auf grau haette dem
   Balken den Bezugsrahmen genommen.

2. KPI-Karten einer Reihe auf gleiche Hoehe.

   Die Umsatzkarte war hoeher als ihre drei Nachbarn, weil sie als einzige
   eine Trendzeile mit Verlaufskurve traegt. Auf dem Dashboard steht zwischen
   Rasterzelle und Karte ein Link, und der reichte die Streckung des Rasters
   nicht durch. h-full auf der Karte.

3. Handlungsbedarf: Hover-Flaeche greift ueber den Inhalt hinaus.

   Der Untergrund endete hart am Pfeil — bei zwei Spalten besonders sichtbar,
   weil daneben 32 px Spaltenabstand ungefaerbt blieben. Jetzt acht Pixel
   Ueberstand und runde Ecken.

   Dabei mitgenommen: Der Trennstrich sass unten und brauchte eine Ausnahme
   fuer "die letzte Zeile" — die es bei zwei Spalten zweimal gibt. Die rechte
   Spalte behielt dadurch einen Strich, unter dem nichts mehr kam. Oben
   liegend ist die Ausnahme die erste Zeile jeder Spalte, und die steht immer
   an derselben Stelle, unabhaengig von der Anzahl der Eintraege.

tsc, lint, build und verify:layout laufen sauber; Hover-Zustaende und
Kartenhoehen nachgemessen statt begutachtet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
2026-08-31 17:06:25 +02:00

136 lines
5.5 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 (
/**
* Der Trennstrich sitzt oben, nicht unten.
*
* Unten liegend braucht er eine Ausnahme für die letzte Zeile — und
* „die letzte Zeile" gibt es bei zwei Spalten zweimal, einmal je
* Spalte, an einer Stelle, die von der Anzahl der Einträge abhängt.
* Die rechte Spalte behielt dadurch einen Strich, unter dem nichts
* mehr kam.
*
* Oben liegend ist die Ausnahme dagegen die *erste* Zeile jeder
* Spalte, und die steht bei zeilenweisem Fluss immer an derselben
* Stelle: die ersten beiden Kinder. Das gilt unabhängig davon, wie
* viele Einträge die Liste hat.
*/
<li
key={eintrag.id}
className={`border-t border-solid border-border-default first:border-t-0 ${
spalten === 2 ? "xl:[&:nth-child(-n+2)]:border-t-0" : ""
}`}
>
<button
onClick={() =>
zeigeToast(eintrag.aktion, `${eintrag.text} — Absprung folgt in der Umsetzung`)
}
/* -mx-2 und die zusätzliche Breite: Der Untergrund lief vorher
genau bis zum Pfeil und schnitt dort hart ab — bei zwei Spalten
besonders sichtbar, weil daneben 32 px Spaltenabstand ungefärbt
blieben. Jetzt greift er acht Pixel über den Inhalt hinaus und
hat runde Ecken; er liest sich als Fläche unter der Zeile statt
als Kasten um sie herum.
Der Trennstrich sitzt dafür an der Zeile und nicht mehr am
Knopf: Er soll die Zeilen trennen und nicht mitwandern. */
className="group -mx-2 flex w-[calc(100%+1rem)] items-center gap-3 rounded-md px-2 py-3 text-left transition-colors duration-150 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>
);
}