c2fb82b0de
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>
127 lines
4.2 KiB
TypeScript
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>
|
|
);
|
|
}
|