feat(frontend): Profil-Topbar — Avatar, Theme-Schalter und Logout nach oben rechts

Sidebar-Bottom-Bereich (Profil, Abmelden, Theme-Toggle) vollstaendig entfernt
und durch eine sticky Topbar im Hauptinhaltsbereich ersetzt:
- Theme-Schalter: kompakte Icon-Button-Gruppe (☀ hell / ☾ dunkel / ⚙ system)
- Benutzer-Avatar + Name klickbar → /profile
- Logout-Icon-Button (SVG, Tooltip "Abmelden")
Sidebar zeigt nur noch Logo, Navigation und Admin-Link.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-13 13:46:33 +01:00
parent 0f63c28110
commit 5f50ba1315
3 changed files with 178 additions and 241 deletions

View file

@ -6,6 +6,19 @@
---
### Aenderungen 2026-03-13 (7): Profil-Bereich nach oben rechts verschoben (Topbar)
#### Frontend
- `shell/AppLayout.tsx` — Profil-/Logout-/Theme-Bereich aus der Sidebar entfernt und in einen neuen `<header className={styles.topbar}>` am Anfang des `<main>`-Bereichs verschoben:
- Theme-Schalter: 3 kompakte Icon-Buttons (☀ ☾ ⚙) als Gruppe, aktiver Modus hervorgehoben
- Benutzer-Profil: Avatar + Name, klickbar → `/profile`
- Logout: Icon-Button (Tuer-Pfeil-SVG) mit Tooltip "Abmelden"
- `<Outlet />` nun in `<div className={styles.mainContent}>` eingebettet (eigene Padding-Klasse)
- Sidebar hat nur noch: Logo/Brand + Navigation + Admin-Link (deutlich aufgeraeumt)
- `shell/AppLayout.module.css` — Theme-Toggle- und UserInfo-Abschnitte entfernt (ca. 150 Zeilen weniger); `.main` auf `display:flex; flex-direction:column` umgestellt; neues `.mainContent { padding:2rem }`; neue Klassen: `.topbar`, `.topbarThemeGroup`, `.topbarThemeBtn(Active)`, `.topbarUser`, `.topbarUserName`, `.topbarIconBtn`
---
### Aenderungen 2026-03-13 (6): Dashboard Home-Tab — Analoguhr, 3-Tage-Wetter, Spruch, kompakte Widgets
#### Frontend

View file

