Files
Dashboard/features/dashboard/components/layout/AnalyticsInnerNav.tsx
T
dejanandClaude Opus 5 4cb0b5c799 feat(responsive): Breakpoints für 375–1920 px
Der Prototyp war auf Schreibtischbreiten gebaut. Bei 375 px lief die Seite um
306 px seitlich über, und dem Inhalt blieben 135 px, weil die Seitenleiste ihre
vollen 240 px behielt.

Seitenleiste (an HiveGrid abgeglichen, nicht erfunden):
- Unterhalb 768 px faehrt sie als Schublade ueber den Inhalt. Derselbe
  Breakpoint, den die Software als MOBILE_BREAKPOINT fuehrt.
- Hamburger links in der Kopfzeile, FeatherMenu, 32x32, md:hidden, vor dem
  Breadcrumb — wie SubframeAppShell ihn rendert.
- Abdeckung dahinter, Escape schliesst, Fokus wandert beim Oeffnen auf den
  ersten Eintrag und kehrt beim Schliessen zum Ausloeser zurueck.
- Fokusfalle mit Tab und Umschalt+Tab: Der Rahmen traegt role=dialog und
  aria-modal, ohne Falle waere das gegenueber einem Screenreader unwahr.
- Der Einklapp-Knopf ist unterhalb md ausgeblendet. Er ragt bauartbedingt
  17 px nach rechts heraus und blieb sonst als angeschnittener, wirkungsloser
  Rest bei x = -16 am Bildrand stehen — der gemeldete Fehler.

Inhalte: Raster einspaltig ab 375 px, Seitenkopf mit Aktionen unter dem Titel
und Icon-only, Zeitraum-Segment und Reiterstreifen waagerecht scrollend statt
umbrechend (ein Segment ueber zwei Zeilen liest sich als zwei Steuerelemente),
Filter-Panel auf volle Zeilenbreite, Tabellen mit eigenem Scrollbereich.

Zwei abgeschnittene Fehler mitbehoben, die der Prueferer nicht sieht:
- KPI-Label lief bei 1280 px um 5 px aus seinem Kasten; bricht jetzt um. Die
  Karte wird dadurch eine Zeile hoeher — der Preis fuer eine vollstaendig
  lesbare Beschriftung.
- Randmarken der Auslastungsachse ragten je rund 10 px ueber die Spur. In sehr
  schmalen Karten entfaellt die Achse ganz; dort bleibt nur rund 40 px Spur,
  auf der selbst zwei Marken aneinanderstossen.

verify-layout deckt jetzt 375-1920 px ab (vorher ab 1024) und meldet zusaetzlich
Seitenueberlauf: ein waagerechter Scrollbalken auf Dokumentebene ist immer ein
Fehler, nie ein Kompromiss. 7 Seiten x 9 Breiten gruen.

Ab 1024 px ist der abgenommene Stand unveraendert — numerisch geprueft ueber die
Bounding-Boxen aller Karten, Raster, Tabellen und Kopfelemente.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
2026-08-29 21:46:51 +02:00

125 lines
5.0 KiB
TypeScript

