refactor(topbar): professionelle einzeilige Kopfzeile (v1.5.3)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 31m6s

Kopfzeile war überladen (~11 konkurrierende Buttons in rot/blau/amber) und die
Versionsnummer kaum lesbar. Neu wie ein Einsatz-Cockpit — ruhig, dreizonig:

- Zone 1 Identität: Logo + "LAGEPLAN" + gut lesbare Version (mono)
- Zone 2 Blickfang: aktiver Einsatz als Pille (Status-Punkt rot=Einsatz /
  blau=Übung, Einsatz-Nr mono, Titel, Ort) — mittig, flex-1, statt xl-only-Box
- Zone 3: grosse Uhr + Speichern + "+ Neu ▾" (Split-Button Einsatz/Übung) +
  Werkzeug-Menü (Export, Präsentation, Vollbild, Tag/Nacht, Einsätze
  verwalten, Schlaucheinstellungen) + Beenden + Benutzer

- Nur noch EIN Farbakzent (Status-Punkt) statt rot+blau+amber-Buttons
- 4 einzelne Icon-Buttons (Theme/Vollbild/Präsentation) → ein Zahnrad-Menü
- Zwei farbige "Neuer Einsatz/Neue Übung"-Buttons → ein "Neu"-Split-Button
- Mobile-Menü entschlackt (Erstellen liegt jetzt im Neu-Button)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-19 20:10:36 +02:00
parent 668476948e
commit 03914083df
2 changed files with 121 additions and 129 deletions

View File

