Matrix-Steuerung eingebettet, Tour als Demo-Popup

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 22:28:56 +02:00
parent 79fb2b9ca6
commit c440063a93
2 changed files with 370 additions and 242 deletions
+84 -86
View File
@@ -432,55 +432,6 @@ export function PlanView({
</div> </div>
</div> </div>
{/* Anzeige-Umschalter (nominal/real, Plan/Ist) -- direkt über der Matrix, weil er nur
deren Zahlen steuert. */}
{hasPhases && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted">Anzeige</span>
<div className="inline-flex rounded-lg border border-border bg-surface p-0.5 text-xs">
{(["nominal", "both", "real"] as ValueMode[]).map((m) => (
<button
key={m}
type="button"
onClick={() => changeValueMode(m)}
className={`rounded-md px-2.5 py-1 font-medium ${
valueMode === m ? "bg-accent text-accent-fg" : "text-muted hover:bg-surface-2"
}`}
>
{m === "nominal" ? "Nominal" : m === "both" ? "Beide" : "Real"}
</button>
))}
</div>
<span className="text-[11px] text-faint">real = kaufkraftbereinigt (Planbeginn)</span>
{/* Zweite Achse: Datenquelle. Erscheint nur, wenn es überhaupt Ist-Werte gibt --
sonst wäre es ein Umschalter ohne Gegenstück. */}
{hasActuals && (
<>
<span className="ml-2 text-xs text-muted">Zahlen</span>
<div className="inline-flex rounded-lg border border-border bg-surface p-0.5 text-xs">
{DATA_SOURCE_OPTIONS.map((o) => (
<button
key={o.value}
type="button"
onClick={() => setDataSource(o.value)}
title={o.label}
className={`rounded-md px-2.5 py-1 font-medium ${
dataSource === o.value ? "bg-accent text-accent-fg" : "text-muted hover:bg-surface-2"
}`}
>
{o.short}
</button>
))}
</div>
{dataSource === "ACTUAL" && (
<span className="text-[11px] text-faint">Abweichung gegenüber Plan farbig</span>
)}
</>
)}
</div>
)}
{!hasPhases && ( {!hasPhases && (
<EmptyState <EmptyState
icon={<Plus className="h-6 w-6" />} icon={<Plus className="h-6 w-6" />}
@@ -493,41 +444,6 @@ export function PlanView({
</EmptyState> </EmptyState>
)} )}
{hasPhases && (
<div className="flex flex-wrap items-center gap-2">
<Button size="sm" className="px-3 py-1.5 text-sm" onClick={() => setShowAdd(true)}>
<Plus className="h-4 w-4" /> Finanzielles Element
</Button>
<button
type="button"
onClick={() => setShowAddPhase(true)}
className="flex items-center gap-1.5 rounded-lg border border-dashed border-accent bg-accent-soft px-3 py-1.5 text-sm font-medium text-accent-soft-fg transition-colors hover:bg-accent-soft"
>
<Plus className="h-4 w-4" /> Lebensphase
</button>
{/* Alle Kategoriegruppen auf einen Schlag auf- oder zuklappen. */}
<button
type="button"
onClick={() =>
setCollapsedCats((prev) =>
prev.size === 0 ? new Set(CATEGORY_ORDER.filter((c) => (elementsByCategory.get(c)?.length ?? 0) > 0)) : new Set()
)
}
className="ml-auto flex items-center gap-1.5 rounded-lg border border-border px-2.5 py-1.5 text-xs font-medium text-muted transition-colors hover:bg-surface-2 hover:text-fg"
>
{collapsedCats.size === 0 ? (
<>
<ChevronRight className="h-3.5 w-3.5" /> Alle zuklappen
</>
) : (
<>
<ChevronDown className="h-3.5 w-3.5" /> Alle aufklappen
</>
)}
</button>
</div>
)}
{computed.ruinAge !== null && ( {computed.ruinAge !== null && (
<div className="flex flex-wrap items-center gap-2 rounded-xl border border-danger bg-danger-soft px-4 py-2 text-sm font-medium text-danger"> <div className="flex flex-wrap items-center gap-2 rounded-xl border border-danger bg-danger-soft px-4 py-2 text-sm font-medium text-danger">
<AlertCircle className="h-4 w-4 shrink-0" /> <AlertCircle className="h-4 w-4 shrink-0" />
@@ -556,8 +472,90 @@ export function PlanView({
<table className="border-collapse text-sm"> <table className="border-collapse text-sm">
<thead> <thead>
<tr> <tr>
<th className="sticky left-0 top-0 z-40 w-56 min-w-56 border-b border-r border-border bg-surface-2 px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-faint"> {/* Die Ecke oben links trägt seit 0.36 alles, was die Matrix STEUERT: was man
Finanzielle Elemente hinzufügt und wie die Zahlen dargestellt werden. Vorher lagen diese Knöpfe
lose darüber und wirkten wie Aktionen der ganzen Seite. */}
<th className="sticky left-0 top-0 z-40 w-56 min-w-56 border-b border-r border-border bg-surface-2 px-2 py-2 text-left align-top">
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-1">
<span className="text-xs font-semibold uppercase tracking-wide text-faint">Matrix</span>
<button
type="button"
onClick={() =>
setCollapsedCats((prev) =>
prev.size === 0
? new Set(CATEGORY_ORDER.filter((c) => (elementsByCategory.get(c)?.length ?? 0) > 0))
: new Set()
)
}
className="ml-auto rounded p-0.5 text-faint hover:bg-surface hover:text-fg"
title={collapsedCats.size === 0 ? "Alle zuklappen" : "Alle aufklappen"}
>
{collapsedCats.size === 0 ? (
<ChevronRight className="h-3.5 w-3.5" />
) : (
<ChevronDown className="h-3.5 w-3.5" />
)}
</button>
</div>
<div className="flex gap-1">
<button
type="button"
onClick={() => setShowAdd(true)}
className="flex flex-1 items-center justify-center gap-1 rounded-md border border-dashed border-border px-1.5 py-1 text-[10px] font-medium normal-case text-muted hover:border-accent hover:text-accent-soft-fg"
>
<Plus className="h-3 w-3" /> Element
</button>
<button
type="button"
onClick={() => setShowAddPhase(true)}
className="flex flex-1 items-center justify-center gap-1 rounded-md border border-dashed border-border px-1.5 py-1 text-[10px] font-medium normal-case text-muted hover:border-accent hover:text-accent-soft-fg"
>
<Plus className="h-3 w-3" /> Phase
</button>
</div>
<div className="flex rounded-md border border-border bg-surface p-0.5">
{(["nominal", "both", "real"] as ValueMode[]).map((m) => (
<button
key={m}
type="button"
onClick={() => changeValueMode(m)}
title={
m === "real"
? "Kaufkraftbereinigt auf den Planbeginn"
: m === "both"
? "Nominal und real"
: "Nominale Beträge"
}
className={`flex-1 rounded px-1 py-0.5 text-[10px] font-medium normal-case ${
valueMode === m ? "bg-accent text-accent-fg" : "text-muted hover:bg-surface-2"
}`}
>
{m === "nominal" ? "Nom." : m === "both" ? "Beide" : "Real"}
</button>
))}
</div>
{hasActuals && (
<div className="flex rounded-md border border-border bg-surface p-0.5">
{DATA_SOURCE_OPTIONS.map((o) => (
<button
key={o.value}
type="button"
onClick={() => setDataSource(o.value)}
title={o.label}
className={`flex-1 rounded px-1 py-0.5 text-[10px] font-medium normal-case ${
dataSource === o.value ? "bg-accent text-accent-fg" : "text-muted hover:bg-surface-2"
}`}
>
{o.short}
</button>
))}
</div>
)}
</div>
</th> </th>
{columns.map((col) => {columns.map((col) =>
col.kind === "phase" ? ( col.kind === "phase" ? (
+286 -156
View File
@@ -1,211 +1,341 @@
"use client"; "use client";
// Interaktive Kurz-Tour über die Planansicht (Teil von Roadmap Nr. 10). // Die Tour: eine kurze Demo statt eines Spotlights.
// //
// Spotlight ohne Engine-Bibliothek: Der abgedunkelte Bereich entsteht aus VIER fixed-Flächen // Bis 0.35 legte sich die Tour über den ECHTEN Bildschirm und hob Bereiche hervor. Das hatte
// um die Bounding-Box des Ziels herum (oben/unten/links/rechts). Das lässt ein «Loch» am Ziel // zwei Nachteile, die sich nicht beheben liessen: Auf einem frisch angelegten, leeren Plan gab
// frei und funktioniert -- anders als ein box-shadow-Trick -- unabhängig von z-index (sticky // es kaum etwas hervorzuheben -- gerade dann, wenn die Tour am nötigsten ist. Und das
// Matrix-Köpfe) und overflow (Matrix-Scrollbereich). Ziele, die es im aktuellen Plan nicht // Ausschneiden der Hervorhebung kämpfte dauernd mit Scroll-Containern und Z-Ebenen.
// gibt oder die unsichtbar sind, werden übersprungen. //
// Neu zeigt die Tour einen NACHGEBAUTEN Bildschirm mit erfundenen Zahlen. Er ist bewusst
// keine echte Ansicht, sondern eine Illustration: gerade so detailliert, dass man den Aufbau
// erkennt, und so einfach, dass sie sich in einem Popup erklären lässt. Der Preis ist, dass
// die Attrappe bei UI-Änderungen nachgeführt werden muss -- dafür funktioniert die Tour ab
// der ersten Sekunde und unabhängig davon, was im Plan schon steht.
import { useEffect, useMemo, useState } from "react"; import { useState } from "react";
import { Lightbulb, X } from "lucide-react"; import { ArrowLeft, ArrowRight, Sparkles, X } from "lucide-react";
import { Button } from "@/components/ui"; import { Button } from "@/components/ui";
export const TOUR_DONE_KEY = "fpt-tour-done"; export const TOUR_DONE_KEY = "fpt-tour-done";
type Focus = "none" | "settings" | "figures" | "actions" | "assistant" | "timeline" | "matrix" | "sidebar";
interface TourStep { interface TourStep {
target: string; // data-tour="…"
title: string; title: string;
text: string; text: string;
focus: Focus;
} }
const STEPS: TourStep[] = [ const STEPS: TourStep[] = [
{ {
target: "profil", title: "Willkommen bei FPT",
title: "Dein Grundprofil", text: "In zwei Minuten siehst du, wie das Tool aufgebaut ist. Alles, was hier steht, sind erfundene Beispielzahlen dein eigener Plan ist noch leer.",
text: "Alter, Pensionsalter und Inflationsannahme die Basis dieses Szenarios. Über «Bearbeiten» änderst du sie jederzeit.", focus: "none",
},
{
title: "Oben stellst du ein, unten siehst du das Ergebnis",
text: "Der Bildschirm hat zwei Hälften. Oben die Steuerung: Grundeinstellungen, Kennzahlen, Schnellaktionen und der Assistent. Unten die Matrix mit allen Zahlen.",
focus: "none",
},
{
title: "Die Grundeinstellungen",
text: "Wer plant, ab wann und wie lange. Alter, Startjahr und Inflation gelten für den ganzen Plan; Planungshorizont und Pensionsalter gehören zum Szenario. Je Person führt ein Knopf in die Pensionsplanung.",
focus: "settings",
},
{
title: "Die Kennzahlen",
text: "Was unter dem Strich herauskommt: dein Vermögen heute, das Endvermögen nominal und real und ob das Kapital über die ganze Planung reicht.",
focus: "figures",
},
{
title: "Der FPT-Assistent",
text: "Dein Wegweiser. Sieben Schritte führen von der Bestandsaufnahme bis zum fertigen Plan. Jeder Schritt erklärt zuerst, worum es geht, und bietet danach das passende Werkzeug an. Wann ein Schritt erledigt ist, entscheidest du.",
focus: "assistant",
}, },
{ {
target: "timeline",
title: "Die Zeitachse", title: "Die Zeitachse",
text: "Deine Lebensphasen im Zeitverlauf: Die Breite zeigt die Dauer, die Fähnchen die Pensionierungen und falls es dazu kommt, den Punkt, an dem das Kapital ausgeht. Die ganze Planung denkt in diesen Phasen.", text: "Deine Planung im Überblick: Lebensphasen, die Zeitpunkte der Pensionierung und, falls das Geld nicht reicht, das Jahr, in dem es knapp wird.",
focus: "timeline",
}, },
{ {
target: "endwealth", title: "Die Matrix",
title: "Dein Endvermögen", text: "Das Herz des Tools. Jede Zeile ist ein finanzielles Element, jede Spalte eine Lebensphase. Die schmalen Spalten dazwischen sind die Übergänge dort entscheidest du, was beim Wechsel geschieht.",
text: "Das Ergebnis auf einen Blick: das Vermögen am Ende der Planung nominal und real (in heutiger Kaufkraft). Reicht das Kapital nicht bis zum Schluss, steht hier stattdessen das Alter, in dem es ausgeht.", focus: "matrix",
}, },
{ {
target: "matrix", title: "Pläne und Szenarien",
title: "Die Matrix das Herzstück", text: "Links verwaltest du alles. Ein Szenario ist eine Variante deines Plans etwa mit anderem Pensionsalter. Du kannst beliebig viele anlegen und vergleichen.",
text: "Zeilen sind deine Finanzen, Spalten deine Lebensphasen. Klick auf eine Zelle: rechts öffnet sich das Bearbeiten-Panel, die Matrix bleibt sichtbar.", focus: "sidebar",
}, },
{ {
target: "transition", title: "Jetzt bist du dran",
title: "Übergänge", text: "Starte mit dem FPT-Assistenten oben rechts. Schritt 1 ist die Bestandsaufnahme: alles, was du heute besitzt. Von dort führt dich der Assistent weiter.",
text: "Zwischen zwei Phasen triffst du einmalige Entscheide verkaufen, beziehen, verrenten. Orange heisst: noch offen. Klick auf den Spaltenkopf führt dich durch alle Entscheide.", focus: "assistant",
},
{
target: "cash",
title: "Das Cash-Konto",
text: "Dein Ausgleichskonto: Was übrig bleibt, landet hier was fehlt, macht es rot. Negativ = Liquiditätslücke.",
},
{
target: "toolbar",
title: "Aktionen zum Szenario",
text: "Hier oben: die Änderungshistorie ansehen, ein neues Szenario aus diesem ableiten, die Rechenwege einsehen, die Matrix als CSV exportieren und diese Tour jederzeit neu starten.",
},
{
target: "menu",
title: "Analysen & Berichte",
text: "Links im Menü öffnest du die grossen Werkzeuge: unter «Analysen» die Grafiken, die Live-Simulation, Monte-Carlo und die Einflussfaktoren; unter «Berichte» erzeugst du ein PDF. «Effektive Werte» hält deine echten Zahlen gegen den Plan.",
}, },
]; ];
interface Box {
top: number;
left: number;
width: number;
height: number;
}
export function Tour({ onClose }: { onClose: () => void }) { export function Tour({ onClose }: { onClose: () => void }) {
// Nur Schritte, deren Ziel im DOM existiert UND sichtbar ist (z. B. die Menü-Leiste ist auf
// schmalen Screens ausgeblendet -- getClientRects() ist dann leer).
const steps = useMemo(
() =>
STEPS.filter((s) => {
const el = document.querySelector(`[data-tour="${s.target}"]`);
return el !== null && el.getClientRects().length > 0;
}),
[]
);
const [index, setIndex] = useState(0); const [index, setIndex] = useState(0);
const [rect, setRect] = useState<Box | null>(null); const step = STEPS[index];
const step = steps[index]; const last = index === STEPS.length - 1;
useEffect(() => {
if (!step) return;
const el = document.querySelector(`[data-tour="${step.target}"]`);
if (!el) return;
el.scrollIntoView({ block: "center", behavior: "smooth" });
// Die Bounding-Box jedes Frame nachführen (folgt dem sanften Scrollen und Resize), aber
// den State nur bei tatsächlicher Änderung neu setzen.
let raf = 0;
const update = () => {
const r = el.getBoundingClientRect();
setRect((prev) =>
prev && prev.top === r.top && prev.left === r.left && prev.width === r.width && prev.height === r.height
? prev
: { top: r.top, left: r.left, width: r.width, height: r.height }
);
raf = requestAnimationFrame(update);
};
update();
return () => cancelAnimationFrame(raf);
}, [step]);
if (!step || !rect) return null;
const pad = 6;
const hole: Box = {
top: Math.max(0, rect.top - pad),
left: Math.max(0, rect.left - pad),
width: rect.width + pad * 2,
height: rect.height + pad * 2,
};
const cardAtBottom = rect.top + rect.height / 2 < window.innerHeight * 0.5;
function finish() { function finish() {
try { try {
localStorage.setItem(TOUR_DONE_KEY, "1"); localStorage.setItem(TOUR_DONE_KEY, "1");
} catch { } catch {
/* localStorage nicht verfügbar -> Tour erscheint beim nächsten Mal erneut */ // Privater Modus o. Ä. -- die Tour lässt sich dann eben erneut öffnen.
} }
onClose(); onClose();
} }
// Vier Abdunkel-Flächen um das Loch. pointer-events-none, damit nichts blockiert wird.
const dim = "fixed z-[45] bg-black/60 pointer-events-none";
const belowTop = hole.top + hole.height;
const rightOfLeft = hole.left + hole.width;
return ( return (
<> <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 p-3 sm:p-6">
<div className={dim} style={{ top: 0, left: 0, width: "100vw", height: hole.top }} /> <div className="flex h-full max-h-[900px] w-full max-w-6xl flex-col overflow-hidden rounded-2xl border border-border bg-surface shadow-2xl">
<div className={dim} style={{ top: belowTop, left: 0, width: "100vw", height: `calc(100vh - ${belowTop}px)` }} /> <header className="flex items-center gap-3 border-b border-border px-4 py-3">
<div className={dim} style={{ top: hole.top, left: 0, width: hole.left, height: hole.height }} /> <Sparkles className="h-4 w-4 shrink-0 text-accent" />
<div <div className="min-w-0 flex-1">
className={dim} <h2 className="truncate text-sm font-semibold text-fg">{step.title}</h2>
style={{ top: hole.top, left: rightOfLeft, width: `calc(100vw - ${rightOfLeft}px)`, height: hole.height }} <p className="text-[11px] text-muted">
/> Schritt {index + 1} von {STEPS.length}
</p>
{/* Pulsierender Rahmen ums Ziel. */}
<div
className="tour-ring pointer-events-none fixed z-[46] rounded-xl"
style={{ top: hole.top, left: hole.left, width: hole.width, height: hole.height }}
/>
{/* Karte -- springt auf die dem Ziel gegenüberliegende Bildschirmhälfte. */}
<div
key={index}
className={`ui-pop fixed left-1/2 z-[50] w-[32rem] max-w-[calc(100vw-2rem)] -translate-x-1/2 rounded-2xl border-2 border-accent bg-surface p-5 shadow-2xl ${
cardAtBottom ? "bottom-8" : "top-8"
}`}
>
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2.5">
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-accent text-accent-fg shadow-sm">
<Lightbulb className="h-5 w-5" />
</span>
<div>
<div className="text-base font-semibold text-fg">{step.title}</div>
<div className="text-[11px] font-medium uppercase tracking-wide text-accent">
Schritt {index + 1} von {steps.length}
</div>
</div>
</div> </div>
<button <button
type="button" type="button"
onClick={finish} onClick={finish}
aria-label="Tour beenden" aria-label="Tour schliessen"
className="rounded-md p-1 text-faint hover:bg-surface-2 hover:text-fg" className="rounded-lg p-1.5 text-faint hover:bg-surface-2 hover:text-fg"
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</button> </button>
</header>
{/* Die Attrappe. Rein illustrativ -- nichts darin ist klickbar. */}
<div className="min-h-0 flex-1 overflow-auto bg-surface-2/40 p-4">
<MockScreen focus={step.focus} />
</div> </div>
<p className="mt-3 text-sm leading-relaxed text-muted">{step.text}</p>
<div className="mt-4 flex items-center justify-between gap-2"> <footer className="border-t border-border px-4 py-3">
<button type="button" onClick={finish} className="text-xs font-medium text-faint hover:text-muted"> <p className="mb-3 text-sm leading-relaxed text-fg">{step.text}</p>
Überspringen <div className="flex items-center gap-2">
</button> <div className="flex flex-1 gap-1">
<div className="flex items-center gap-3"> {STEPS.map((_, i) => (
<div className="flex gap-1"> <div
{steps.map((_, i) => ( key={i}
<span key={i} className={`h-1.5 w-1.5 rounded-full ${i === index ? "bg-accent" : "bg-border-strong"}`} /> className={`h-1 flex-1 rounded-full transition-colors ${i <= index ? "bg-accent" : "bg-surface-2"}`}
/>
))} ))}
</div> </div>
<div className="flex gap-2"> <Button variant="ghost" size="sm" disabled={index === 0} onClick={() => setIndex((i) => i - 1)}>
{index > 0 && ( <ArrowLeft className="h-4 w-4" /> Zurück
<Button variant="secondary" size="sm" onClick={() => setIndex(index - 1)}> </Button>
Zurück {last ? (
</Button> <Button size="sm" onClick={finish}>
)} <Sparkles className="h-4 w-4" /> Mit dem Assistenten starten
{index < steps.length - 1 ? ( </Button>
<Button size="sm" onClick={() => setIndex(index + 1)}> ) : (
Weiter <Button size="sm" onClick={() => setIndex((i) => i + 1)}>
</Button> Weiter <ArrowRight className="h-4 w-4" />
) : ( </Button>
<Button size="sm" onClick={finish}> )}
Fertig </div>
</Button> </footer>
)} </div>
</div>
);
}
// --- Die Attrappe ---------------------------------------------------------------------------
//
// Feste, erfundene Zahlen. Sie müssen weder stimmen noch zueinander passen -- ihr einziger
// Zweck ist, den Aufbau greifbar zu machen.
function MockScreen({ focus }: { focus: Focus }) {
const dim = (area: Focus) => (focus === "none" || focus === area ? "opacity-100" : "opacity-20");
const ring = (area: Focus) => (focus === area ? "ring-2 ring-accent" : "");
return (
<div className="flex gap-3 text-[10px]">
{/* Seitenleiste */}
<div
className={`hidden w-36 shrink-0 flex-col gap-1 rounded-lg border border-border bg-surface p-2 transition-opacity sm:flex ${dim("sidebar")} ${ring("sidebar")}`}
>
<div className="mb-1 font-semibold uppercase tracking-wide text-faint">Meine Pläne</div>
<div className="rounded bg-accent-soft px-1.5 py-1 font-medium text-accent-soft-fg">Familie Muster</div>
<div className="pl-2 text-muted">Szenarien</div>
<div className="rounded bg-surface-2 px-1.5 py-0.5 pl-3 text-fg">Basis</div>
<div className="px-1.5 py-0.5 pl-3 text-muted">Pension mit 62</div>
<div className="pl-2 text-muted">Effektive Werte</div>
<div className="pl-2 text-muted">Analysen</div>
<div className="pl-2 text-muted">Berichte</div>
</div>
<div className="flex min-w-0 flex-1 flex-col gap-3">
{/* Oberer Bereich */}
<div className="rounded-lg border border-border bg-surface-2/60 p-2">
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
<MockTile title="Grundeinstellungen" cls={`${dim("settings")} ${ring("settings")}`}>
<div>Anna · 42 J.</div>
<div>Beat · 45 J.</div>
<div className="text-faint">Start 2026 · Inflation 1,5 %</div>
<div className="mt-1 border-t border-border pt-1">Horizont 40 Jahre · bis 2065</div>
<div className="flex items-center gap-1">
Pension 65 · Ende 82
<span className="ml-auto rounded border border-border px-1 text-[9px]">Pensionsplanung</span>
</div>
</MockTile>
<MockTile title="Kennzahlen" cls={`${dim("figures")} ${ring("figures")}`}>
<MockFigure label="Vermögen heute" value="412'000" />
<MockFigure label="Endvermögen nominal" value="1'284'000" />
<MockFigure label="Endvermögen real" value="702'000" />
<div className="mt-1 rounded bg-success-soft px-1.5 py-0.5 text-success">
Kapital reicht über die ganze Planung
</div>
</MockTile>
<MockTile title="Schnellaktionen" cls={`${dim("actions")} ${ring("actions")}`}>
{["Neues Szenario", "Tour", "Änderungshistorie", "Rechenwege", "CSV-Export"].map((a) => (
<div key={a} className="rounded border border-border px-1.5 py-0.5 text-muted">
{a}
</div>
))}
</MockTile>
<div
className={`flex flex-col gap-1 rounded-lg border border-attention bg-attention-soft px-2 py-1.5 transition-opacity ${dim("assistant")} ${ring("assistant")}`}
>
<div className="flex items-center gap-1">
<Sparkles className="h-3 w-3 text-attention-fg" />
<span className="font-semibold uppercase tracking-wide text-fg">FPT-Assistent</span>
<span className="ml-auto font-semibold text-muted">2 / 7</span>
</div>
{[
["3. Lebensphasen", "4 Lebensphasen"],
["4. Erwerbsjahre planen", ""],
["5. Pensionierung planen", ""],
].map(([t, sub]) => (
<div key={t} className="flex items-start gap-1">
<span className="mt-0.5 h-2.5 w-2.5 shrink-0 rounded-sm border border-border bg-surface" />
<span className="min-w-0">
<span className="block truncate font-semibold text-fg">{t}</span>
{sub && <span className="block truncate text-muted">{sub}</span>}
</span>
</div>
))}
{["1. Bestandsaufnahme", "2. Eckdaten"].map((t) => (
<div key={t} className="flex items-start gap-1 opacity-50">
<span className="mt-0.5 flex h-2.5 w-2.5 shrink-0 items-center justify-center rounded-sm bg-success text-[7px] text-white">
</span>
<span className="truncate text-muted line-through">{t}</span>
</div>
))}
</div>
</div>
{/* Zeitachse */}
<div
className={`mt-2 rounded-lg border border-border bg-surface p-2 transition-opacity ${dim("timeline")} ${ring("timeline")}`}
>
<div className="mb-1 flex justify-between text-faint">
<span>2026</span>
<span>2065</span>
</div>
<div className="flex h-4 overflow-hidden rounded">
<div className="flex w-[45%] items-center justify-center bg-accent/70 text-[8px] text-white">
Erwerbsjahre
</div>
<div className="flex w-[15%] items-center justify-center bg-attention/70 text-[8px] text-white">
Übergangszeit
</div>
<div className="flex w-[40%] items-center justify-center bg-success/60 text-[8px] text-white">
Ruhestand
</div>
</div>
<div className="mt-1 flex justify-between text-faint">
<span>Anna 42 · Beat 45</span>
<span>Pension 65</span>
<span>Anna 82 · Beat 85</span>
</div> </div>
</div> </div>
</div> </div>
{/* Matrix */}
<div
className={`overflow-hidden rounded-lg border border-border bg-surface transition-opacity ${dim("matrix")} ${ring("matrix")}`}
>
<table className="w-full border-collapse">
<thead>
<tr className="bg-surface-2">
<th className="border-b border-r border-border px-2 py-1.5 text-left align-top">
<div className="font-semibold uppercase tracking-wide text-faint">Matrix</div>
<div className="mt-1 flex gap-1">
<span className="rounded border border-dashed border-border px-1 text-[9px] font-normal text-muted">
+ Element
</span>
<span className="rounded border border-dashed border-border px-1 text-[9px] font-normal text-muted">
+ Phase
</span>
</div>
</th>
{["Erwerbsjahre", "Übergang", "Ruhestand I", "Übergang", "Ruhestand II"].map((h, i) => (
<th
key={i}
className={`border-b border-r border-border px-2 py-1.5 text-center font-medium ${
h === "Übergang" ? "bg-attention text-attention-fg" : "text-fg"
}`}
>
{h}
{h === "Übergang" && <div className="text-[8px] font-normal">2 offen</div>}
</th>
))}
</tr>
</thead>
<tbody>
{[
["Lohn Anna", "96'000", "→", "—", "→", "—"],
["Lebenshaltung", "78'000", "→", "72'000", "→", "68'000"],
["Pensionskasse Anna", "310'000", "40 % Kapital", "Rente 18'400", "→", "18'400"],
["Säule 3a Anna", "84'000", "Bezug mit 64", "—", "→", "—"],
["Wertschriften", "120'000", "→", "486'000", "→", "310'000"],
].map((row, i) => (
<tr key={i} className={i % 2 ? "bg-surface-2/40" : ""}>
<td className="border-b border-r border-border px-2 py-1 text-fg">{row[0]}</td>
{row.slice(1).map((c, j) => (
<td
key={j}
className={`border-b border-r border-border px-2 py-1 text-center tabular-nums ${
j % 2 === 1 ? "bg-attention-soft text-attention-soft-fg" : "text-muted"
}`}
>
{c}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div> </div>
</> </div>
);
}
function MockTile({ title, cls, children }: { title: string; cls: string; children: React.ReactNode }) {
return (
<div className={`flex flex-col gap-0.5 rounded-lg border border-border bg-surface px-2 py-1.5 transition-opacity ${cls}`}>
<div className="mb-0.5 font-semibold uppercase tracking-wide text-faint">{title}</div>
{children}
</div>
);
}
function MockFigure({ label, value }: { label: string; value: string }) {
return (
<div className="flex justify-between gap-1">
<span className="truncate text-muted">{label}</span>
<span className="shrink-0 font-semibold tabular-nums text-fg">{value}</span>
</div>
); );
} }