feat(260924-h7x): Dashboard-Reiter als Umschalter in der Kopfzeile

- Kopfzeile bekommt Einhaengepunkt header-center-slot; auf / entfaellt der Text Startseite
- DashboardTabs rendert per createPortal dorthin, eigene Zeile ueber dem Raster entfaellt
- eingelassene Spur mit erhabenem aktivem Reiter, waagrecht scrollbar mit weicher Randausblendung
- role=tablist/tab, Pfeiltasten/Pos1/Ende, sichtbarer Fokusring, aktiver Reiter wird ins Bild gescrollt
- Plus-Knopf ausserhalb der Spur, Loeschdialog am Dokumentkoerper, Ziehhinweis als sr-only/Tooltip

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-24 12:38:08 +02:00
parent 57c338fe53
commit 7416a925a6
7 changed files with 487 additions and 201 deletions
@@ -0,0 +1,6 @@
/**
* Kennung des Einhaengepunkts in der Mitte der Kopfzeile (quick-260924-h7x).
* Die Kopfzeile rendert ein leeres Element mit dieser Kennung; Seiten
* zeichnen per `createPortal` hinein (heute: die Dashboard-Reiter).
*/
export const HEADER_CENTER_SLOT_ID = 'header-center-slot';
@@ -150,3 +150,31 @@ describe('Header — Sitzungswaechter (quick-260917-gyd)', () => {
).toHaveTextContent('?');
});
});
describe('Header — Mitte der Kopfzeile (quick-260924-h7x)', () => {
it('auf der Startseite: kein Text „Startseite“, aber der leere Einhaengepunkt fuer die Reiter', async () => {
mockPathname = '/';
vi.stubGlobal('location', { href: '', pathname: '/', search: '' });
fetchSessionState.mockResolvedValue({ status: 'unavailable' });
await renderHeader();
expect(screen.queryByText('Startseite')).not.toBeInTheDocument();
const slot = document.getElementById('header-center-slot');
expect(slot).not.toBeNull();
expect(slot).toBeEmptyDOMElement();
await waitFor(() => expect(fetchSessionState).toHaveBeenCalled());
});
it('auf anderen Seiten: „Startseite“ bleibt wie bisher', async () => {
mockPathname = '/modules/proxmox';
vi.stubGlobal('location', { href: '', pathname: '/modules/proxmox', search: '' });
fetchSessionState.mockResolvedValue({ status: 'unavailable' });
await renderHeader();
expect(screen.getByText('Startseite')).toBeInTheDocument();
expect(document.getElementById('header-center-slot')).not.toBeNull();
await waitFor(() => expect(fetchSessionState).toHaveBeenCalled());
});
});
+16 -7
View File
@@ -10,6 +10,7 @@ import { fetchSessionState, logout } from '@/lib/auth-actions';
import { buildNextParam } from '@/lib/safe-next';
import { ThemeToggle } from '@/components/theme-toggle';
import { BugReportButton } from '@/components/bug-report/bug-report-button';
import { HEADER_CENTER_SLOT_ID } from '@/components/layout/header-slot';
import { usePathname } from 'next/navigation';
export function Header() {
@@ -97,7 +98,7 @@ export function Header() {
return (
<header className="sticky top-0 z-50 flex h-[var(--header-height)] items-center border-b border-border bg-background px-4 shadow-sm">
{/* Left: Logo + Mobile hamburger */}
<div className="flex items-center gap-3">
<div className="flex shrink-0 items-center gap-3">
<button
type="button"
onClick={() => setMobileOpen(true)}
@@ -132,15 +133,23 @@ export function Header() {
/>
</div>
{/* Center: Breadcrumb / page title */}
<div className="flex-1 px-4 text-center">
<span className="text-sm text-muted-foreground truncate">
{tHeader('breadcrumb.home')}
</span>
{/* Mitte: Einhaengepunkt fuer die Dashboard-Reiter (quick-260924-h7x).
`DashboardTabs` rendert per Portal hierher; der Punkt muss deshalb
im React-Baum der Kopfzeile LEER bleiben. Auf der Startseite
entfaellt der Text „Startseite“, auf allen anderen Seiten bleibt
er wie bisher. `min-w-0` laesst die Mitte schrumpfen, damit Logo
und Aktionen rechts auch bei schmalem Fenster Platz behalten. */}
<div className="flex min-w-0 flex-1 items-center justify-center px-2 text-center md:px-4">
<div id={HEADER_CENTER_SLOT_ID} className="flex min-w-0 max-w-full justify-center" />
{pathname !== '/' && (
<span className="text-sm text-muted-foreground truncate">
{tHeader('breadcrumb.home')}
</span>
)}
</div>
{/* Right: Actions */}
<div className="flex items-center gap-2">
<div className="flex shrink-0 items-center gap-2">
<BugReportButton />
<ThemeToggle />