From 466c0796449c3bb656558d3b13ba03caf1453632 Mon Sep 17 00:00:00 2001 From: DeeCee03 Date: Sat, 29 Aug 2026 20:42:41 +0200 Subject: [PATCH] feat(nav): innere Leiste statt Reiter im Analytics-Bereich MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX --- docs/NAVIGATION.md | 33 +++++- .../components/layout/AnalyticsInnerNav.tsx | 108 ++++++++++++++++++ .../components/layout/AnalyticsShell.tsx | 32 ++---- .../dashboard/components/layout/navigation.ts | 7 ++ 4 files changed, 153 insertions(+), 27 deletions(-) create mode 100644 features/dashboard/components/layout/AnalyticsInnerNav.tsx diff --git a/docs/NAVIGATION.md b/docs/NAVIGATION.md index 6eb115c..380bc3d 100644 --- a/docs/NAVIGATION.md +++ b/docs/NAVIGATION.md @@ -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). diff --git a/features/dashboard/components/layout/AnalyticsInnerNav.tsx b/features/dashboard/components/layout/AnalyticsInnerNav.tsx new file mode 100644 index 0000000..471c7d9 --- /dev/null +++ b/features/dashboard/components/layout/AnalyticsInnerNav.tsx @@ -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 | null>(null); + const schliessen = React.useRef | null>(null); + + React.useEffect( + () => () => { + if (oeffnen.current) clearTimeout(oeffnen.current); + if (schliessen.current) clearTimeout(schliessen.current); + }, + [] + ); + + const ausgeklappt = zeigerDrauf || fokusInnen; + + return ( +
+
{ + 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); + } + }} + > + + {ANALYSE_SEITEN.map((seite) => ( + + } + selected={seite.id === aktiv} + variant={ausgeklappt ? "icon-and-text" : "icon-only"} + > + {seite.label} + + + ))} + +
+ + {/* Schmale Fenster: Reiter statt Leiste — wie im Original. */} +
+ + {ANALYSE_SEITEN.map((seite) => ( + + {seite.label} + + ))} + +
+ +
{children}
+
+ ); +} diff --git a/features/dashboard/components/layout/AnalyticsShell.tsx b/features/dashboard/components/layout/AnalyticsShell.tsx index 50166ea..ee4623a 100644 --- a/features/dashboard/components/layout/AnalyticsShell.tsx +++ b/features/dashboard/components/layout/AnalyticsShell.tsx @@ -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 ( - <> + - - {/* 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]`. */} -
- - {ANALYSE_SEITEN.map((s) => ( - - {s.label} - - ))} - -
- {children} - +
); } diff --git a/features/dashboard/components/layout/navigation.ts b/features/dashboard/components/layout/navigation.ts index 93299a8..d45ce89 100644 --- a/features/dashboard/components/layout/navigation.ts +++ b/features/dashboard/components/layout/navigation.ts @@ -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.",