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:
1 parent
aa86dfe44d
commit
466c079644
4 files changed
+153
-27
No files matched your search
+29
-4
@@ -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.",
|
||||
|
||||
Reference in new issue
Block a user