"use client";
import React from "react";
import Link from "next/link";
import { SidebarInside } from "@/ui/components/SidebarInside";
import { Tabs } from "@/ui/components/Tabs";
import { FeatherIcon } from "./FeatherIcon";
import { ANALYSE_SEITEN } from "./navigation";
import type { SeitenId } from "../../data/filtering";
/**
* Innere Navigation des Analytics-Bereichs.
*
* Nachbau von `SidebarIconsLayout` aus dem Zielsystem — der Leiste, die dort
* jeder Arbeitsbereich mit Geschwisterseiten benutzt (Leads, Termine, HR,
* Einstellungen; 17 Aufrufstellen). Analytics mit fünf Analyse-Seiten ist
* derselbe Fall.
*
* Verhalten wie dort: eine schmale Icon-Leiste, die bei Zeigerkontakt oder
* Tastaturfokus auf Beschriftungen aufklappt — verzögert, damit sie beim
* bloßen Überstreichen nicht aufspringt. Unterhalb `sm` tritt an ihre Stelle
* eine Reiterleiste; auch das ist aus dem Original übernommen.
*
* ────────────────────────────────────────────────────────────────────────────
* BEIM EINBAU: Dieser Nachbau wird NICHT portiert.
*
* Verbindlich ist die Verabredung — Analytics bekommt eine innere Leiste mit
* den fünf Analyse-Seiten. Gebaut wird sie in HiveGrid mit dem dortigen
* `SidebarIconsLayout`, das genau diesen Zweck hat (Leads, Termine, HR,
* Einstellungen nutzen es bereits). Diese Datei beschreibt das Ziel, sie ist
* nicht das Bauteil. Rangfolge: docs/ZIELSYSTEM.md Abschnitt 0.
* ────────────────────────────────────────────────────────────────────────────
*/
const OEFFNEN_MS = 300;
const SCHLIESSEN_MS = 350;
export function AnalyticsInnerNav({
aktiv,
children,
}: {
aktiv: SeitenId;
children: React.ReactNode;
}) {
const [zeigerDrauf, setZeigerDrauf] = React.useState(false);
const [fokusInnen, setFokusInnen] = React.useState(false);
const oeffnen = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const schliessen = React.useRef<ReturnType<typeof setTimeout> | null>(null);
React.useEffect(
() => () => {
if (oeffnen.current) clearTimeout(oeffnen.current);
if (schliessen.current) clearTimeout(schliessen.current);
},
[]
);
const ausgeklappt = zeigerDrauf || fokusInnen;
return (
<div className="sm:flex sm:flex-row">
<div
className="relative hidden sm:sticky sm:top-14 sm:flex sm:self-start"
onMouseEnter={() => {
if (schliessen.current) clearTimeout(schliessen.current);
oeffnen.current = setTimeout(() => setZeigerDrauf(true), OEFFNEN_MS);
}}
onMouseLeave={() => {
if (oeffnen.current) clearTimeout(oeffnen.current);
schliessen.current = setTimeout(() => setZeigerDrauf(false), SCHLIESSEN_MS);
}}
onFocusCapture={() => setFokusInnen(true)}
onBlurCapture={(event) => {
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
setFokusInnen(false);
}
}}
>
<SidebarInside
visible
variant={ausgeklappt ? "open" : "closed"}
className="min-h-[calc(100svh-3.5rem)]"
>
{ANALYSE_SEITEN.map((seite) => (
<Link
key={seite.id}
href={seite.href}
title={seite.label}
aria-current={seite.id === aktiv ? "page" : undefined}
className="flex w-full justify-start no-underline"
>
<SidebarInside.NavItem
icon={<FeatherIcon name={seite.icon} />}
selected={seite.id === aktiv}
variant={ausgeklappt ? "icon-and-text" : "icon-only"}
>
{seite.label}
</SidebarInside.NavItem>
</Link>
))}
</SidebarInside>
</div>
{/* Schmale Fenster: Reiter statt Leiste — wie im Original.
Waagerecht scrollend statt umbrechend: Fünf Reiter brauchen mindestens
425 px, bei 375 px lief die ganze Seite dadurch seitlich über. Über
zwei Zeilen gebrochen liest sich ein Reiterband als zwei Steuerelemente
— dasselbe Mittel wie beim Zeitraum-Segment in der Filterleiste. */}
<div className="dv-scroll-x w-full min-w-0 overflow-x-auto border-b border-solid border-border-default bg-white px-4 py-2 sm:hidden">
<Tabs className="w-max min-w-full">
{ANALYSE_SEITEN.map((seite) => (
<Link key={seite.id} href={seite.href} className="contents no-underline">
<Tabs.Item active={seite.id === aktiv} className="whitespace-nowrap">
{seite.label}
</Tabs.Item>
</Link>
))}
</Tabs>
</div>
<div className="w-full min-w-0 overflow-x-clip">{children}</div>
</div>
);
}