Files
Dashboard/features/dashboard/components/layout/AnalyticsInnerNav.tsx
T
dejanandClaude Opus 5 b3d71c1f19 docs: Rangfolge beim Einbau festhalten — HiveGrid gewinnt
Klarstellung, damit beim Port keine globalen Regeln aus diesem Repo in die
Software wandern.

Verbindlich ist aus diesem Repo nur das Analytics-Eigene und das Fachliche:
Kennzahl-Definitionen, Diagrammregeln, Verhalten — und die Navigationsstruktur
als Verabredung (zwei Einträge in der Seitenleiste, Analytics mit innerer
Leiste).

Nicht verbindlich ist alles Globale: Schrift, Schriftgrößen, Abstände, Farben,
Radien, Control-Höhen, App-Shell, Kopfzeile, i18n, Formatierung. Das kommt
ausschließlich aus HiveGrid. Dass dieser Prototyp dieselbe Schrift, dieselbe
Seitenleisten-Komponente und dieselben Token benutzt, dient allein der
Lesbarkeit neben der Software — es ist keine Vorlage, an der sich die Software
auszurichten hätte. Weicht ein Wert hier von HiveGrid ab, ist der Wert hier
falsch.

Ebenso festgehalten, was Gerüst ist und wegfällt: AppShell, unsere Kopie der
Seitenleiste, PageHeader, die Kopfzeile, die Routen unter app/ und die
Dashboard-Platzhalterseite. Der Nachbau der inneren Leiste wird nicht portiert;
in HiveGrid entsteht sie über das dortige SidebarIconsLayout.

Vermerkt in ZIELSYSTEM.md (neuer Abschnitt 0 mit Zuständigkeitstabelle), im
Kopf von HANDOVER.md und README.md, bei der Struktur in NAVIGATION.md, als
Vorbemerkung zu den bewussten Abweichungen in LEITPLANKEN.md — und als
Kommentarblock in AppShell.tsx und AnalyticsInnerNav.tsx, weil beim Port oft
zuerst der Code gelesen wird.

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

119 lines
4.5 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. */}
<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>
);
}