UI-Gesamtumbau: Du-Form, Inspector-Panel, Onboarding-Assistent, Tour, Palette
Deploy App / deploy (push) Successful in 1m1s
Deploy App / deploy (push) Successful in 1m1s
Rein an der Oberflaeche -- Berechnung, Datenmodell und API-Semantik unveraendert (103 Tests unveraendert gruen). Paket A (Fundament): - durchgehend Du-Form und echte Umlaute in allen sichtbaren Texten, inkl. API-Fehlermeldungen (vorher Mix aus Sie/Du und ae/oe/ue) - neue UI-Primitiven (ui.tsx): Button, Modal mit ESC/Fokus-Falle/Animation, Bestaetigungs-Dialog statt window.confirm, Toasts statt alert, Skeleton-Loader, EmptyState - eigene Attention-Farbe (Amber) fuer offene Entscheide, getrennt vom Akzent - Micro-Interactions mit prefers-reduced-motion-Fallback Paket B (Onboarding, Roadmap Nr. 10): - gefuehrter Plan-Assistent in 5 Schritten; Einkommen bewusst pro Person (raeumt die 9.9-AHV-Falle aus); reine Orchestrierung bestehender Endpunkte - Beispielplan mit einem Klick; Uebergaenge absichtlich offen - interaktive Tour ueber die Planansicht (localStorage, jederzeit neu startbar) - abgeleitete "Naechste Schritte"-Karte (offene Entscheide, fehlende Elemente, fehlende Pensionsphase, Ruin -> Einflussfaktoren) Paket C (Struktur): - Inspector-Panel rechts statt Modals fuer alle Einzel-Bearbeitungen; Matrix bleibt sichtbar, Zellklick wechselt den Inhalt - Phasenkopf auf vier Kern-Infos entschlackt (Rest in der 0.11-Detailansicht) - Matrix mit eigenem Scrollbereich, Koepfe beidachsig fixiert - Sidebar-Gruppen "Meine Plaene" / "Wissen"; "So rechnet FPT" statt SPEZIFIKATION; "Szenario-Profil" statt "Plan-Einstellungen" - Aktions-Icons ohne Hover sichtbar (Touch) Paket D (Extras): - Sparklines je Element-Zeile aus den 0.11-Verlaufswerten - Befehls-Palette (Ctrl/Cmd+K) - Ruin-Banner verlinkt auf die Einflussfaktoren Nebenbei: der ProfileMenu-Lint-Fehler und der Selection-Rest (9.17) sind behoben -- npm run lint laeuft erstmals fehlerfrei. SPEZIFIKATION auf 0.13: neue Kapitel 3.2.8, 3.7.6-3.7.9, 9.23, 9.24; 3.6.3 und 3.7.1 ueberarbeitet, 9.17 bereinigt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
// Interaktive Kurz-Tour über die Planansicht (Teil von Roadmap Nr. 10). Bewusst simpel
|
||||
// gehalten: eine feste Karte unten rechts plus ein pulsierender Rahmen um das jeweilige
|
||||
// Ziel-Element (per data-tour-Attribut) -- kein Spotlight-Overlay, keine Positionierungs-
|
||||
// Engine. Ziele, die es im aktuellen Plan nicht gibt (z. B. Übergänge bei nur einer
|
||||
// Phase), werden zur Laufzeit übersprungen.
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Lightbulb, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui";
|
||||
|
||||
export const TOUR_DONE_KEY = "fpt-tour-done";
|
||||
|
||||
interface TourStep {
|
||||
target: string; // data-tour="…"
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const STEPS: TourStep[] = [
|
||||
{
|
||||
target: "profil",
|
||||
title: "Dein Grundprofil",
|
||||
text: "Alter, Pensionsalter und Inflationsannahme. Alles hier gilt für dieses Szenario – über «Profil» änderst du es jederzeit.",
|
||||
},
|
||||
{
|
||||
target: "timeline",
|
||||
title: "Die Zeitachse",
|
||||
text: "Deine Lebensphasen im Überblick: Breite = Dauer, Fähnchen = Pensionierung. Die ganze Planung denkt in diesen Phasen.",
|
||||
},
|
||||
{
|
||||
target: "matrix",
|
||||
title: "Die Matrix – das Herzstück",
|
||||
text: "Zeilen sind deine Finanzen, Spalten deine Lebensphasen. Klick auf eine Zelle: rechts öffnet sich das Bearbeiten-Panel, die Matrix bleibt sichtbar.",
|
||||
},
|
||||
{
|
||||
target: "transition",
|
||||
title: "Übergänge",
|
||||
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.",
|
||||
},
|
||||
{
|
||||
target: "cash",
|
||||
title: "Das Cash-Konto",
|
||||
text: "Dein Ausgleichskonto: Was übrig bleibt, landet hier – was fehlt, macht es rot. Negativ = Liquiditätslücke.",
|
||||
},
|
||||
{
|
||||
target: "analysen",
|
||||
title: "Analysen",
|
||||
text: "Grafiken, Monte-Carlo-Simulation und Einflussfaktoren: Wie sicher ist dein Plan, und welche Annahme entscheidet wirklich?",
|
||||
},
|
||||
];
|
||||
|
||||
export function Tour({ onClose }: { onClose: () => void }) {
|
||||
// Nur Schritte, deren Ziel im DOM existiert (hängt vom Plan ab).
|
||||
const steps = useMemo(
|
||||
() => STEPS.filter((s) => document.querySelector(`[data-tour="${s.target}"]`) !== null),
|
||||
[]
|
||||
);
|
||||
const [index, setIndex] = useState(0);
|
||||
const step = steps[index];
|
||||
|
||||
useEffect(() => {
|
||||
if (!step) return;
|
||||
const el = document.querySelector(`[data-tour="${step.target}"]`);
|
||||
if (!el) return;
|
||||
el.classList.add("tour-highlight");
|
||||
el.scrollIntoView({ block: "center", behavior: "smooth" });
|
||||
return () => el.classList.remove("tour-highlight");
|
||||
}, [step]);
|
||||
|
||||
if (!step) return null;
|
||||
|
||||
function finish() {
|
||||
try {
|
||||
localStorage.setItem(TOUR_DONE_KEY, "1");
|
||||
} catch {
|
||||
/* localStorage nicht verfügbar -> Tour erscheint beim nächsten Mal erneut */
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ui-pop fixed bottom-4 left-1/2 z-40 w-[26rem] max-w-[calc(100vw-2rem)] -translate-x-1/2 rounded-2xl border border-border bg-surface p-4 shadow-2xl">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-accent-soft text-accent-soft-fg">
|
||||
<Lightbulb className="h-4 w-4" />
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-fg">{step.title}</div>
|
||||
<div className="text-[11px] text-faint">
|
||||
Schritt {index + 1} von {steps.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={finish}
|
||||
aria-label="Tour beenden"
|
||||
className="rounded-md p-1 text-faint hover:bg-surface-2 hover:text-fg"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted">{step.text}</p>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<div className="flex gap-1">
|
||||
{steps.map((_, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`h-1.5 w-1.5 rounded-full ${i === index ? "bg-accent" : "bg-border-strong"}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{index > 0 && (
|
||||
<Button variant="secondary" size="sm" onClick={() => setIndex(index - 1)}>
|
||||
Zurück
|
||||
</Button>
|
||||
)}
|
||||
{index < steps.length - 1 ? (
|
||||
<Button size="sm" onClick={() => setIndex(index + 1)}>
|
||||
Weiter
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="sm" onClick={finish}>
|
||||
Fertig
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user