@@ -23,7 +23,6 @@ import {
Save,
FolderOpen,
Download,
Clock,
Sun,
Moon,
Maximize,
@@ -45,6 +44,9 @@ import {
Lock,
Unlock,
CheckCircle2,
Plus,
ChevronDown,
Loader2,
} from 'lucide-react'
import { HoseSettingsDialog } from '@/components/dialogs/hose-settings-dialog'
import type { Project, DrawFeature, ProjectMode } from '@/types'
@@ -161,155 +163,149 @@ export function Topbar({
}
return (
<header className="h-14 md:h-16 border-b border-border bg-card flex items-center justify-between px-2 md:px-4 shrink-0 overflow-x-auto print:hidden">
<div className="flex items-center gap-1.5 md:gap-4 shrink-0">
<div className="flex items-center gap-1.5 md:gap-2">
<Logo size={32} />
<span className="font-semibold text-lg hidden md:inline">Lageplan</span>
<span className="text-[10px] text-muted-foreground hidden md:inline">v{process.env.APP_VERSION}</span>
</div>
<div className="h-6 w-px bg-border hidden md:block" />
<div className="flex items-center gap-1 md:gap-2">
<Button
data-tour="save"
variant="outline"
className="h-9 md:h-10 px-2 md:px-4 text-sm"
onClick={onSaveProject}
disabled={!project || isSaving}
title="Speichern"
>
<Save className="w-5 h-5 md:mr-1.5" />
<span className="hidden lg:inline">{isSaving ? 'Speichern...' : 'Speichern'}</span>
</Button>
<Button
variant={presentationLocked ? 'default' : 'outline'}
className={`hidden md:flex h-9 md:h-10 px-2 md:px-3 text-sm ${presentationLocked ? 'bg-amber-600 hover:bg-amber-700 text-white border-amber-600' : ''}`}
onClick={onTogglePresentationLock}
title={presentationLocked ? 'Präsentationsmodus deaktivieren' : 'Präsentationsmodus aktivieren'}
>
{presentationLocked ? <Lock className="w-5 h-5 md:mr-1" /> : <Unlock className="w-5 h-5 md:mr-1" />}
<span className="hidden lg:inline">{presentationLocked ? 'Gesperrt' : 'Frei'}</span>
</Button>
{/* Direkte Einstiegspunkte: Einsatz (rot) und Übung (blau) */}
<Button
data-tour="new-project"
onClick={() => onNewProject('EINSATZ')}
className="h-9 md:h-10 px-2 md:px-3 text-sm bg-red-600 hover:bg-red-700 text-white"
title="Neuen Einsatz erstellen"
>
<Flame className="w-5 h-5 md:mr-1" />
<span className="hidden lg:inline">Neuer Einsatz</span>
</Button>
<Button
onClick={() => onNewProject('UEBUNG')}
variant="outline"
className="hidden md:flex h-9 md:h-10 px-2 md:px-3 text-sm border-blue-500 text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-950/40"
title="Neue Übung erstellen"
>
<GraduationCap className="w-5 h-5 md:mr-1" />
<span className="hidden lg:inline">Neue Übung</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="hidden md:flex h-9 md:h-10 px-2 md:px-3 text-sm" title="Menü">
<MoreVertical className="w-5 h-5 md:mr-1" />
<span className="hidden lg:inline">Menü</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-52">
<DropdownMenuItem onClick={handleOpenLoadDialog} className="py-2.5 px-3">
<List className="w-4 h-4 mr-2" />
Einsätze verwalten
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport('png')} className="py-2.5 px-3">
<Download className="w-4 h-4 mr-2" />
Als PNG exportieren
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport('pdf')} className="py-2.5 px-3">
<Download className="w-4 h-4 mr-2" />
Als PDF exportieren
</DropdownMenuItem>
{onClearAll && (
<DropdownMenuItem onClick={onClearAll} className="py-2.5 px-3 text-destructive focus:text-destructive">
<Eraser className="w-4 h-4 mr-2" />
Zeichnung leeren
</DropdownMenuItem>
)}
{(userRole === 'SERVER_ADMIN' || userRole === 'TENANT_ADMIN') && (
<DropdownMenuItem onClick={() => window.location.href = '/admin'} className="py-2.5 px-3">
<Settings className="w-4 h-4 mr-2" />
Einstellungen
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<header className="h-14 md:h-16 border-b border-border bg-card flex items-center gap-2 md:gap-3 px-2 md:px-4 shrink-0 print:hidden relative">
{/* Zone 1 — Identität */}
<div className="flex items-center gap-2 shrink-0">
<Logo size={32} />
<div className="hidden sm:flex flex-col leading-none">
<span className="font-bold text-sm tracking-wide">LAGEPLAN</span>
<span className="text-[10px] text-muted-foreground font-mono mt-0.5">v{process.env.APP_VERSION}</span>
</div>
</div>
<div className="flex items-center gap-1 md:gap-2 shrink-0">
{/* Project info - desktop only */}
{project && (
<div className="hidden xl:flex items-center gap-2 text-sm border border-border rounded-lg px-3 py-1.5 bg-muted/30">
<div className="h-8 w-px bg-border hidden md:block shrink-0" />
{/* Zone 2 — Aktiver Einsatz (Blickfang) */}
<div className="flex-1 min-w-0 flex items-center">
{project ? (
<div className="flex items-center gap-2 min-w-0 rounded-lg border border-border bg-muted/40 pl-2.5 pr-3 py-1.5">
<span
className={`w-2.5 h-2.5 rounded-full shrink-0 ${project.mode === 'UEBUNG' ? 'bg-blue-500' : 'bg-red-500 animate-pulse'}`}
title={project.mode === 'UEBUNG' ? 'Übung' : 'Einsatz'}
/>
{(project as any).einsatzNr && (
<span className="font-mono text-xs bg-primary/10 text-primary px-1.5 py-0.5 rounded">{(project as any).einsatzNr}</span>
<span className="font-mono text-xs font-bold shrink-0">{(project as any).einsatzNr}</span>
)}
<span className="font-semibold truncate max-w-[180px]">{project.title}</span>
<span className="font-semibold text-sm truncate">{project.title}</span>
{project.location && (
<><span className="text-muted-foreground">|</span><span className="text-muted-foreground truncate max-w-[150px]">{project.location}</span></>
<span className="hidden lg:inline text-sm text-muted-foreground truncate">· {project.location}</span>
)}
</div>
) : (
<span className="text-sm text-muted-foreground hidden sm:inline">Kein Einsatz aktiv</span>
)}
</div>
{/* Clock - desktop only */}
<div className="hidden lg:flex items-center gap-2 text-sm font-medium tabular-nums">
<Clock className="w-4 h-4 text-muted-foreground" />
<span className="text-primary font-bold">{now.toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' })}</span>
</div>
{/* Desktop: individual buttons */}
<Button
variant={isDarkMode ? 'default' : 'outline'}
className="hidden md:flex h-9 px-2 gap-1.5 text-sm font-semibold"
onClick={onToggleTheme}
title={isDarkMode ? 'Tagmodus' : 'Nachtmodus'}
>
{isDarkMode ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
</Button>
{/* Zone 3 — Uhr + Aktionen */}
<div className="flex items-center gap-1 md:gap-1.5 shrink-0">
{/* Uhr — Ops-Rooms leben nach der Uhr */}
<span className="hidden sm:inline text-base md:text-lg font-bold tabular-nums mr-0.5 md:mr-1.5">
{now.toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' })}
</span>
{/* Speichern */}
<Button
data-tour="save"
variant="outline"
className="hidden md:flex h-9 px-2"
onClick={onToggleFullscreen}
title={isFullscreen ? 'Vollbild verlassen' : 'Vollbild'}
size="icon"
className="h-9 w-9 md:h-10 md:w-10 shrink-0"
onClick={onSaveProject}
disabled={!project || isSaving}
title="Speichern (Ctrl+S)"
>
{isFullscreen ? <Minimize className="w-4 h-4" /> : <Maximize className="w-4 h-4" />}
{isSaving ? <Loader2 className="w-5 h-5 animate-spin" /> : <Save className="w-5 h-5" />}
</Button>
{/* Neu — Split-Button für Einsatz/Übung (ein Akzent statt zwei farbiger Buttons) */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button data-tour="new-project" className="h-9 md:h-10 px-2 md:px-2.5 shrink-0" title="Neu erstellen">
<Plus className="w-5 h-5 lg:mr-1" />
<span className="hidden lg:inline">Neu</span>
<ChevronDown className="w-3.5 h-3.5 ml-0.5 opacity-70" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onClick={() => onNewProject('EINSATZ')} className="py-2.5">
<Flame className="w-4 h-4 mr-2 text-red-600" />
Neuer Einsatz
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onNewProject('UEBUNG')} className="py-2.5">
<GraduationCap className="w-4 h-4 mr-2 text-blue-600" />
Neue Übung
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Werkzeuge (Desktop) — sammelt alle Neben-Aktionen in einem Menü */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="hidden md:flex h-10 w-10 shrink-0" title="Werkzeuge">
<Settings className="w-5 h-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem onClick={handleOpenLoadDialog}>
<List className="w-4 h-4 mr-2" />
Einsätze verwalten
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleExport('png')}>
<Download className="w-4 h-4 mr-2" />
Als PNG exportieren
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport('pdf')}>
<Download className="w-4 h-4 mr-2" />
Als PDF exportieren
</DropdownMenuItem>
{onClearAll && (
<DropdownMenuItem onClick={onClearAll} className="text-destructive focus:text-destructive">
<Eraser className="w-4 h-4 mr-2" />
Zeichnung leeren
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
{onTogglePresentationLock && (
<DropdownMenuItem onClick={onTogglePresentationLock}>
{presentationLocked ? <Lock className="w-4 h-4 mr-2" /> : <Unlock className="w-4 h-4 mr-2" />}
{presentationLocked ? 'Präsentation: gesperrt' : 'Präsentationsmodus'}
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={onToggleFullscreen}>
{isFullscreen ? <Minimize className="w-4 h-4 mr-2" /> : <Maximize className="w-4 h-4 mr-2" />}
{isFullscreen ? 'Vollbild verlassen' : 'Vollbild'}
</DropdownMenuItem>
<DropdownMenuItem onClick={onToggleTheme}>
{isDarkMode ? <Sun className="w-4 h-4 mr-2" /> : <Moon className="w-4 h-4 mr-2" />}
{isDarkMode ? 'Tagmodus' : 'Nachtmodus'}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsHoseSettingsOpen(true)}>
<Settings className="w-4 h-4 mr-2" />
Schlaucheinstellungen
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Einsatz beenden (Desktop) */}
{project && onEndProject && (
<Button
variant="outline"
className="hidden md:flex h-9 px-2 md:px-3 border-red-300 text-red-600 hover:bg-red-50 hover:text-red-700 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-950/40"
className="hidden md:flex h-10 px-2 lg:px-3 shrink-0 border-red-300 text-red-600 hover:bg-red-50 hover:text-red-700 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-950/40"
onClick={onEndProject}
title="Einsatz speichern und schliessen"
title={project.mode === 'UEBUNG' ? 'Übung speichern und schliessen' : 'Einsatz speichern und schliessen'}
>
<CheckCircle2 className="w-4 h-4 lg:mr-1" />
<span className="hidden lg:inline">Einsatz beenden</span>
<CheckCircle2 className="w-4 h-4 xl:mr-1" />
<span className="hidden xl:inline">Beenden</span>
</Button>
)}
{/* Desktop: User menu dropdown */}
{/* Benutzer (Desktop) */}
{userName && onLogout && (
<div className="hidden md:flex items-center ml-1 pl-2 border-l border-border">
<div className="hidden md:flex items-center ml-0.5 pl-1.5 border-l border-border">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-9 px-2 gap-1.5 text-sm text-muted-foreground hover:text-foreground">
<User className="w-4 h-4" />
<span className="hidden lg:inline max-w-[100px] truncate">{userName}</span>
<Button variant="ghost" size="icon" className="h-10 w-10 text-muted-foreground hover:text-foreground" title={userName}>
<User className="w-5 h-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
@@ -366,17 +362,13 @@ export function Topbar({
</>
)}
{/* Einsatz / Übung */}
{/* Einsatz / Übung (Erstellen liegt im „Neu"-Button) */}
{project && onEndProject && (
<DropdownMenuItem onClick={onEndProject} className="text-red-600 focus:text-red-700">
<CheckCircle2 className="w-4 h-4 mr-2" />
{project.mode === 'UEBUNG' ? 'Übung beenden' : 'Einsatz beenden'}
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => onNewProject('UEBUNG')}>
<GraduationCap className="w-4 h-4 mr-2" />
Neue Übung
</DropdownMenuItem>
<DropdownMenuItem onClick={handleOpenLoadDialog}>
<List className="w-4 h-4 mr-2" />
Einsätze verwalten