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,126 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user