Files
dejanandClaude Opus 5 bada5db344 Menue nicht mehr abschneiden, Zeile teilt sich nach dem Ausblenden neu auf
Zwei Fehler aus dem Sparring, beide an der Diagrammkarte.

**Das Optionsmenue war abgeschnitten.** Die Karte trug `overflow-hidden`, das
Menue klappt aus dem Kopf heraus, und der zweite Eintrag war nur zur Haelfte
zu sehen. Das Beschneiden ist entfallen; die runden Ecken brauchen es nicht,
weil Kopf und Koerper 16 px Innenabstand haben und nichts an der Kante anliegt.

**Nach dem Ausblenden blieb eine Luecke.** Die Zeilen waren ein Raster mit
festen Spalten. Blendet man eine Karte aus, gibt ChartCard `null` zurueck, die
Spalte bleibt aber stehen und die Nachbarkarte behaelt ihre zwei Drittel.

Neu ist `ChartReihe` in PageContainer.tsx: eine Flexzeile statt eines Rasters.
Die verbliebenen Karten verteilen den Platz unter sich neu, ohne dass jemand
mitzaehlen muesste, wie viele gerade sichtbar sind. Nachgemessen auf
/analytics: Der Zahlungsverlauf geht von 574 px auf 1164 px, sobald die
Nachbarkachel verschwindet.

Zehn Zeilen auf allen fuenf Analyse-Seiten umgestellt. `gleichHoch` haelt die
Unterscheidung aus dem letzten Sparring fest: gestreckt nur, wo die Karten
dieselbe Bauart haben.

Beim Umbau eine Regression gebaut und durch Nachmessen gefunden: Aus 885/443
wurde 664/664. `[&>*]:flex-1` erzeugt den Selektor `.zeile > *`, und der ist
spezifischer als eine Klasse am Kind, also verlor `xl:flex-[2]`. Jetzt
`xl:flex-[2]!`, mit dem Grund als Kommentar an der Komponente.

Nachgemessen nach der Aenderung, alle Zeilen: 885/443 wo zwei zu eins gewollt
ist, 389/389 und 409/409 und 595/595 wo gleiche Hoehe gewollt ist.

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

156 lines
5.7 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 (
/* Kein `overflow-hidden`: Das Optionsmenü klappt aus dem Kartenkopf heraus
und wurde an der Kartenkante abgeschnitten. Ein Menü, von dem man den
zweiten Eintrag nur zur Hälfte sieht, ist unbenutzbar.
Die Kante braucht das Beschneiden auch nicht: Kopf und Körper haben
16 px Innenabstand, es liegt nichts an den runden Ecken an. */
<section
className={`flex flex-col 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>
);
}