mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-26 16:52:53 +02:00
refactor(arcade, shared-ui): migrate CommandBar to GlobalSpotlight, delete CommandBar
CommandBar was a near-duplicate of QuickInputBar's InputBar with the UX of a Cmd+K modal. Only arcade still used it. Migrate arcade onto the existing GlobalSpotlight (hosted by PillNavigation) so there is a single Cmd+K modal across all mana apps, then remove CommandBar entirely. Arcade changes (games/arcade/apps/web/src/routes/(app)/+layout.svelte): - Merge commandBarQuickActions into spotlightActions (nav-level items) - Convert handleCommandBarSearch into a ContentSearcher that returns the game list grouped under a single 'Spiele' category - Drop the standalone Cmd+K handler; PillNavigation + GlobalSpotlight handle the shortcut - Remove the <CommandBar> render; add `contentSearcher` + spotlightPlaceholder to <PillNavigation> shared-ui cleanup: - Delete packages/shared-ui/src/command-bar/ (CommandBar.svelte, CommandBar.types.ts, index.ts) - Drop CommandBar / CommandBarItem from the public @mana/shared-ui export - Delete docs/central-services/COMMAND-BAR.md (stale) No more duplication: highlight + debounce live in search-core, and the only remaining Cmd+K surface is GlobalSpotlight. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9bc77dd3b9
commit
99efb93816
7 changed files with 62 additions and 1170 deletions
|
|
@ -1,383 +0,0 @@
|
|||
# Central Command Bar
|
||||
|
||||
Die zentrale Command Bar bietet eine einheitliche Schnellsuche und Navigation über alle Mana-Apps hinweg. Sie wird mit `Cmd/Ctrl+K` aktiviert und bietet Suche, Quick Actions und Tastatur-Navigation.
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ @mana/shared-ui │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ CommandBar.svelte │ │
|
||||
│ │ - Suche mit Debounce (150ms) │ │
|
||||
│ │ - Quick Actions │ │
|
||||
│ │ - Tastatur-Navigation │ │
|
||||
│ │ - Ergebnis-Anzeige mit Avataren │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
┌────▼────┐ ┌─────▼────┐ ┌─────▼────┐
|
||||
│ Todo │ │ Calendar │ │ Contacts │
|
||||
│ │ │ │ │ │
|
||||
│ Sucht │ │ Sucht │ │ Sucht │
|
||||
│ Tasks │ │ Events │ │ Kontakte │
|
||||
└─────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
## Package
|
||||
|
||||
| Package | Beschreibung |
|
||||
|---------|--------------|
|
||||
| `@mana/shared-ui` | CommandBar Svelte-Komponente + TypeScript-Typen |
|
||||
|
||||
## Keyboard Shortcut
|
||||
|
||||
| Shortcut | Aktion |
|
||||
|----------|--------|
|
||||
| `Cmd/Ctrl+K` | Command Bar öffnen |
|
||||
| `↑↓` | Navigation durch Ergebnisse |
|
||||
| `Enter` | Auswahl bestätigen |
|
||||
| `Escape` | Schließen |
|
||||
|
||||
## TypeScript Interfaces
|
||||
|
||||
### CommandBarItem
|
||||
|
||||
Ein Suchergebnis:
|
||||
|
||||
```typescript
|
||||
interface CommandBarItem {
|
||||
id: string; // Eindeutige ID
|
||||
title: string; // Haupttext (z.B. Name, Titel)
|
||||
subtitle?: string; // Untertitel (z.B. Datum, E-Mail)
|
||||
icon?: string; // Icon-Name
|
||||
imageUrl?: string; // Avatar/Bild URL
|
||||
isFavorite?: boolean; // Favorit-Markierung (zeigt Herz-Icon)
|
||||
}
|
||||
```
|
||||
|
||||
### QuickAction
|
||||
|
||||
Eine Schnellaktion (ohne Suche):
|
||||
|
||||
```typescript
|
||||
interface QuickAction {
|
||||
id: string; // Eindeutige ID
|
||||
label: string; // Anzeigetext
|
||||
icon: string; // Icon-Name
|
||||
href?: string; // Link-Ziel (optional)
|
||||
shortcut?: string; // Tastenkürzel-Anzeige
|
||||
onclick?: () => void; // Click-Handler (alternativ zu href)
|
||||
}
|
||||
```
|
||||
|
||||
### Unterstützte Icons
|
||||
|
||||
Die CommandBar unterstützt folgende eingebaute Icons:
|
||||
|
||||
| Icon-Name | Beschreibung |
|
||||
|-----------|--------------|
|
||||
| `plus` | Plus-Zeichen (Neu erstellen) |
|
||||
| `heart` | Herz (Favoriten) |
|
||||
| `tag` | Tag/Label |
|
||||
| `upload` | Upload (Import) |
|
||||
| `calendar` | Kalender |
|
||||
| `clock` | Uhr |
|
||||
| `check` | Häkchen |
|
||||
| `settings` | Zahnrad (Einstellungen) |
|
||||
| `list` | Liste |
|
||||
|
||||
## Komponenten-Props
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
open: boolean; // Sichtbarkeit
|
||||
onClose: () => void; // Schließen-Handler
|
||||
onSearch: (query: string) => Promise<CommandBarItem[]>; // Such-Funktion
|
||||
onSelect: (item: CommandBarItem) => void; // Auswahl-Handler
|
||||
quickActions?: QuickAction[]; // Schnellaktionen
|
||||
placeholder?: string; // Suchfeld-Placeholder
|
||||
emptyText?: string; // Text bei leeren Ergebnissen
|
||||
searchingText?: string; // Text während Suche
|
||||
}
|
||||
```
|
||||
|
||||
## Nutzung
|
||||
|
||||
### Basis-Beispiel
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { CommandBar } from '@mana/shared-ui';
|
||||
import type { CommandBarItem, QuickAction } from '@mana/shared-ui';
|
||||
|
||||
let commandBarOpen = $state(false);
|
||||
|
||||
// Quick Actions definieren
|
||||
const quickActions: QuickAction[] = [
|
||||
{ id: 'new', label: 'Neu erstellen', icon: 'plus', href: '/new', shortcut: 'N' },
|
||||
{ id: 'settings', label: 'Einstellungen', icon: 'settings', href: '/settings' },
|
||||
];
|
||||
|
||||
// Such-Funktion implementieren
|
||||
async function handleSearch(query: string): Promise<CommandBarItem[]> {
|
||||
const results = await api.search(query);
|
||||
return results.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.name,
|
||||
subtitle: item.description,
|
||||
}));
|
||||
}
|
||||
|
||||
// Auswahl verarbeiten
|
||||
function handleSelect(item: CommandBarItem) {
|
||||
goto(`/item/${item.id}`);
|
||||
}
|
||||
|
||||
// Keyboard Shortcut registrieren
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
|
||||
event.preventDefault();
|
||||
commandBarOpen = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<CommandBar
|
||||
bind:open={commandBarOpen}
|
||||
onClose={() => (commandBarOpen = false)}
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
{quickActions}
|
||||
placeholder="Suchen..."
|
||||
emptyText="Keine Ergebnisse"
|
||||
searchingText="Suche..."
|
||||
/>
|
||||
```
|
||||
|
||||
## App-spezifische Implementierungen
|
||||
|
||||
### Todo App
|
||||
|
||||
```typescript
|
||||
// Quick Actions
|
||||
const quickActions: QuickAction[] = [
|
||||
{ id: 'new', label: 'Neue Aufgabe erstellen', icon: 'plus', href: '/task/new', shortcut: 'N' },
|
||||
{ id: 'kanban', label: 'Kanban-Board', icon: 'list', href: '/kanban' },
|
||||
{ id: 'stats', label: 'Statistiken', icon: 'chart', href: '/statistics' },
|
||||
{ id: 'settings', label: 'Einstellungen', icon: 'settings', href: '/settings' },
|
||||
];
|
||||
|
||||
// Suche: Tasks durchsuchen
|
||||
async function handleSearch(query: string): Promise<CommandBarItem[]> {
|
||||
const tasks = await getTasks({ search: query });
|
||||
return tasks.slice(0, 10).map((task) => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
subtitle: task.isCompleted
|
||||
? '✓ Erledigt'
|
||||
: task.dueDate
|
||||
? new Date(task.dueDate).toLocaleDateString('de-DE')
|
||||
: 'Kein Datum',
|
||||
}));
|
||||
}
|
||||
|
||||
// Auswahl: Zu Task navigieren
|
||||
function handleSelect(item: CommandBarItem) {
|
||||
goto(`/task/${item.id}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Calendar App
|
||||
|
||||
```typescript
|
||||
// Quick Actions
|
||||
const quickActions: QuickAction[] = [
|
||||
{ id: 'new', label: 'Neuen Termin erstellen', icon: 'plus', href: '/event/new', shortcut: 'N' },
|
||||
{ id: 'today', label: 'Zu Heute springen', icon: 'calendar', onclick: () => viewStore.goToToday() },
|
||||
{ id: 'agenda', label: 'Agenda anzeigen', icon: 'list', href: '/agenda' },
|
||||
{ id: 'settings', label: 'Einstellungen', icon: 'settings', href: '/settings' },
|
||||
];
|
||||
|
||||
// Suche: Events durchsuchen
|
||||
async function handleSearch(query: string): Promise<CommandBarItem[]> {
|
||||
const result = await searchEvents(query);
|
||||
if (result.error || !result.data) return [];
|
||||
|
||||
return result.data.slice(0, 10).map((event) => ({
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
subtitle: format(new Date(event.startTime), 'dd. MMM yyyy, HH:mm', { locale: de }),
|
||||
}));
|
||||
}
|
||||
|
||||
// Auswahl: Zu Event navigieren
|
||||
function handleSelect(item: CommandBarItem) {
|
||||
goto(`/event/${item.id}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Contacts App
|
||||
|
||||
```typescript
|
||||
// Quick Actions
|
||||
const quickActions: QuickAction[] = [
|
||||
{ id: 'new', label: 'Neuen Kontakt erstellen', icon: 'plus', href: '/contacts/new', shortcut: 'N' },
|
||||
{ id: 'favorites', label: 'Favoriten anzeigen', icon: 'heart', href: '/favorites' },
|
||||
{ id: 'tags', label: 'Tags verwalten', icon: 'tag', href: '/tags' },
|
||||
{ id: 'import', label: 'Kontakte importieren', icon: 'upload', href: '/data?tab=import' },
|
||||
];
|
||||
|
||||
// Suche: Kontakte durchsuchen
|
||||
async function handleSearch(query: string): Promise<CommandBarItem[]> {
|
||||
const response = await contactsApi.list({ search: query, limit: 10 });
|
||||
return (response.contacts || []).map((contact) => ({
|
||||
id: contact.id,
|
||||
title: contact.displayName ||
|
||||
[contact.firstName, contact.lastName].filter(Boolean).join(' ') ||
|
||||
contact.email || 'Unbekannt',
|
||||
subtitle: contact.company || contact.email,
|
||||
imageUrl: contact.photoUrl,
|
||||
isFavorite: contact.isFavorite,
|
||||
}));
|
||||
}
|
||||
|
||||
// Auswahl: Kontakt-Modal öffnen
|
||||
function handleSelect(item: CommandBarItem) {
|
||||
goto(`/contacts/${item.id}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Funktionen
|
||||
|
||||
### Suche
|
||||
|
||||
- **Debounce:** 150ms Verzögerung für Performance
|
||||
- **Loading-State:** Spinner während der Suche
|
||||
- **Empty State:** Konfigurierbare Meldung bei keinen Ergebnissen
|
||||
- **Limit:** Ergebnisse werden typischerweise auf 10 begrenzt
|
||||
|
||||
### Quick Actions
|
||||
|
||||
Wenn kein Suchtext eingegeben ist, werden Quick Actions angezeigt:
|
||||
|
||||
- Navigation mit Pfeiltasten
|
||||
- Ausführung mit Enter
|
||||
- Keyboard-Shortcuts werden rechts angezeigt
|
||||
|
||||
### Ergebnis-Anzeige
|
||||
|
||||
- **Avatar:** Bild oder Initialen
|
||||
- **Titel:** Haupttext
|
||||
- **Untertitel:** Zusatzinfo (grau)
|
||||
- **Favorit:** Herz-Icon wenn `isFavorite: true`
|
||||
- **Hover:** Visuelles Feedback bei Maus-Over
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
| Taste | Aktion |
|
||||
|-------|--------|
|
||||
| `↑` / `↓` | Durch Ergebnisse navigieren |
|
||||
| `Enter` | Ausgewähltes Element öffnen |
|
||||
| `Escape` | Command Bar schließen |
|
||||
|
||||
## Styling
|
||||
|
||||
Die Command Bar verwendet ein dunkles Theme mit CSS-Variablen:
|
||||
|
||||
```css
|
||||
.command-modal {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 12px;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.command-result.selected {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.result-avatar {
|
||||
background: #3b82f6; /* Primary Color */
|
||||
}
|
||||
|
||||
.result-favorite {
|
||||
color: #ef4444; /* Rot für Herz */
|
||||
}
|
||||
```
|
||||
|
||||
### Animationen
|
||||
|
||||
- **Fade In:** Backdrop erscheint mit 0.15s
|
||||
- **Slide In:** Modal gleitet von oben mit 0.2s
|
||||
- **Loading Spinner:** Rotation Animation
|
||||
|
||||
## Integration in Layout
|
||||
|
||||
Typischerweise wird die CommandBar im App-Layout integriert:
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/(app)/+layout.svelte -->
|
||||
<script lang="ts">
|
||||
import { CommandBar } from '@mana/shared-ui';
|
||||
|
||||
let commandBarOpen = $state(false);
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
// Cmd/Ctrl+K funktioniert auch in Input-Feldern
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
|
||||
event.preventDefault();
|
||||
commandBarOpen = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<!-- Haupt-Layout -->
|
||||
<PillNavigation ... />
|
||||
|
||||
<main>
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<!-- Command Bar (global) -->
|
||||
<CommandBar
|
||||
bind:open={commandBarOpen}
|
||||
onClose={() => (commandBarOpen = false)}
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
{quickActions}
|
||||
/>
|
||||
```
|
||||
|
||||
## Dateien
|
||||
|
||||
### @mana/shared-ui
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|--------------|
|
||||
| `src/command-bar/CommandBar.svelte` | Hauptkomponente |
|
||||
| `src/command-bar/index.ts` | Exports |
|
||||
| `src/index.ts` | Package-Export |
|
||||
|
||||
## Vorteile
|
||||
|
||||
- **Einheitliche UX:** Gleiche Interaktion in allen Apps
|
||||
- **Schnelle Navigation:** Sofortiger Zugriff auf häufige Aktionen
|
||||
- **Tastatur-fokussiert:** Optimiert für Power-User
|
||||
- **Flexibel:** App-spezifische Such-Logik und Quick Actions
|
||||
- **Responsive:** Funktioniert auf Desktop und Mobile
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Such-Performance:** Limit auf 10 Ergebnisse setzen
|
||||
2. **Quick Actions:** Maximal 4-5 Aktionen für Übersichtlichkeit
|
||||
3. **Sinnvolle Shortcuts:** Mnemonische Kürzel (N=Neu, S=Settings)
|
||||
4. **Gute Subtitles:** Zusätzliche Info für eindeutige Identifikation
|
||||
5. **Keyboard First:** Cmd+K sollte immer funktionieren, auch in Input-Feldern
|
||||
Loading…
Add table
Add a link
Reference in a new issue