01 / MINI-LEKTION · 3 MINUTEN
Weniger Klicks sind noch kein besserer Arbeitsablauf.
In einer komplexen Projekt-App möchte jemand eine Aufgabe zuweisen. Eine Command Palette kann den Weg über mehrere Ansichten abkürzen. Dafür muss die Person aber wissen, dass es den Schnellzugriff gibt, und den gewünschten Treffer richtig deuten. Eine vermeintlich schnelle Auswahl kann sonst zur falschen Aktion führen.
- Auffindbarkeit: Ein sichtbarer Button ergänzt das Tastaturkürzel. Die Funktion darf nicht nur denjenigen offenstehen, die ein Kürzel bereits kennen.
- Erkennen statt Auswendiglernen: Beschriftungen und fachlich passende Synonyme helfen bei der Suche. Menschen sollten nicht die internen Befehlsnamen erraten müssen.
- Schrittweise Offenlegung: Die Suche bietet zunächst einen überschaubaren Einstieg. Erst nach einer Auswahl erscheinen nötige Eingaben, Kontext oder Bestätigung.
Die Palette ergänzt eine verständliche Navigation. Sie ist keine Ausrede, zentrale Bereiche oder wichtige Aktionen nur noch über die Suche erreichbar zu machen. Prüfe den tatsächlichen Aufgabenerfolg und Fehlaktionen, statt lediglich Klicks zu zählen.
02 / DEINE CHALLENGE · 8 MINUTEN
Navigation und Aktionen in einer Suche.
Du gestaltest den Schnellzugriff einer fiktiven Projekt-App. Die folgenden Annahmen dienen der Übung, nicht einer echten Nutzungsanalyse:
- Häufig, geringes Risiko: Projekt öffnen, Aufgabe erstellen.
- Mittlere Häufigkeit, im Beispiel geringes Risiko: Aufgabe zuweisen, Bericht exportieren.
- Selten, mittleres Risiko: Teammitglied einladen, Projekt archivieren.
- Sehr selten, hohes Risiko: Projekt dauerhaft löschen.
Die Risikoeinstufung ist kontextabhängig. Ein Export vertraulicher Daten oder eine Einladung mit weitreichenden Rechten wäre keineswegs automatisch risikoarm.
- Struktur · 2 Minuten: Gruppiere Navigation und Aktionen. Entscheide, welche Information ein Treffer vor der Aktivierung zeigen muss. „Zuletzt verwendet“ darf erst echte Nutzung in diesem Kontext abbilden; erfinde keine persönliche Historie.
- UI · 4 Minuten: Entwirf für 1440 px Breite eine Palette mit 560 px als Ausgangsbreite, höchstens 480 px Höhe und mindestens 44 px hohen Ergebniszeilen. Zeige eine leere Suche, Treffer für „Projekt“ und keine Treffer. Lasse Beschriftungen bei 200 % Text wachsen, statt sie abzuschneiden.
- Interaktion · 2 Minuten: Definiere sichtbares Öffnen, Ctrl/⌘ K, Pfeiltasten, Enter, Escape und Tab. Was passiert nach Enter auf „Projekt löschen“? Die Auswahl muss zunächst einen nachvollziehbaren Bestätigungsprozess öffnen.
Die Maße sind Startwerte, keine universelle Vorgabe. Auf kleinen Bildschirmen muss sich die Palette an den verfügbaren Raum anpassen. Ein erster Figma-Entwurf genügt für diese kurze Challenge.
03 / BEDIENBARE MUSTERLÖSUNG
Ein Treffer sagt, was als Nächstes geschieht.
Öffne die Palette über den sichtbaren Button. Suche zuerst „Projekt“, dann „Rechnung“ und schließlich einen nicht vorhandenen Begriff. Mit Pfeiltasten wechselst du den aktiven Treffer; Enter aktiviert ihn. Escape schließt, Tab bleibt die reguläre Fokusnavigation im Dialog.
LOKALES COMMAND-PALETTE-LABOR
Der Shortcut gilt, solange der Fokus im Labor liegt. Der sichtbare Button funktioniert auch ohne Tastaturkürzel. Alle Aktionen sind fiktiv; nutze ausschließlich Beispieldaten.
Website Relaunch
Lokaler Status: aktiv
„Startseite prüfen“ · zuständig: Alex Beispiel
- Startseite prüfen
Probiere eine lokale Zuweisung oder Archivierung und anschließend die Löschsuche. Enter auf dem Lösch-Treffer entfernt nichts: Erst ein eigener Schritt mit Projektname und ausdrücklicher Bestätigung kann den lokalen Zustand umschalten. Das Zurücksetzen der Demo ist keine Aussage darüber, dass eine echte dauerhafte Löschung rückgängig gemacht werden könnte.
04 / TECHNISCHER IMPULS · 3 MINUTEN
Suchauswahl und Ausführung bewusst trennen.
Interaktionsmodell und vollständigen Quellcode öffnen
Dies ist eine eigene Musterlösung mit fiktiven Daten, keine getestete Produktfunktion. Lokale Änderungen bleiben nur im Arbeitsspeicher dieser Lektion und gehen beim Neuladen verloren. Es werden keine Einladungen versendet und keine echten Projekte geändert.
1. Zwei klar benannte Ergebnistypen.
Die Gruppen heißen Navigation und Aktionen. Zusätzlich steht pro Treffer „Seite öffnen“ oder „Schritt öffnen“. Navigationsziele führen zu tatsächlichen lokalen Beispielseiten; dort gibt es einen Link zurück zur Lektion. Aktionsbefehle öffnen erst eine Eingabe oder Prüfung. Dadurch behauptet ein Listeneintrag nicht schon, dass eine Änderung ausgeführt wurde.
Der Berichtsschritt zeigt die fiktiven Exportdaten. Erst der zusätzliche Download-Button erzeugt eine lokale CSV mit Projektname und Beispielstatus. Eingegebene Aufgaben und Adressen gehören nicht zu diesem Download. Die Einladung simuliert nur die Rückmeldung; sie sendet keine E-Mail und vergibt keine Rechte.
2. Synonyme bleiben nachvollziehbar.
Die Suche gleicht normalisierte Begriffe mit Beschriftungen und gepflegten Synonymen ab. Großschreibung, Umlaute und ß werden vereinheitlicht; mehrere Suchwörter müssen gemeinsam passen. „Rechnung“ findet in dieser Demo den Berichtsexport. Dieser erzeugt einen Beispielbericht, keine echte Rechnung. Ob ein Synonym fachlich sinnvoll ist, muss im Produkt überprüft werden.
Die Lösung implementiert weder Fuzzy Search noch KI-Interpretation. Das macht ihre Grenzen sichtbar: Ein unbekanntes Wort führt zu einer leeren Liste mit Vorschlägen und einer Reset-Aktion. Eine intelligente Suche wäre nur dann besser, wenn sie passende Treffer verlässlich erkennt und Fehlinterpretationen verständlich behandelt.
3. Combobox und aktive Auswahl gehören zusammen.
Der DOM-Fokus bleibt beim Durchlaufen der Ergebnisse im Suchfeld. Dieses verbindet sich über aria-controls mit der Listbox und benennt mit aria-activedescendant den aktiven Treffer. Die Optionen tragen aria-selected. Pfeiltasten aktualisieren die Auswahl, Enter aktiviert genau diesen Treffer und ein leeres Ergebnis hat keinen aktiven Nachfahren. Die Hervorhebung nutzt zusätzlich zur Farbe eine Kontur und eine sichtbare Markierung.
Damit geht die Musterlösung über eine bloße Liste von per Tab erreichbaren Buttons hinaus. Die W3C Authoring Practices beschreiben die Beziehungen und Tastaturinteraktionen des Combobox-Patterns. Die konkrete Verwendung als Befehlsauswahl muss trotzdem mit assistiven Technologien geprüft werden. W3C APG: Combobox Pattern.
4. Modalität und Fokus sind Teil der Funktion.
Das native dialog wird mit showModal() geöffnet. Suche und Aktionsschritt haben jeweils einen verständlichen Dialogtitel. Beim Öffnen der Suche geht der Fokus ins Eingabefeld; beim Wechsel zu einem Aktionsschritt zunächst zur Überschrift, damit dessen Kontext wahrnehmbar bleibt. Escape oder der sichtbare Schließen-Button verlassen den Dialog. Danach kehrt der Fokus zum vorherigen Element im Labor zurück.
Tab und Shift+Tab müssen innerhalb des geöffneten modalen Dialogs bleiben. Das native Element übernimmt wichtige Grundlagen, ersetzt aber keine Prüfung im Zielbrowser. W3C APG: Modal Dialog Pattern, MDN: dialog.
Der Shortcut wird absichtlich nur im fokussierten Labor behandelt. Die Lektion übernimmt nicht pauschal ein Browserkürzel für die gesamte Website. Auch wiederholtes Gedrückthalten von Enter in der Suche soll nicht mehrere Schritte unkontrolliert auslösen.
5. Riskante Aktionen brauchen konkreten Kontext.
Der Löschbefehl steht am Ende der Befehlsliste und nennt die nötige Bestätigung. Sein eigener Schritt benennt „Website Relaunch“, die mögliche Folge einer echten Produktaktion und den Unterschied zur Simulation. Die Bestätigung wird erst aktiv, wenn der Projektname genau eingegeben wurde. Das ist ein Gestaltungsvorschlag für diese Übung, kein pauschal notwendiges Ritual für jede riskante Aktion.
Archivierung und Zuweisung zeigen ebenfalls den betroffenen Kontext. Nach einer Archivierung sind weitere Änderungen am inaktiven Beispielprojekt nicht stillschweigend erlaubt. Die Demo erklärt den Zustand und bietet außerhalb der Palette Wiederherstellung beziehungsweise Zurücksetzen. Ein echtes System müsste zusätzlich Berechtigungen, serverseitige Gültigkeit und Fehlerfälle prüfen.
6. Große Schrift braucht Platz statt Ellipsen.
Ergebniszeilen haben eine Mindesthöhe und dürfen wachsen. Der Dialog und die Liste scrollen bei Bedarf in ihrem eigenen Bereich. Bei der 200-%-Option darf der Dialog mehr verfügbare Viewport-Höhe nutzen; die Bedienhinweise sind aufklappbar. Die 200-%-Option vergrößert ausschließlich den Palettentext; sie ergänzt, ersetzt aber nicht Browserzoom und die Prüfung mit einer mobilen Bildschirmtastatur.
coaching-command-palette.ts
export const paletteLessonPath = "/kurse/ui-ux-coaching/command-palettes";
export const paletteProjectName = "Website Relaunch";
export type PaletteCommandId = "projects" | "tasks" | "create" | "assign" | "export" | "invite" | "archive" | "delete";
export const paletteCommands: { id: PaletteCommandId; label: string; kind: "navigation" | "action"; detail: string; aliases: string[]; href?: string; risk?: boolean }[] = [
{ id: "projects", label: "Projekte öffnen", kind: "navigation", detail: "Öffnet eine lokale Beispielseite", aliases: ["Projekt", "Liste", "öffnen"], href: "/kurse/ui-ux-coaching/command-palettes/demo/projekte" },
{ id: "tasks", label: "Aufgaben öffnen", kind: "navigation", detail: "Öffnet eine lokale Beispielseite", aliases: ["Todos", "Task", "Liste"], href: "/kurse/ui-ux-coaching/command-palettes/demo/aufgaben" },
{ id: "create", label: "Aufgabe erstellen", kind: "action", detail: "Öffnet die Eingabe für eine Beispielaufgabe", aliases: ["neu", "Todo", "Task", "anlegen"] },
{ id: "assign", label: "Aufgabe zuweisen", kind: "action", detail: "Für „Startseite prüfen“ · nächsten Schritt öffnen", aliases: ["Verantwortung", "Person", "Teammitglied", "übertragen"] },
{ id: "export", label: "Bericht exportieren", kind: "action", detail: "Beispieldaten prüfen, dann CSV herunterladen", aliases: ["Rechnung", "Abrechnung", "Download", "CSV"] },
{ id: "invite", label: "Teammitglied einladen", kind: "action", detail: "Einladung nur lokal simulieren", aliases: ["Person", "Einladung", "Team", "Zugang"] },
{ id: "archive", label: "Projekt archivieren", kind: "action", detail: "Website Relaunch · vorher prüfen", aliases: ["ablegen", "Projekt", "abschließen"] },
{ id: "delete", label: "Projekt dauerhaft löschen", kind: "action", detail: "Öffnet eine separate Bestätigung · hohes Risiko", aliases: ["Projekt löschen", "entfernen", "delete"], risk: true },
];
export function normalizeCommand(value: string): string {
return value.toLowerCase().replace(/ß/g,"ss").normalize("NFD").replace(/[\u0300-\u036f]/g,"").trim();
}
export function findPaletteCommands(query: string) {
const words = normalizeCommand(query).split(/\s+/).filter(Boolean);
return paletteCommands.filter(command => {
const haystack = normalizeCommand([command.label,...command.aliases].join(" "));
return words.every(word => haystack.includes(word));
});
}
export function movePaletteIndex(index: number, direction: number, count: number): number {
return count === 0 ? -1 : ((index + direction) % count + count) % count;
}
export type PaletteState = { project: "aktiv" | "archiviert" | "geloescht"; tasks: string[]; assignee: string; notice: string };
export function initialPaletteState(): PaletteState {
return {project:"aktiv",tasks:["Startseite prüfen"],assignee:"Alex Beispiel",notice:"Noch keine Aktion ausgeführt."};
}
export type PaletteAction = {type:"create";title:string} | {type:"assign";person:string} | {type:"invite";email:string} | {type:"archive"} | {type:"restore"} | {type:"delete";confirmation:string};
export function commandUnavailable(command: PaletteCommandId, state: PaletteState): string | null {
if (["create","assign","archive"].includes(command) && state.project !== "aktiv") return "Das Beispielprojekt ist nicht aktiv. Setze die Demo zurück oder stelle die Archivierung wieder her.";
if (command === "delete" && state.project === "geloescht") return "Das Beispielprojekt ist bereits als gelöscht markiert.";
return null;
}
export function applyPaletteAction(state: PaletteState, action: PaletteAction): PaletteState {
if (action.type !== "restore" && commandUnavailable(action.type,state)) return state;
switch(action.type) {
case "create": return action.title.trim() ? {...state,tasks:[...state.tasks,action.title.trim()],notice:"Beispielaufgabe lokal erstellt. Es wurde nichts gespeichert oder gesendet."} : state;
case "assign": return ["Alex Beispiel","Sam Beispiel"].includes(action.person) ? {...state,assignee:action.person,notice:`„Startseite prüfen“ ist lokal ${action.person} zugeordnet.`} : state;
case "invite": return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(action.email.trim()) ? {...state,notice:"Einladung lokal simuliert. Es wurde keine Nachricht versendet und kein Zugang vergeben."} : state;
case "archive": return {...state,project:"archiviert",notice:"Beispielprojekt lokal archiviert. Die Demo bietet eine Wiederherstellung."};
case "restore": return state.project === "archiviert" ? {...state,project:"aktiv",notice:"Lokale Archivierung rückgängig gemacht."} : state;
case "delete": return action.confirmation === paletteProjectName ? {...state,project:"geloescht",notice:"Löschung lokal simuliert. Das ist kein echter Löschvorgang. „Demo zurücksetzen“ stellt die Beispieldaten wieder her."} : state;
}
}
coaching-command-palette.tsx
"use client";
import { useEffect, useId, useRef, useState, type KeyboardEvent } from "react";
import { applyPaletteAction, commandUnavailable, findPaletteCommands, initialPaletteState, movePaletteIndex, paletteProjectName, paletteLessonPath, type PaletteCommandId } from "@/lib/coaching-command-palette";
import "./coaching-command-palette.css";
export function CoachingCommandPalette() {
const id=useId();
const [state,setState]=useState(initialPaletteState);
const [open,setOpen]=useState(false);
const [step,setStep]=useState<PaletteCommandId | null>(null);
const [query,setQuery]=useState("");
const [active,setActive]=useState(0);
const [large,setLarge]=useState(false);
const [value,setValue]=useState("");
const [person,setPerson]=useState("Sam Beispiel");
const [error,setError]=useState("");
const dialog=useRef<HTMLDialogElement>(null);
const search=useRef<HTMLInputElement>(null);
const title=useRef<HTMLHeadingElement>(null);
const trigger=useRef<HTMLButtonElement>(null);
const resetButton=useRef<HTMLButtonElement>(null);
const returnTo=useRef<HTMLElement | null>(null);
const optionRefs=useRef<(HTMLDivElement | null)[]>([]);
const list=useRef<HTMLDivElement>(null);
const results=findPaletteCommands(query);
const selected=active >= 0 ? results[active] : undefined;
const selectedStep=step ? findPaletteCommands("").find(command=>command.id===step)! : null;
const unavailable=step ? commandUnavailable(step,state) : null;
useEffect(()=>{
if(!open) return;
if(!dialog.current?.open) dialog.current?.showModal();
if(step===null) search.current?.focus(); else title.current?.focus();
},[open,step]);
useEffect(()=>{
const option=optionRefs.current[active], container=list.current;
if(!open || step!==null || !option || !container) return;
const row=option.getBoundingClientRect(), box=container.getBoundingClientRect();
if(row.top<box.top) container.scrollTop-=box.top-row.top;
else if(row.bottom>box.bottom) container.scrollTop+=row.bottom-box.bottom;
},[active,query,open,step]);
function show() {
returnTo.current=document.activeElement instanceof HTMLElement ? document.activeElement : trigger.current;
setQuery("");setActive(0);setStep(null);setError("");setOpen(true);
}
function close() { dialog.current?.close(); }
function activate(index: number) {
const command=results[index];
if(!command) return;
if(command.kind==="navigation" && command.href) { close();window.location.assign(command.href);return; }
setStep(command.id);setValue("");setError("");
}
function keydown(event: KeyboardEvent<HTMLInputElement>) {
if(event.nativeEvent.isComposing) return;
if(event.key==="ArrowDown" || event.key==="ArrowUp") { event.preventDefault();setActive(previous=>movePaletteIndex(previous,event.key==="ArrowDown"?1:-1,results.length)); }
if(event.key==="Enter") { event.preventDefault();if(!event.repeat) activate(active); }
}
function finish() {
if(!step || unavailable) return;
if(step==="create" && !value.trim()) {setError("Gib einen Titel für die Beispielaufgabe ein.");return;}
if(step==="invite" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim())) {setError("Nutze eine Beispieladresse wie sam@example.com.");return;}
if(step==="delete" && value!==paletteProjectName) {setError(`Tippe genau „${paletteProjectName}“, um diese lokale Simulation zu bestätigen.`);return;}
if(step==="export") {
const blob=new Blob([`Projekt;Status\n${paletteProjectName};${state.project}\n`],{type:"text/csv;charset=utf-8"});
const url=URL.createObjectURL(blob), link=document.createElement("a");
link.href=url;link.download="fiktiver-projektbericht.csv";link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
setState(previous=>({...previous,notice:"Eine CSV mit fiktiven Projektdaten wurde lokal zum Download bereitgestellt."}));
} else {
setState(previous=>applyPaletteAction(previous,step==="create"?{type:"create",title:value}:step==="assign"?{type:"assign",person}:step==="invite"?{type:"invite",email:value}:step==="archive"?{type:"archive"}:{type:"delete",confirmation:value}));
}
close();
}
return <div className="palette-lab" onKeyDown={event=>{if((event.ctrlKey||event.metaKey)&&!event.altKey&&event.key.toLowerCase()==="k"){event.preventDefault();if(!event.repeat&&!dialog.current?.open) show();}}}>
<p className="eyebrow">LOKALES COMMAND-PALETTE-LABOR</p>
<div className="palette-toolbar"><button ref={trigger} type="button" onClick={show}>Befehle suchen <kbd>Ctrl / ⌘ K</kbd></button><label><input type="checkbox" checked={large} onChange={event=>setLarge(event.target.checked)}/>200 % Text in der Palette</label></div>
<p>Der Shortcut gilt, solange der Fokus im Labor liegt. Der sichtbare Button funktioniert auch ohne Tastaturkürzel. Alle Aktionen sind fiktiv; nutze ausschließlich Beispieldaten.</p>
<nav className="palette-toolbar" aria-label="Sichtbare Beispielnavigation"><a href={`${paletteLessonPath}/demo/projekte`}>Projekte öffnen</a><a href={`${paletteLessonPath}/demo/aufgaben`}>Aufgaben öffnen</a></nav>
<div className="palette-project"><h3>{paletteProjectName}</h3><p>Lokaler Status: <strong>{state.project==="geloescht"?"Löschung simuliert":state.project}</strong></p><p>„Startseite prüfen“ · zuständig: {state.assignee}</p><ul>{state.tasks.map((task,index)=><li key={index}>{task}</li>)}</ul><div role="status" className="palette-notice">{state.notice}</div><div className="palette-toolbar">{state.project==="archiviert"&&<button type="button" onClick={()=>{resetButton.current?.focus();setState(previous=>applyPaletteAction(previous,{type:"restore"}));}}>Archivierung rückgängig</button>}<button ref={resetButton} type="button" onClick={()=>setState(initialPaletteState())}>Demo zurücksetzen</button></div></div>
<dialog ref={dialog} className="palette-dialog" data-large-text={large ? "true" : undefined} style={{fontSize:large?"28px":"14px"}} aria-labelledby={`${id}-title`} onClose={()=>{setOpen(false);setStep(null);if(returnTo.current?.isConnected) returnTo.current.focus();else trigger.current?.focus();}}>
<header><h3 ref={title} id={`${id}-title`} tabIndex={-1}>{selectedStep?.label ?? "Schnellzugriff"}</h3><button type="button" onClick={close}>Schließen</button></header>
{step===null ? <div className="palette-search"><label htmlFor={`${id}-query`}>Befehl suchen</label><input ref={search} id={`${id}-query`} role="combobox" aria-autocomplete="list" aria-expanded={open} aria-controls={`${id}-results`} aria-activedescendant={selected?`${id}-option-${selected.id}`:undefined} aria-describedby={`${id}-keys`} autoComplete="off" value={query} onChange={event=>{setQuery(event.target.value);setActive(0);}} onKeyDown={keydown}/><p className="palette-count" role="status">{results.length} Treffer</p>
{results.length===0&&<div className="palette-empty"><p>Keine passenden Befehle. Versuche „Aufgabe“, „Projekt“ oder „Rechnung“.</p><button type="button" onClick={()=>{setQuery("");setActive(0);search.current?.focus();}}>Suche zurücksetzen</button></div>}
<div ref={list} id={`${id}-results`} role="listbox" aria-label="Verfügbare Befehle" className="palette-results">{["navigation","action"].map(kind=>{
const group=results.filter(command=>command.kind===kind);
return group.length>0 ? <div role="group" aria-label={kind==="navigation"?"Navigation":"Aktionen · nächster Schritt"} key={kind}><p className="palette-group" aria-hidden="true">{kind==="navigation"?"Navigation":"Aktionen · nächster Schritt"}</p>{group.map(command=>{const index=results.indexOf(command);return <div ref={element=>{optionRefs.current[index]=element;}} key={command.id} id={`${id}-option-${command.id}`} role="option" aria-selected={index===active} className={`palette-option${command.risk?" palette-risk":""}`} onMouseDown={event=>event.preventDefault()} onMouseEnter={()=>setActive(index)} onClick={()=>activate(index)}><strong>{index===active&&<span aria-hidden="true">› </span>}{command.label}</strong><span>{command.kind==="navigation"?"Seite öffnen →":"Schritt öffnen …"}{command.risk?" · Bestätigung erforderlich":""}</span><small>{command.detail}</small></div>;})}</div>:null;
})}</div><details className="palette-key-help"><summary>Bedienhinweise</summary><p id={`${id}-keys`}>↑ / ↓ Auswahl · Enter öffnet · Escape schließt · Tab bewegt den Fokus regulär. Navigation öffnet lokale Demoseiten; Aktionen öffnen erst den nächsten Schritt.</p></details></div> : <div className="palette-step">
<button type="button" onClick={()=>{setStep(null);setError("");}}>Zur Befehlssuche</button>
<p>Kontext: <strong>{paletteProjectName}</strong> · nur lokale Demo.</p>
{unavailable ? <p className="palette-error">{unavailable}</p> : <form noValidate onSubmit={event=>{event.preventDefault();finish();}}>
{step==="create"&&<><label htmlFor={`${id}-value`}>Titel der Beispielaufgabe</label><input aria-invalid={Boolean(error)} aria-describedby={`${id}-error`} id={`${id}-value`} value={value} onChange={event=>{setValue(event.target.value);setError("");}}/></>}
{step==="assign"&&<><p>Aufgabe: „Startseite prüfen“. Aktuell: {state.assignee}.</p><label htmlFor={`${id}-person`}>Neue zuständige Person</label><select id={`${id}-person`} value={person} onChange={event=>setPerson(event.target.value)}><option>Alex Beispiel</option><option>Sam Beispiel</option></select></>}
{step==="invite"&&<><p>Es wird nur der Ablauf simuliert. Niemand erhält eine Einladung oder Zugriff.</p><label htmlFor={`${id}-value`}>Beispiel-E-Mail</label><input aria-invalid={Boolean(error)} aria-describedby={`${id}-error`} id={`${id}-value`} type="email" autoComplete="off" value={value} onChange={event=>{setValue(event.target.value);setError("");}}/></>}
{step==="export"&&<><p>Vorschau: Website Relaunch · Status {state.project}. Die CSV enthält nur diese fiktiven Projektdaten und keine eingegebenen Aufgaben oder Adressen.</p><p>Erst der folgende Button startet den lokalen Beispieldownload.</p></>}
{step==="archive"&&<p>Du archivierst das Beispielprojekt. In dieser Demo kannst du es anschließend außerhalb der Palette wiederherstellen.</p>}
{step==="delete"&&<><div className="palette-danger-note"><strong>Dauerhaft löschen erfordert eine bewusste Entscheidung.</strong><p>In einem echten Produkt könnte diese Aktion Projekt und Aufgaben unwiederbringlich entfernen. Hier wird nur ein lokaler Zustand umgeschaltet. Enter in der Suche löscht nichts.</p></div><label htmlFor={`${id}-value`}>Bestätigung: Tippe Website Relaunch</label><input aria-invalid={Boolean(error)} aria-describedby={`${id}-error`} id={`${id}-value`} autoComplete="off" value={value} onChange={event=>{setValue(event.target.value);setError("");}}/></>}
<p id={`${id}-error`} className="palette-error" role="status">{error}</p><button type="submit" className={step==="delete"?"palette-danger-button":""} disabled={step==="delete"&&value!==paletteProjectName}>{step==="create"?"Aufgabe lokal erstellen":step==="assign"?"Zuweisung lokal ändern":step==="invite"?"Einladung lokal simulieren":step==="export"?"CSV-Beispiel herunterladen":step==="archive"?"Archivierung lokal bestätigen":"Löschung lokal simulieren"}</button>
</form>}
</div>}
</dialog>
</div>;
}
coaching-command-palette.css
.palette-lesson .lesson-header h1 { overflow-wrap: anywhere; hyphens: auto; }
.palette-lab { margin: 28px 0; padding: 24px; background: #f0f2f6; border: 1px solid #cbd5e1; border-radius: 16px; min-width: 0; }
.palette-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 16px 0; }
.palette-toolbar button,.palette-toolbar a { min-height: 44px; padding: 11px 14px; border: 1px solid #64748b; border-radius: 8px; background: #fff; color: #172033; font: inherit; font-size: 14px; cursor: pointer; }
.palette-toolbar a { display: inline-flex; align-items: center; text-decoration: underline; }
.palette-toolbar label { display: flex; align-items: center; gap: 10px; font-size: 14px; min-height: 44px; }
.palette-toolbar input { width: 20px; height: 20px; accent-color: #4338ca; flex-shrink: 0; }
.palette-toolbar kbd { display: inline-block; margin-left: 8px; padding: 3px 5px; background: #eef2ff; border-radius: 4px; font: 12px monospace; }
.palette-lab > p:not(.eyebrow) { font-size: 14px; }
.palette-project { background: #fff; padding: 20px; border: 1px solid #cbd5e1; border-radius: 12px; }
.palette-project h3 { font-size: 24px; line-height: 1.3; margin: 0 0 12px; }
.palette-project p,.palette-project li { font-size: 14px; }
.palette-project ul { padding-left: 20px; list-style: disc; }
.palette-project li { overflow-wrap: anywhere; }
.palette-notice { font-size: 14px; line-height: 1.8; background: #eef2ff; padding: 14px; border-radius: 8px; margin: 20px 0; }
.palette-lab :is(button,a,input):focus-visible { outline: 3px solid #4338ca; outline-offset: 3px; }
.palette-dialog { width: 560px; max-width: calc(100vw - 32px); max-height: min(480px,calc(100dvh - 32px)); padding: 0; margin: auto; border: 1px solid #64748b; border-radius: 16px; background: #fff; color: #172033; overflow-y: auto; }
.palette-dialog::backdrop { background: rgb(12 20 35 / 60%); }
.palette-dialog header { display: flex; align-items: start; justify-content: space-between; flex-wrap: wrap; gap: 12px; padding: 18px; border-bottom: 1px solid #cbd5e1; }
.palette-dialog header h3 { font-size: 1.35em; line-height: 1.4; margin: 0; overflow-wrap: anywhere; }
.palette-dialog button { min-height: 44px; padding: .7em .9em; border: 1px solid #64748b; border-radius: 8px; color: #172033; background: #fff; font: inherit; font-size: 1em; cursor: pointer; overflow-wrap: anywhere; }
.palette-dialog button:disabled { background: #f1f5f9; color: #64748b; cursor: default; }
.palette-dialog :is(button,input,select,h3):focus-visible { outline: 3px solid #4338ca; outline-offset: 3px; }
.palette-search { padding: 18px; }
.palette-dialog label { display: block; font-size: 1em; font-weight: 600; margin: 10px 0; }
.palette-dialog :is(input,select) { width: 100%; min-width: 0; min-height: 56px; border: 1px solid #64748b; border-radius: 7px; padding: .75em; background: #fff; color: #172033; font: inherit; }
.palette-dialog p { font-size: 1em; line-height: 1.8; margin: 12px 0; overflow-wrap: anywhere; }
.palette-dialog .palette-count { font-size: .9em; color: #475569; }
.palette-results { max-height: 260px; overflow-y: auto; padding: 4px; }
.palette-dialog .palette-group { font-size: .85em; color: #475569; font-weight: 600; margin: 20px 0 8px; }
.palette-option { display: grid; gap: 5px; min-height: 44px; padding: 12px; border: 2px solid transparent; border-radius: 8px; cursor: pointer; line-height: 1.6; margin: 5px 0; overflow-wrap: anywhere; }
.palette-option strong { font-size: 1em; }
.palette-option span,.palette-option small { font-size: .9em; color: #475569; }
.palette-option[aria-selected="true"] { border-color: #4338ca; background: #eef2ff; }
.palette-option.palette-risk strong { color: #a61b1b; }
.palette-dialog .palette-key-help { font-size: .85em; color: #475569; border-top: 1px solid #cbd5e1; padding-top: 12px; }
.palette-empty { padding: 12px; background: #f8fafc; border-radius: 8px; }
.palette-step { padding: 18px; }
.palette-dialog .palette-error { color: #a61b1b; min-height: 1.8em; }
.palette-danger-note { padding: 14px; border-left: 3px solid #a61b1b; background: #fff3f3; line-height: 1.8; margin: 18px 0; }
.palette-dialog .palette-danger-button { color: #fff; background: #a61b1b; border-color: #a61b1b; }
.palette-dialog .palette-danger-button:disabled { color: #475569; background: #f1f5f9; border-color: #64748b; }
@media(max-width:600px) { .palette-lab { padding: 16px; } .palette-project { padding: 16px; } .palette-search,.palette-step,.palette-dialog header { padding: 16px; } }
.palette-dialog[data-large-text="true"] { max-height: calc(100dvh - 32px); }
.palette-dialog .palette-key-help summary { cursor: pointer; min-height: 44px; padding: 10px 0; font-size: 1em; }
.palette-dialog .palette-key-help summary:focus-visible { outline: 3px solid #4338ca; outline-offset: 3px; }
05 / REFLEXION · 2 MINUTEN
Welche Abkürzung hilft wirklich?
Die Person sucht „Rechnung“, dein Produkt nennt es „Abrechnung exportieren“.
Wählst du Synonyme, unscharfe Suche oder KI-Interpretation? Welche Grenze muss erkennbar bleiben?
Mögliche Antwort zur Suche
Ich beginne bei bekannten Begriffen mit gepflegten Synonymen und verständlichen Trefferbeschriftungen. Ich prüfe, ob das Ziel tatsächlich zum Nutzerwunsch passt: Rechnung erstellen und einen Abrechnungsbericht exportieren sind unterschiedliche Aufgaben. Ein bloßer Worttreffer darf diese Differenz nicht verdecken.
Fuzzy Search kann Tippfehler abfangen, erzeugt aber möglicherweise unpassende Treffer. KI-Interpretation könnte komplexere Wünsche erschließen, benötigt jedoch eine verständliche Vorschau und Behandlung von Unsicherheit. Keine dieser Techniken rechtfertigt eine ungeprüfte riskante Ausführung.
Zwanzig Treffer, darunter Seiten und Aktionen.
Wie machst du den Unterschied ohne zwanzig unterschiedliche Icons oder zusätzliche Statusfarben sichtbar?
Mögliche Antwort zur Ergebnishierarchie
Ich gruppiere nach Zieltyp und ergänze kurze, konsistente Hinweise wie „Seite öffnen“ und „Aktion vorbereiten“. Innerhalb der Gruppen bleibt die Zeilenstruktur gleich. Kontext nennt bei Bedarf das betroffene Projekt oder Objekt. Risiko erhält eine ausdrückliche Kennzeichnung und einen passenden Folgeschritt.
Zusätzlich prüfe ich Auffindbarkeit, korrekte Auswahl und Fehlaktivierungen. Eine kleinere kuratierte Liste kann hilfreicher sein als ein zweites vollständiges App-Menü. Geschwindigkeit und Zufriedenheit gehören zur Bewertung, ersetzen aber den korrekten Aufgabenerfolg nicht.
Dein Selbstcheck
- Ist der Einstieg ohne Kenntnis des Shortcuts auffindbar?
- Finden Begriffe aus dem Nutzerkontext den fachlich richtigen Befehl?
- Sagt jeder Treffer, ob eine Seite oder ein nächster Aktionsschritt folgt?
- Funktionieren Pfeile, Enter, Escape, Tab und die Fokusrückgabe zusammen?
- Bleiben riskante Aktionen nach der Auswahl überprüfbar und bewusst bestätigbar?
DEIN MERKSATZ
Schnell für Erfahrene. Verständlich für alle.
Eine Command Palette soll erfahrene Menschen unterstützen, ohne neue auszuschließen. Sie ergänzt verständliche Navigation und macht aus einer schnellen Suche keine unkontrollierte Ausführung.