@ -195,161 +195,110 @@
background: rgba(96, 165, 250, 0.15);
}
/* ===== Theme Toggle ===== */
.themeToggle {
padding: 0.5rem 0.75rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebarCollapsed .themeToggle {
padding: 0.5rem 0;
display: flex;
justify-content: center;
}
.themeBtn {
background: rgba(255, 255, 255, 0.08);
border: none;
color: rgba(255, 255, 255, 0.7);
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.875rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.themeBtn:hover {
background: rgba(255, 255, 255, 0.15);
color: white;
}
.themeBtnGroup {
display: flex;
gap: 2px;
background: rgba(255, 255, 255, 0.05);
border-radius: var(--radius-sm);
padding: 2px;
}
.themeOption {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
padding: 0.3rem 0.25rem;
background: none;
border: none;
color: rgba(255, 255, 255, 0.5);
font-size: 0.6875rem;
font-weight: 500;
border-radius: 3px;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.themeOption:hover {
color: rgba(255, 255, 255, 0.8);
}
.themeOptionActive {
background: rgba(255, 255, 255, 0.15);
color: white;
}
.themeIcon {
font-size: 0.75rem;
line-height: 1;
}
/* ===== User Info ===== */
.userInfo {
padding: 1rem 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebarCollapsed .userInfo {
padding: 0.75rem 0;
display: flex;
justify-content: center;
}
.userProfile {
cursor: pointer;
padding: 0.375rem;
margin: -0.375rem;
border-radius: var(--radius-sm);
transition: background 0.15s;
}
.userProfile:hover {
background: rgba(255, 255, 255, 0.1);
}
.userProfileInner {
display: flex;
align-items: center;
gap: 0.625rem;
}
.userDetails {
min-width: 0;
}
.userName {
font-size: 0.875rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.userEmail {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
margin-top: 0.125rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.profileHint {
font-size: 0.6875rem;
color: rgba(255, 255, 255, 0.35);
margin-top: 0.375rem;
transition: color 0.15s;
}
.userProfile:hover .profileHint {
color: rgba(255, 255, 255, 0.7);
}
.logoutBtn {
margin-top: 0.75rem;
width: 100%;
padding: 0.5rem;
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: var(--radius-sm);
font-size: 0.8125rem;
transition: all 0.15s;
}
.logoutBtn:hover {
background: rgba(255, 255, 255, 0.15);
color: white;
}
/* ===== Main Content ===== */
.main {
flex: 1;
margin-left: var(--sidebar-width);
padding: 2rem;
transition: margin-left 0.2s ease;
display: flex;
flex-direction: column;
}
.mainContent {
flex: 1;
padding: 2rem;
}
/* ===== Topbar (oben rechts: Profil + Logout + Modiwahl) ===== */
.topbar {
position: sticky;
top: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.625rem;
padding: 0.5rem 1.5rem;
background: var(--color-bg);
border-bottom: 1px solid var(--color-border);
min-height: 52px;
}
/* Theme-Schalter Gruppe */
.topbarThemeGroup {
display: flex;
gap: 2px;
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
padding: 2px;
}
.topbarThemeBtn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
border-radius: 3px;
color: var(--color-text-muted);
font-size: 0.8125rem;
cursor: pointer;
transition: all 0.15s;
}
.topbarThemeBtn:hover {
color: var(--color-text);
background: var(--color-bg-card);
}
.topbarThemeBtnActive {
background: var(--color-primary);
color: white !important;
}
/* Benutzer-Profil */
.topbarUser {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.625rem 0.25rem 0.25rem;
border-radius: var(--radius-md);
border: 1px solid transparent;
cursor: pointer;
transition: all 0.15s;
}
.topbarUser:hover {
background: var(--color-bg-card);
border-color: var(--color-border);
}
.topbarUserName {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text);
white-space: nowrap;
}
/* Abmelden-Button */
.topbarIconBtn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.15s;
}
.topbarIconBtn:hover {
background: var(--color-bg-card);
color: var(--color-text);
}

View file

@ -572,94 +572,6 @@ export function AppLayout() {
</div>
)}
<div className={styles.userInfo}>
{collapsed ? (
<div
className={styles.userProfile}
onClick={() => navigate('/profile')}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') navigate('/profile');
}}
title={`${user?.firstName} ${user?.lastName}`}
>
<UserAvatar
firstName={user?.firstName ?? ''}
lastName={user?.lastName ?? ''}
avatar={user?.avatar}
size={32}
/>
</div>
) : (
<>
<div
className={styles.userProfile}
onClick={() => navigate('/profile')}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ')
navigate('/profile');
}}
>
<div className={styles.userProfileInner}>
<UserAvatar
firstName={user?.firstName ?? ''}
lastName={user?.lastName ?? ''}
avatar={user?.avatar}
size={36}
/>
<div className={styles.userDetails}>
<div className={styles.userName}>
{user?.firstName} {user?.lastName}
</div>
<div className={styles.userEmail}>{user?.email}</div>
</div>
</div>
<div className={styles.profileHint}>Zum Profil &rarr;</div>
</div>
<button className={styles.logoutBtn} onClick={handleLogout}>
Abmelden
</button>
</>
)}
</div>
{/* Theme Toggle (unter dem Profil) */}
<div className={styles.themeToggle}>
{collapsed ? (
<button
className={styles.themeBtn}
onClick={() => {
const next =
mode === 'light'
? 'dark'
: mode === 'dark'
? 'system'
: 'light';
setMode(next);
}}
title={`Theme: ${THEME_OPTIONS.find((o) => o.value === mode)?.label}`}
>
{THEME_OPTIONS.find((o) => o.value === mode)?.icon}
</button>
) : (
<div className={styles.themeBtnGroup}>
{THEME_OPTIONS.map((opt) => (
<button
key={opt.value}
className={`${styles.themeOption} ${mode === opt.value ? styles.themeOptionActive : ''}`}
onClick={() => setMode(opt.value)}
title={opt.label}
>
<span className={styles.themeIcon}>{opt.icon}</span>
{opt.label}
</button>
))}
</div>
)}
</div>
</aside>
{/* Main Content */}
@ -669,7 +581,70 @@ export function AppLayout() {
marginLeft: collapsed ? 60 : undefined,
}}
>
{/* Topbar: Profil + Logout + Modiwahl oben rechts */}
<header className={styles.topbar}>
{/* Theme-Schalter (☀ ☾ ⚙) */}
<div className={styles.topbarThemeGroup}>
{THEME_OPTIONS.map((opt) => (
<button
key={opt.value}
className={`${styles.topbarThemeBtn} ${mode === opt.value ? styles.topbarThemeBtnActive : ''}`}
onClick={() => setMode(opt.value)}
title={opt.label}
>
{opt.icon}
</button>
))}
</div>
{/* Benutzer-Profil (klickbar → /profile) */}
<div
className={styles.topbarUser}
onClick={() => navigate('/profile')}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') navigate('/profile');
}}
title="Profil bearbeiten"
>
<UserAvatar
firstName={user?.firstName ?? ''}
lastName={user?.lastName ?? ''}
avatar={user?.avatar}
size={32}
/>
<span className={styles.topbarUserName}>
{user?.firstName} {user?.lastName}
</span>
</div>
{/* Abmelden-Icon */}
<button
className={styles.topbarIconBtn}
onClick={handleLogout}
title="Abmelden"
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M10 3h3a1 1 0 011 1v8a1 1 0 01-1 1h-3" />
<path d="M7 11l3-3-3-3" />
<path d="M10 8H3" />
</svg>
</button>
</header>
<div className={styles.mainContent}>
<Outlet />
</div>
</main>
</div>
);