0953880a80
Deploy App / deploy (push) Successful in 1m10s
Kapitalverwendung (Punkt C) am richtigen Ort: - Prozent-Aufteilung des bezogenen Alterskapitals wandert vom Cash-Uebergang zum Bezugs-Entscheid der PK (nur bei Kapitalbezug) bzw. der Saeule 3a -- mit zwei Guthaben liess sie sich vorher gar nicht getrennt beantworten - Dialoge fuehren neu brutto -> Steuersatz -> netto -> Verteilung - BUGFIX: Der zugeteilte Betrag erhoehte still den internen Bestand, deshalb zeigten Ziel-Element und "Kapital verteilen" eine 0. Er laeuft jetzt ueber Carry.capitalIn als Zusatzeinlage der Folgephase und ist ueberall sichtbar - Saeule 3a ist am Pensions-Uebergang neu ein offener Entscheid Phasendauer (gemeldeter Fehler): - Die Folgephase gleicht eine geaenderte Dauer aus; Gesamtdauer bleibt gleich - Vorher kappte das Tool nur die bearbeitete Phase -> Phase 2 ueberspannte danach die Pensionierung und die Invariante aus Punkt 44 kippte - Rueckfrage vorher, Blockade wenn die Folgephase unter 1 Jahr fiele - neue reine Funktion planDurationChange Bedienung: - Element-Zeile und Phasenkopf: Stift (umbenennen, beim Element inkl. Zuordnung), Papierkorb, Expand -- alle immer sichtbar - PATCH /api/elements/<id> nimmt neu auch ownerRole - Hilfetexte via Portal (wurden in scrollenden Dialogen abgeschnitten) - Verteil-Dialoge: Zuordnung je Zeile, nach vom/ins Cash gruppiert, Vorbelegung mit dem EFFEKTIVEN Wert inkl. Vererbung (zeigte vorher 0) - Matrix: gleiche Spaltenbreiten + horizontales Scrollen, "Alle auf-/ zuklappen", Kategorie-Summe in der zugeklappten Zeile - Phasen-Detailansicht nutzt die neue Aufteilungs-Grafik - Uebersicht: "Leer starten" auch im leeren Zustand Nebenbei: dritte verstuemmelte Hex-Farbe (#7c3aed) repariert, Phasen-Panel nutzt den eigenen Bestaetigungs-Dialog statt window.confirm; mehrere veraltete Referenzen und die buildCarryData-Tabelle in der Spez nachgezogen. SPEZIFIKATION 0.33. 278 -> 288 Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { Info } from "lucide-react";
|
|
|
|
// Hilfetext-Bubble.
|
|
//
|
|
// Der Text wird über ein PORTAL an den <body> gehängt und `fixed` positioniert. Vorher lag er
|
|
// als `absolute` Kind im Fluss -- in einem scrollenden Dialog (Verteil-Dialoge, Assistent)
|
|
// schnitt dessen `overflow` die Box ab, teilweise nach wenigen Zeilen. Fix positioniert
|
|
// bezieht sich auf den Viewport und entkommt damit jedem Overflow-Container.
|
|
//
|
|
// Zusätzlich: Die Box klappt nach OBEN, wenn unten kein Platz mehr ist, und wird an den
|
|
// Bildschirmrändern geklemmt, damit sie nie halb ausserhalb steht.
|
|
|
|
const WIDTH = 256; // entspricht w-64
|
|
const MARGIN = 8;
|
|
|
|
export function InfoBubble({ text }: { text: string }) {
|
|
const [open, setOpen] = useState(false);
|
|
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
|
|
const btnRef = useRef<HTMLButtonElement | null>(null);
|
|
const boxRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
// Position aus der Lage des Knopfes berechnen -- erst wenn geöffnet, und erneut, sobald die
|
|
// tatsächliche Höhe der Box bekannt ist (für das Hochklappen).
|
|
useEffect(() => {
|
|
if (!open) {
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- Position gilt nur im offenen Zustand
|
|
setPos(null);
|
|
return;
|
|
}
|
|
const place = () => {
|
|
const btn = btnRef.current;
|
|
if (!btn) return;
|
|
const r = btn.getBoundingClientRect();
|
|
const height = boxRef.current?.offsetHeight ?? 0;
|
|
const spaceBelow = window.innerHeight - r.bottom;
|
|
const above = height > 0 && spaceBelow < height + MARGIN && r.top > height + MARGIN;
|
|
const left = Math.min(
|
|
Math.max(MARGIN, r.left + r.width / 2 - WIDTH / 2),
|
|
Math.max(MARGIN, window.innerWidth - WIDTH - MARGIN)
|
|
);
|
|
setPos({ top: above ? r.top - height - 6 : r.bottom + 6, left });
|
|
};
|
|
place();
|
|
// Nach dem ersten Zeichnen erneut -- dann steht die Höhe fest.
|
|
const raf = requestAnimationFrame(place);
|
|
window.addEventListener("scroll", place, true);
|
|
window.addEventListener("resize", place);
|
|
return () => {
|
|
cancelAnimationFrame(raf);
|
|
window.removeEventListener("scroll", place, true);
|
|
window.removeEventListener("resize", place);
|
|
};
|
|
}, [open]);
|
|
|
|
return (
|
|
<span className="relative inline-flex align-middle ml-1">
|
|
<button
|
|
ref={btnRef}
|
|
type="button"
|
|
aria-label="Hilfe anzeigen"
|
|
onMouseEnter={() => setOpen(true)}
|
|
onMouseLeave={() => setOpen(false)}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setOpen((o) => !o);
|
|
}}
|
|
className="flex h-4 w-4 items-center justify-center rounded-full bg-accent-soft text-accent-soft-fg hover:opacity-80"
|
|
>
|
|
<Info className="h-2.5 w-2.5" strokeWidth={2.5} />
|
|
</button>
|
|
{open &&
|
|
typeof document !== "undefined" &&
|
|
createPortal(
|
|
<div
|
|
ref={boxRef}
|
|
role="tooltip"
|
|
style={{
|
|
position: "fixed",
|
|
top: pos?.top ?? -9999,
|
|
left: pos?.left ?? -9999,
|
|
width: WIDTH,
|
|
// Vor der ersten Messung unsichtbar, damit die Box nicht kurz aufblitzt.
|
|
visibility: pos ? "visible" : "hidden",
|
|
}}
|
|
className="pointer-events-none z-[60] rounded-lg border border-border bg-surface p-2.5 text-xs leading-snug text-fg shadow-lg"
|
|
>
|
|
{text}
|
|
</div>,
|
|
document.body
|
|
)}
|
|
</span>
|
|
);
|
|
}
|