Abwägung an der Quelle statt nach Gefühl. Das Zielsystem nutzt beide Muster, aber für verschiedene Aufgaben: - Innere Leiste (SidebarIconsLayout, 17 Aufrufstellen): Arbeitsbereiche mit Geschwisterseiten — Leads, Termine, HR, Einstellungen. - Reiter (Tabs): Abschnitte innerhalb EINER Seite — Formular-Editor, Chat-Dateien, Projekt-Teilbereiche, Übergabe-Dialog. Analytics mit fünf Analyse-Seiten ist der erste Fall, strukturell dasselbe wie Leads mit acht. Drei Gründe: 1. Kohärenz: dieselbe Bedienung an derselben Stelle wie in Leads. 2. Der Platzeinwand trägt nicht. Die Leiste ist eine 32 px schmale Icon-Spalte und klappt erst bei Zeigerkontakt oder Tastaturfokus auf 191 px aus, verzögert (300 ms auf, 350 ms zu), damit sie beim Überstreichen nicht aufspringt. Die Breite brauchen die Analyse-Seiten mit ihren fünfspaltigen Kennzahlenreihen. 3. Reiter sind dort der Rückfall, nicht die Wahl: SidebarIconsLayout zeigt unterhalb sm selbst eine Reiterleiste. Auf dem Schreibtisch Reiter zu nehmen hieße, die Handy-Notlösung zur Hauptform zu machen. Der Nachbau übernimmt diesen Rückfall mit. Nachgemessen: 32 px eingeklappt, 191 px bei Zeigerkontakt, aktiver Eintrag hervorgehoben, aria-current gesetzt. verify-layout weiterhin grün. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
109 lines
3.7 KiB
TypeScript
109 lines
3.7 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.
|
|
*/
|
|
|
|
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. */}
|
|
<div className="w-full border-b border-solid border-border-default bg-white px-4 py-2 sm:hidden">
|
|
<Tabs>
|
|
{ANALYSE_SEITEN.map((seite) => (
|
|
<Link key={seite.id} href={seite.href} className="contents no-underline">
|
|
<Tabs.Item active={seite.id === aktiv}>{seite.label}</Tabs.Item>
|
|
</Link>
|
|
))}
|
|
</Tabs>
|
|
</div>
|
|
|
|
<div className="w-full min-w-0 overflow-x-clip">{children}</div>
|
|
</div>
|
|
);
|
|
}
|