Aus dem Sparring: Bei duenner Datenlage riss die oben buendige Ausrichtung Loecher in die Seite. Nachgemessen auf /analytics mit Zeitraum "heute" und Gewerk "Heizung": "Umsatz nach Gewerk" 112 px neben "Zahlungseingang im Zeitverlauf" 427 px — 315 px Seitenhintergrund dazwischen. Das liest sich nicht als "wenig Daten", sondern als "etwas ist kaputt". Zwei Aenderungen, beide klein: - Der Kartenkoerper zentriert seinen Inhalt senkrecht. Der Ueberschuss geht je zur Haelfte nach oben und unten, statt unten abgeladen zu werden. Eine Stelle in ChartCard und eine in WidgetKachel — nicht in fuenf Diagrammen. - items-start faellt auf den Zeilen, die ausschliesslich Diagrammkarten tragen. Die Zeile mit den beiden Tabellen auf der Kundenseite bleibt oben buendig: Eine Tabelle hat ein natuerliches Ende und eine eigene Blaetterleiste, Strecken schiebt die nur nach unten. Die Karten bleiben dynamisch — keine feste Hoehe, mehr Inhalt laesst die Karte und mit ihr die Zeile weiter wachsen. Die Leere verschwindet nicht, sie zieht aus dem Seitenhintergrund in die Karte. Das ist der Unterschied zwischen einem Raster mit Luecken und einer Karte, in der gerade wenig steht. Nachgemessen nach der Aenderung: 427/427, 389/389 auf /analytics, 372/372 und 423/423 auf /dashboard. Dabei ist nebenbei die Leerflaeche unter dem Vertriebstrichter verschwunden, die vorher auffiel. In DESIGN-SYSTEM.md als Regel festgehalten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
150 lines
5.4 KiB
TypeScript
150 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import {
|
|
FeatherDownload,
|
|
FeatherEyeOff,
|
|
FeatherInfo,
|
|
FeatherMoreHorizontal,
|
|
} from "@subframe/core";
|
|
import { useWidgets } from "../../context/WidgetContext";
|
|
import { useToast } from "../../context/ToastContext";
|
|
|
|
/**
|
|
* Rahmen für jede Visualisierung (COMP-09).
|
|
*
|
|
* Enthält die einzige "echte" Interaktionslogik des Prototyps:
|
|
* Über das ⋯-Menü lässt sich die Karte ausblenden; sie taucht dann in der
|
|
* Filterleiste zum Wiedereinblenden auf. Der Zustand ist persistiert.
|
|
*/
|
|
export function ChartCard({
|
|
id,
|
|
titel,
|
|
untertitel,
|
|
hinweis,
|
|
aktion,
|
|
legende,
|
|
children,
|
|
className = "",
|
|
}: {
|
|
/** Stabile ID — Schlüssel für Ausblenden/Einblenden. */
|
|
id: string;
|
|
titel: string;
|
|
untertitel?: string;
|
|
/** Erklärt die Kennzahl; erscheint als Tooltip am (i). */
|
|
hinweis?: string;
|
|
aktion?: React.ReactNode;
|
|
legende?: React.ReactNode;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}) {
|
|
const { istSichtbar, verstecke } = useWidgets();
|
|
const { zeigeToast } = useToast();
|
|
const [menuOffen, setMenuOffen] = React.useState(false);
|
|
const menu = React.useRef<HTMLDivElement>(null);
|
|
const ausloeser = React.useRef<HTMLButtonElement>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (!menuOffen) return;
|
|
const schliesse = (event: MouseEvent) => {
|
|
if (!menu.current?.contains(event.target as Node)) setMenuOffen(false);
|
|
};
|
|
// Escape schliesst und gibt den Fokus an den Ausloeser zurueck — sonst
|
|
// landet die Tastaturbedienung nach dem Schliessen am Seitenanfang.
|
|
const taste = (event: KeyboardEvent) => {
|
|
if (event.key === "Escape") {
|
|
setMenuOffen(false);
|
|
ausloeser.current?.focus();
|
|
}
|
|
};
|
|
document.addEventListener("mousedown", schliesse);
|
|
document.addEventListener("keydown", taste);
|
|
return () => {
|
|
document.removeEventListener("mousedown", schliesse);
|
|
document.removeEventListener("keydown", taste);
|
|
};
|
|
}, [menuOffen]);
|
|
|
|
if (!istSichtbar(id)) return null;
|
|
|
|
return (
|
|
<section
|
|
className={`flex flex-col overflow-hidden rounded-lg border border-solid border-border-default bg-white shadow-sm ${className}`}
|
|
>
|
|
<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="min-w-0 break-words text-[14px] font-semibold leading-tight text-text-neutral-primary">
|
|
{titel}
|
|
</h2>
|
|
{hinweis ? (
|
|
<span
|
|
title={hinweis}
|
|
className="flex h-4 w-4 flex-none cursor-help items-center justify-center text-text-neutral-subtle hover:text-text-neutral-secondary"
|
|
>
|
|
<FeatherInfo className="text-[13px]" />
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
{untertitel ? (
|
|
<p className="line-clamp-2 break-words text-[12px] leading-snug text-text-neutral-tertiary">
|
|
{untertitel}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="dv-no-print relative flex flex-none items-center gap-1" ref={menu}>
|
|
{aktion}
|
|
<button
|
|
ref={ausloeser}
|
|
onClick={() => setMenuOffen((wert) => !wert)}
|
|
aria-label={`Optionen für ${titel}`}
|
|
aria-haspopup="menu"
|
|
aria-expanded={menuOffen}
|
|
className="flex h-8 w-8 items-center justify-center rounded-md text-text-neutral-tertiary hover:bg-neutral-100 hover:text-text-neutral-primary"
|
|
>
|
|
<FeatherMoreHorizontal className="text-[16px]" />
|
|
</button>
|
|
|
|
{menuOffen ? (
|
|
<div
|
|
role="menu"
|
|
className="absolute right-0 top-9 z-40 flex w-52 flex-col rounded-lg border border-solid border-border-default bg-white py-1 shadow-lg"
|
|
>
|
|
<button
|
|
onClick={() => {
|
|
verstecke(id, titel);
|
|
setMenuOffen(false);
|
|
}}
|
|
className="flex items-center gap-2 px-3 py-2 text-left text-[13px] text-text-neutral-primary hover:bg-neutral-50"
|
|
>
|
|
<FeatherEyeOff className="text-[14px] text-text-neutral-tertiary" />
|
|
Widget ausblenden
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
zeigeToast("Als Bild vorgemerkt", `${titel} — Export folgt in der Umsetzung`);
|
|
setMenuOffen(false);
|
|
}}
|
|
className="flex items-center gap-2 px-3 py-2 text-left text-[13px] text-text-neutral-primary hover:bg-neutral-50"
|
|
>
|
|
<FeatherDownload className="text-[14px] text-text-neutral-tertiary" />
|
|
Als Bild exportieren
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</header>
|
|
|
|
{legende ? <div className="flex-none px-4 pb-2">{legende}</div> : null}
|
|
|
|
{/* justify-center: Karten einer Zeile sind gleich hoch, aber nicht gleich
|
|
voll. Eine Karte mit einem Balken neben einer mit einem Diagramm über
|
|
400 px bekommt den Überschuss oben und unten je zur Hälfte, statt ihn
|
|
unten abzuladen. Der Inhalt behält seine natürliche Höhe — die Karte
|
|
wächst weiter mit, wenn mehr hineinkommt. */}
|
|
<div className="flex min-h-0 grow flex-col justify-center px-4 pb-4">{children}</div>
|
|
</section>
|
|
);
|
|
}
|