Files
FPT/src/components/CommandPalette.tsx
T
admGitAICDS c2fb82b0de
Deploy App / deploy (push) Successful in 1m1s
UI-Gesamtumbau: Du-Form, Inspector-Panel, Onboarding-Assistent, Tour, Palette
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>
2026-07-19 07:40:42 +02:00

127 lines
4.2 KiB
TypeScript

"use client";
// Befehls-Palette (Cmd/Ctrl+K): schnelles Springen zwischen Plänen und Szenarien plus
// die wichtigsten Aktionen -- der Effizienz-Pfad für geübte Nutzer, ohne der Maus-
// Bedienung etwas wegzunehmen.
import { useEffect, useMemo, useRef, useState } from "react";
import { CornerDownLeft, Search } from "lucide-react";
import type { PlanListItem } from "@/lib/types";
export interface PaletteAction {
id: string;
label: string;
hint?: string;
run: () => void;
}
export function CommandPalette({
open,
onClose,
plans,
actions,
onOpenScenario,
}: {
open: boolean;
onClose: () => void;
plans: PlanListItem[];
actions: PaletteAction[];
onOpenScenario: (id: string) => void;
}) {
const [query, setQuery] = useState("");
const [index, setIndex] = useState(0);
const inputRef = useRef<HTMLInputElement | null>(null);
const items = useMemo<PaletteAction[]>(() => {
const scenarioItems: PaletteAction[] = plans.flatMap((p) =>
p.scenarios.map((s) => ({
id: `s-${s.id}`,
label: `${p.name} · ${s.name}`,
hint: "Szenario öffnen",
run: () => onOpenScenario(s.id),
}))
);
const all = [...actions, ...scenarioItems];
const q = query.trim().toLowerCase();
if (!q) return all;
return all.filter((i) => i.label.toLowerCase().includes(q));
}, [plans, actions, query, onOpenScenario]);
useEffect(() => {
if (open) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- Reset beim Öffnen
setQuery("");
setIndex(0);
requestAnimationFrame(() => inputRef.current?.focus());
}
}, [open]);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- Auswahl folgt der Eingabe
setIndex(0);
}, [query]);
if (!open) return null;
function choose(item: PaletteAction | undefined) {
if (!item) return;
onClose();
item.run();
}
return (
<div className="ui-fade fixed inset-0 z-50 flex items-start justify-center bg-black/40 px-4 pt-[15vh]" onClick={onClose}>
<div
onClick={(e) => e.stopPropagation()}
className="ui-pop w-full max-w-lg overflow-hidden rounded-2xl border border-border bg-surface shadow-2xl"
>
<div className="flex items-center gap-2 border-b border-border px-4 py-3">
<Search className="h-4 w-4 shrink-0 text-faint" />
<input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
setIndex((i) => Math.min(i + 1, items.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setIndex((i) => Math.max(i - 1, 0));
} else if (e.key === "Enter") {
e.preventDefault();
choose(items[index]);
} else if (e.key === "Escape") {
onClose();
}
}}
placeholder="Plan, Szenario oder Aktion suchen…"
className="w-full bg-transparent text-sm text-fg outline-none placeholder:text-faint"
/>
<kbd className="rounded border border-border bg-surface-2 px-1.5 py-0.5 text-[10px] text-faint">ESC</kbd>
</div>
<div className="max-h-80 overflow-y-auto py-1">
{items.length === 0 && <p className="px-4 py-3 text-sm text-muted">Nichts gefunden.</p>}
{items.slice(0, 40).map((item, i) => (
<button
key={item.id}
type="button"
onMouseEnter={() => setIndex(i)}
onClick={() => choose(item)}
className={`flex w-full items-center justify-between gap-3 px-4 py-2 text-left text-sm ${
i === index ? "bg-accent-soft text-accent-soft-fg" : "text-fg"
}`}
>
<span className="min-w-0 flex-1 truncate">{item.label}</span>
<span className="flex items-center gap-1.5 text-[10px] text-faint">
{item.hint}
{i === index && <CornerDownLeft className="h-3 w-3" />}
</span>
</button>
))}
</div>
</div>
</div>
);
}