feat(nav): innere Leiste statt Reiter im Analytics-Bereich

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
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-29 20:42:41 +02:00
1 parent aa86dfe44d
commit 466c079644
4 files changed
+153 -27

No files matched your search

+29 -4
View File
@@ -20,7 +20,7 @@ Gerendert wird mit derselben Komponente wie in der Software
derselben Schrift (Instrument Sans, selbst gehostet aus denselben Dateien).
**Analytics ist ein einzelner Menüpunkt**, kein aufklappbarer Bereich. Die fünf
Analyse-Seiten sind darin über **Reiter** erreichbar:
Analyse-Seiten sind darin über eine **innere Leiste** erreichbar:
```
/analytics Übersicht
@@ -31,9 +31,34 @@ Analyse-Seiten sind darin über **Reiter** erreichbar:
```
Zwei Navigationsebenen für dieselbe Sache — Unterpunkte in der Seitenleiste
*und* Reiter — würden nur konkurrieren. Die Reiter folgen dem Hausmuster aus
`SettingsHubShell.tsx` im Zielsystem: ein `Link` um ein `Tabs.Item`, damit jeder
Reiter eine echte Adresse hat und mittelklickbar bleibt.
*und* eine innere Leiste — würden nur konkurrieren.
### Warum innere Leiste und nicht Reiter
Beides gibt es im Zielsystem, aber für verschiedene Aufgaben:
| | innere Leiste (`SidebarIconsLayout`) | Reiter (`Tabs`) |
|---|---|---|
| Wofür | Arbeitsbereich mit **Geschwisterseiten** | Abschnitte **innerhalb einer Seite** |
| Beispiele | Leads, Termine, HR, Einstellungen — 17 Aufrufstellen | Formular-Editor, Chat-Dateien, Projekt-Teilbereiche, Übergabe-Dialog |
Analytics mit fünf Analyse-Seiten ist der erste Fall — strukturell dasselbe wie
Leads mit seinen acht. Drei Gründe geben den Ausschlag:
1. **Kohärenz.** Wer in der Software von Leads nach Analytics wechselt, findet
dieselbe Bedienung an derselben Stelle.
2. **Der Platzeinwand trägt nicht.** Die Leiste ist standardmäßig eine 32 px
schmale Icon-Spalte und klappt erst bei Zeigerkontakt oder Tastaturfokus auf
Beschriftungen aus (verzögert, damit sie beim Überstreichen nicht
aufspringt). Sie kostet also kaum Breite — und die brauchen die
Analyse-Seiten mit ihren fünfspaltigen Kennzahlenreihen und breiten Tabellen.
3. **Reiter sind dort der Rückfall, nicht die Wahl.** `SidebarIconsLayout`
zeigt unterhalb `sm` selbst eine Reiterleiste. Auf dem Schreibtisch Reiter zu
verwenden hieße, die Handy-Notlösung zur Hauptform zu machen. Unser Nachbau
übernimmt diesen Rückfall mit.
Ein weiterer Vorteil: Die Leiste wächst. Käme eine sechste Analyse-Seite dazu,
ist das eine Zeile mehr — ein Reiterband würde eng.
„Dashboard" führt hier nirgendwohin und meldet das beim Klick — es ist ein
eigenes Feature, siehe [SCOPE-ANALYTICS-DASHBOARD.md](SCOPE-ANALYTICS-DASHBOARD.md).
@@ -0,0 +1,108 @@
"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>
);
}
@@ -1,9 +1,8 @@
"use client";
import React from "react";
import Link from "next/link";
import { Tabs } from "@/ui/components/Tabs";
import { PageHeader } from "./PageHeader";
import { AnalyticsInnerNav } from "./AnalyticsInnerNav";
import { AnalyticsToolbar } from "../filters/AnalyticsToolbar";
import { ANALYSE_SEITEN } from "./navigation";
import type { SeitenId } from "../../data/filtering";
@@ -16,10 +15,12 @@ import type { FilterOptionen } from "../../view-models/filter-optionen";
* sind darin über Reiter erreichbar und tauchen nicht als Unterpunkte in der
* Navigation auf — sonst konkurrieren zwei Navigationsebenen um dieselbe Sache.
*
* Die Reiter folgen dem Hausmuster aus `SettingsHubShell.tsx` im Zielsystem:
* ein `Link` um ein `Tabs.Item`. Damit ist jeder Reiter eine echte Navigation —
* mittelklickbar, kopierbar, mit eigener Adresse — und kein Klick-Handler, der
* so tut als ob.
* Die Unter-Navigation ist eine **innere Leiste**, kein Reiterband: Das
* Zielsystem benutzt für Arbeitsbereiche mit Geschwisterseiten durchgängig
* `SidebarIconsLayout` (Leads, Termine, HR, Einstellungen). Reiter sind dort
* für Abschnitte *innerhalb einer Seite* reserviert — und sind zugleich der
* Rückfall der inneren Leiste auf schmalen Fenstern. Begründung ausführlich
* in docs/NAVIGATION.md.
*
* Kopfzeile und Filterleiste liegen hier und nicht in den Seiten: Sie sind für
* alle fünf gleich, und der Filterzustand gilt bereichsweit.
@@ -36,25 +37,10 @@ export function AnalyticsShell({
const seite = ANALYSE_SEITEN.find((s) => s.id === aktiv) ?? ANALYSE_SEITEN[0];
return (
<>
<AnalyticsInnerNav aktiv={aktiv}>
<PageHeader titel={seite.label} beschreibung={seite.beschreibung} />
{/* Die Subframe-Tabs strecken ihre Einträge über die volle Breite. Auf
einem breiten Analyse-Bildschirm reißt das die Beschriftungen weit
auseinander, deshalb eine Maximalbreite — dasselbe Mittel nutzt das
Zielsystem in LeadSettingsPage mit `min-w-[720px]`. */}
<div className="flex w-full flex-col px-6 pb-1">
<Tabs className="max-w-[760px]">
{ANALYSE_SEITEN.map((s) => (
<Link key={s.id} href={s.href} className="contents no-underline">
<Tabs.Item active={s.id === aktiv}>{s.label}</Tabs.Item>
</Link>
))}
</Tabs>
</div>
<AnalyticsToolbar seite={aktiv} optionen={optionen} />
{children}
</>
</AnalyticsInnerNav>
);
}
@@ -87,35 +87,42 @@ export interface AnalyseSeite {
label: string;
href: string;
beschreibung: string;
/** Icon-Name aus @subframe/core (ohne das Präfix "Feather"). */
icon: string;
}
export const ANALYSE_SEITEN: AnalyseSeite[] = [
{
id: "uebersicht",
icon: "LayoutDashboard",
label: "Übersicht",
href: "/analytics",
beschreibung: "Die Kennzahlen, nach denen der Betrieb gesteuert wird — Umsatz, Auslastung, offene Posten und Handlungsbedarf.",
},
{
id: "finanzen",
icon: "Euro",
label: "Finanzen",
href: "/analytics/finanzen",
beschreibung: "Umsatz, Zahlungseingang und offene Forderungen — die Sicht der Buchhaltung auf den Betrieb.",
},
{
id: "auftraege",
icon: "ClipboardList",
label: "Aufträge",
href: "/analytics/auftraege",
beschreibung: "Von der Anfrage bis zur Abnahme — wo die Vorgänge stehen, was gebunden ist und was über Termin läuft.",
},
{
id: "kunden",
icon: "Users",
label: "Kunden",
href: "/analytics/kunden",
beschreibung: "Woher Kunden kommen, wie viel sie bringen und wo Nachfassen lohnt.",
},
{
id: "team",
icon: "HardHat",
label: "Team",
href: "/analytics/team",
beschreibung: "Auslastung, produktive Stunden und Abschlüsse der Monteure — die Kennzahl hinter der Terminplanung.",