refactor(topbar): professionelle einzeilige Kopfzeile (v1.5.3)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 31m6s
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user