diff --git a/README.md b/README.md index de4c472..9497d0c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Entwicklung** — nicht der Produktivbetrieb. ```bash npm install -npm run dev # http://localhost:4300 +npm run dev # http://localhost:4300 → leitet auf /analytics ``` --- @@ -22,7 +22,8 @@ npm run dev # http://localhost:4300 | Bereich | Inhalt | |---|---| -| **6 Seiten** | Übersicht, Finanzen, Aufträge, Kunden, Team, Komponenten | +| **5 Analyse-Seiten** | Übersicht, Finanzen, Aufträge, Kunden, Team — als Reiter im Menüpunkt **Analytics** | +| **Seitenleiste** | spiegelt die von HiveGrid Cloud (dieselbe Komponente, dieselben Gruppen) | | **26 KPIs** | vollständig definiert in [`docs/KPI-KATALOG.md`](docs/KPI-KATALOG.md) | | **9 Chart-Bausteine** | Fläche, Säulen, liegende Balken, Ring, Trichter, Auslastung, Sparkline, Legende, Leerzustand | | **Filtersystem** | Zeitraum + 5 Dimensionen, seitenabhängig relevant, in localStorage persistiert | diff --git a/app/auftraege/page.tsx b/app/analytics/auftraege/page.tsx similarity index 100% rename from app/auftraege/page.tsx rename to app/analytics/auftraege/page.tsx diff --git a/app/finanzen/page.tsx b/app/analytics/finanzen/page.tsx similarity index 100% rename from app/finanzen/page.tsx rename to app/analytics/finanzen/page.tsx diff --git a/app/kunden/page.tsx b/app/analytics/kunden/page.tsx similarity index 100% rename from app/kunden/page.tsx rename to app/analytics/kunden/page.tsx diff --git a/app/analytics/page.tsx b/app/analytics/page.tsx new file mode 100644 index 0000000..dfa0f70 --- /dev/null +++ b/app/analytics/page.tsx @@ -0,0 +1,5 @@ +import { UebersichtRoute } from "@/features/dashboard/routes/UebersichtRoute"; + +export default function Page() { + return ; +} diff --git a/app/team/page.tsx b/app/analytics/team/page.tsx similarity index 100% rename from app/team/page.tsx rename to app/analytics/team/page.tsx diff --git a/app/page.tsx b/app/page.tsx index dfa0f70..05e7de5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,11 @@ -import { UebersichtRoute } from "@/features/dashboard/routes/UebersichtRoute"; +import { redirect } from "next/navigation"; +import { ANALYTICS_START } from "@/features/dashboard/components/layout/navigation"; +/** + * Der Prototyp bildet das Analytics-Feature ab, nicht das Dashboard. + * Die Wurzel leitet deshalb auf den Analytics-Einstieg weiter — in der + * Software liegt unter "/" das Dashboard, das hier nicht enthalten ist. + */ export default function Page() { - return ; + redirect(ANALYTICS_START); } diff --git a/docs/NAVIGATION.md b/docs/NAVIGATION.md index 2b37bfc..1ed53e3 100644 --- a/docs/NAVIGATION.md +++ b/docs/NAVIGATION.md @@ -2,22 +2,47 @@ ## Struktur +Die Seitenleiste **spiegelt die von HiveGrid Cloud** — dieselbe Komponente +(`ui/components/SidebarWithSections`), dieselben Gruppen, dieselbe Reihenfolge: + ``` -Seitenleiste (fest, einklappbar auf Icon-Breite) -├── Analyse -│ ├── Übersicht / Kernkennzahlen, Handlungsbedarf -│ ├── Finanzen /finanzen Umsatz, Forderungen, Zahlungsverhalten -│ ├── Aufträge /auftraege Pipeline, Termine, Förderung -│ ├── Kunden /kunden Akquise, Quellen, Reaktivierung -│ └── Team /team Auslastung, Stunden, Abschlüsse -└── Entwicklung - └── Komponenten /komponenten Bausteine und Regeln (nur für die Umsetzung) +HIVEGRID +├── Dashboard gehört zur Software, hier nicht enthalten +├── Analytics ◄── der Prototyp lebt hier /analytics +├── Aufgaben gehört zur Software +├── Kunden +│ ├── Kontakte · Leads +├── Projekte +│ ├── Pipelines · Plantafel · Dateien +├── Kommunikation +│ ├── Postfach · Formulare · Telefonie +├── Organisation +│ └── Termine +├───────────────────── +├── Komponenten Bausteingalerie des Prototyps /komponenten +└── Einstellungen gehört zur Software ``` -**Warum Seitenleiste statt Tab-Leiste:** Die bestehende HiveGrid-Oberfläche -navigiert links. Der Prototyp lässt sich so ohne Umbau der -Informationsarchitektur einsetzen. Einklappen schafft Platz für breite Tabellen -und das fünfspaltige KPI-Raster. +**Analytics ist ein einzelner Menüpunkt**, kein aufklappbarer Bereich. Die fünf +Analyse-Seiten sind darin über **Reiter** erreichbar: + +``` +/analytics Übersicht +/analytics/finanzen Finanzen +/analytics/auftraege Aufträge +/analytics/kunden Kunden +/analytics/team Team +``` + +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. + +**Nur Analytics und Komponenten führen hier irgendwohin.** Die übrigen Einträge +zeigen, wo die Analyse in der fertigen Anwendung sitzt, und melden beim Klick, +dass sie zur Software gehören. Eine tote Schaltfläche ohne Rückmeldung wäre +schlechter als gar keine. --- diff --git a/docs/SCOPE-ANALYTICS-DASHBOARD.md b/docs/SCOPE-ANALYTICS-DASHBOARD.md index c0f8d28..950f36b 100644 --- a/docs/SCOPE-ANALYTICS-DASHBOARD.md +++ b/docs/SCOPE-ANALYTICS-DASHBOARD.md @@ -1,8 +1,8 @@ # Scope: Analytics und Dashboard sind zwei Features -**Stand: 29.08.2026 · Festgehalten, nicht entschieden.** -Auf dieser Grundlage ist noch nichts gebaut. Die offenen Punkte in Abschnitt 5 -sind mit den Kollegen zu klären, bevor jemand Code anfasst. +**Stand: 29.08.2026.** Von den offenen Punkten in Abschnitt 5 ist bisher genau +einer entschieden und gebaut: die Navigation (5.5). Alles andere ist mit den +Kollegen zu klären, bevor jemand Code anfasst. --- @@ -95,7 +95,7 @@ wie sie sind. ## 4. Wie die beiden zusammenhängen ``` -Analytics (Bereich „Statistiken") Dashboard (Startseite) +Analytics (ein Menüpunkt, Reiter innen) Dashboard (eigener Menüpunkt) ├── Übersicht ← Rolle unklar, ┐ ├── Begrüßung ├── Finanzen siehe 5.1 │ ├── KPI-Kacheln ┐ aus dem ├── Aufträge ├──────┤ Trend-Charts ┤ Analytics- @@ -134,10 +134,14 @@ akzeptieren oder anders lösen. Globaler Filter wie in Analytics oder pro Widget wie in P11? Die Software hat sich für „pro Widget" entschieden; das ist beim Übertragen zu beachten. -**5.5 Navigation.** -Eigene Gruppe „Statistiken" neben Kunden/Projekte — siehe -[ZIELSYSTEM.md](ZIELSYSTEM.md) 3.1b. Hängt an 5.1: Solange unklar ist, ob es -eine Übersichtsseite gibt, ist auch der Einstiegspunkt der Gruppe offen. +**5.5 Navigation — entschieden am 29.08.2026.** +Analytics ist **ein einzelner Menüpunkt** in der Seitenleiste, gleichrangig +neben Dashboard. Die fünf Analyse-Seiten sind darin über Reiter erreichbar, +nicht als Unterpunkte in der Navigation. Das Dashboard bleibt als eigener +Menüpunkt daneben. Umgesetzt; siehe [NAVIGATION.md](NAVIGATION.md). + +Offen bleibt daran nur 5.1: Wenn die Übersichtsseite zum Dashboard wird, +verliert der Analytics-Bereich seinen ersten Reiter. **5.6 Team-Seite.** Unabhängig davon offen, weil Zeiterfassung im v2-Backlog steht — siehe diff --git a/docs/ZIELSYSTEM.md b/docs/ZIELSYSTEM.md index e0467ed..a8a9cef 100644 --- a/docs/ZIELSYSTEM.md +++ b/docs/ZIELSYSTEM.md @@ -176,7 +176,12 @@ Dimensionsfilter. Analyse „warum ist das so". Beides in eine Seite zu legen, macht aus dem Einstieg eine Halde. -**Empfehlung: Option 1**, mit dem Namen **„Statistiken"** — so heißt der +**Entschieden am 29.08.2026: Option 1**, mit dem Namen **„Analytics"** — der +Prototyp setzt das um. Die Seitenleiste spiegelt jetzt die der Software +(dieselbe Komponente, dieselben Gruppen); „Analytics" ist ein einzelner +Menüpunkt neben „Dashboard", die fünf Seiten sind darin über Reiter erreichbar. + +*Ursprüngliche Empfehlung war der Name* **„Statistiken"** — so heißt der Breadcrumb im Prototyp bereits, und „Analyse" ist im Haus durch `leads/analytics` belegt. Das bestehende `/dashboard` bleibt Startseite; unsere Seite „Übersicht" braucht dann einen anderen Namen, sonst konkurrieren zwei diff --git a/features/dashboard/components/layout/AnalyticsShell.tsx b/features/dashboard/components/layout/AnalyticsShell.tsx new file mode 100644 index 0000000..50166ea --- /dev/null +++ b/features/dashboard/components/layout/AnalyticsShell.tsx @@ -0,0 +1,60 @@ +"use client"; + +import React from "react"; +import Link from "next/link"; +import { Tabs } from "@/ui/components/Tabs"; +import { PageHeader } from "./PageHeader"; +import { AnalyticsToolbar } from "../filters/AnalyticsToolbar"; +import { ANALYSE_SEITEN } from "./navigation"; +import type { SeitenId } from "../../data/filtering"; +import type { FilterOptionen } from "../../view-models/filter-optionen"; + +/** + * Rahmen des Analytics-Bereichs. + * + * „Analytics" ist in der Seitenleiste **ein** Menüpunkt. Die fünf Analyse-Seiten + * 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. + * + * Kopfzeile und Filterleiste liegen hier und nicht in den Seiten: Sie sind für + * alle fünf gleich, und der Filterzustand gilt bereichsweit. + */ +export function AnalyticsShell({ + aktiv, + optionen, + children, +}: { + aktiv: SeitenId; + optionen: FilterOptionen; + children: React.ReactNode; +}) { + 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/AppShell.tsx b/features/dashboard/components/layout/AppShell.tsx index 53b3e88..cc980a7 100644 --- a/features/dashboard/components/layout/AppShell.tsx +++ b/features/dashboard/components/layout/AppShell.tsx @@ -3,134 +3,140 @@ import React from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { - FeatherBell, - FeatherHelpCircle, - FeatherPanelLeft, - FeatherSearch, -} from "@subframe/core"; -import { ALLE_SEITEN, NAV_GRUPPEN } from "./navigation"; +import { FeatherBell, FeatherHelpCircle, FeatherSearch } from "@subframe/core"; +import { SidebarWithSections } from "@/ui/components/SidebarWithSections"; +import { NAV_GRUPPEN, NAV_SEKUNDAER, ANALYSE_SEITEN, type NavEintrag } from "./navigation"; import { FeatherIcon } from "./FeatherIcon"; +import { useToast } from "../../context/ToastContext"; import { DATENSATZ_INFO } from "../../data/seed"; import { formatDatum, formatZahl } from "../../utils/formatters"; import { HEUTE } from "../../utils/dateUtils"; /** - * Rahmen der Anwendung: feste Seitenleiste links, Inhalt rechts. + * Rahmen der Anwendung — Seitenleiste nach dem Vorbild von HiveGrid Cloud. * - * Warum Seitenleiste statt Tab-Leiste: Die echte HiveGrid-Oberfläche - * navigiert links; so passt der Prototyp beim Einbau ohne Umbau der - * Informationsarchitektur. Die Leiste lässt sich auf Icon-Breite einklappen, - * damit breite Tabellen und 12-Spalten-Raster Platz bekommen. + * Bewusst dieselbe Komponente wie dort (`~/ui/components/SidebarWithSections`) + * mit denselben Gruppen und derselben Reihenfolge. Wer den Prototyp neben der + * Software öffnet, soll dieselbe Navigation sehen; die Übergabe wird dadurch + * eine Frage des Einsetzens und nicht des Übersetzens. + * + * **Nur „Analytics" führt hier irgendwohin.** Die übrigen Einträge zeigen den + * Ort in der fertigen Anwendung und melden beim Klick, dass sie zur Software + * gehören — eine tote Schaltfläche ohne Rückmeldung wäre schlechter als gar + * keine. */ export function AppShell({ children }: { children: React.ReactNode }) { const pathname = usePathname(); + const { zeigeToast } = useToast(); const [eingeklappt, setEingeklappt] = React.useState(false); - const aktiveSeite = - ALLE_SEITEN.find((s) => s.href !== "/" && pathname.startsWith(s.href)) ?? - ALLE_SEITEN.find((s) => s.href === "/"); + const istAktiv = (eintrag: NavEintrag) => + eintrag.ziel.art === "route" && + (eintrag.ziel.href === "/analytics" + ? pathname.startsWith("/analytics") + : pathname.startsWith(eintrag.ziel.href)); + + const renderEintrag = (eintrag: NavEintrag) => { + const item = ( + } + selected={istAktiv(eintrag)} + variant={eingeklappt ? "icon-only" : "icon-and-text"} + > + {eintrag.label} + + ); + + if (eintrag.ziel.art === "route") { + return ( + + {item} + + ); + } + + return ( + + ); + }; + + const aktiveAnalyse = ANALYSE_SEITEN.find((s) => + s.href === "/analytics" ? pathname === "/analytics" : pathname.startsWith(s.href) + ); + const bereich = pathname.startsWith("/komponenten") ? "Komponenten" : "Analytics"; return (
- +
+ +
- Statistiken - / - - {aktiveSeite?.label ?? "Übersicht"} - + {bereich} + {aktiveAnalyse ? ( + <> + / + + {aktiveAnalyse.label} + + + ) : null}
Suchen - + ⌘K
diff --git a/features/dashboard/components/layout/navigation.ts b/features/dashboard/components/layout/navigation.ts index be2c40e..b983ef1 100644 --- a/features/dashboard/components/layout/navigation.ts +++ b/features/dashboard/components/layout/navigation.ts @@ -1,75 +1,156 @@ import type { SeitenId } from "../../data/filtering"; +/** + * Navigationsstruktur — Spiegel der Seitenleiste von HiveGrid Cloud. + * + * Die Gruppen, ihre Reihenfolge und die Einträge entsprechen + * `apps/web/app/application/sidebar/AppSidebar.tsx` im Zielsystem. Der Grund + * ist die Übergabe: Wer diesen Prototyp neben der Software öffnet, soll dieselbe + * Navigation sehen und nicht erst übersetzen müssen. + * + * **Nur „Analytics" ist in diesem Prototyp echt.** Alle anderen Einträge sind + * Kontext — sie zeigen, wo die Analyse in der fertigen Anwendung sitzt. Sie + * führen bewusst nirgendwohin und melden das beim Klick, statt still zu bleiben. + * + * Icons: Die Software nutzt eigene Icon-Komponenten (`AddressBookIcon`, + * `BoardIcon` …), die auf einem hausinternen Set aufsetzen. Hier stehen die + * nächstliegenden Feather-Icons aus `@subframe/core`, das beide Repos teilen — + * optisch praktisch deckungsgleich, ohne ein ganzes Icon-Set mitzuschleppen. + */ + +export type NavZiel = { art: "route"; href: string } | { art: "software" }; + export interface NavEintrag { - id: SeitenId | "komponenten"; - href: string; + id: string; label: string; - /** Icon-Name aus @subframe/core (Feather-Set). */ + /** Icon-Name aus @subframe/core (ohne das Präfix "Feather"). */ icon: string; - beschreibung: string; + ziel: NavZiel; } -/** - * Navigationsstruktur. - * - * Vier Analyse-Seiten kommen direkt aus REQUIREMENTS-analytics.md - * (Übersicht, Finanzen, Aufträge, Kunden). "Team" war dort als v2 markiert, - * ist hier aber enthalten, weil Auslastung im Handwerk die Kennzahl ist, - * nach der Betriebe steuern. "Komponenten" ist reine Entwickler-Doku. - */ -export const NAV_GRUPPEN: { titel: string; eintraege: NavEintrag[] }[] = [ +export interface NavGruppe { + id: string; + /** Leerer Titel = Startgruppe ohne Überschrift, wie in der Software. */ + titel: string; + eintraege: NavEintrag[]; +} + +/** Einstieg des Analytics-Bereichs — auch das Ziel der Weiterleitung von „/". */ +export const ANALYTICS_START = "/analytics"; + +export const NAV_GRUPPEN: NavGruppe[] = [ { - titel: "Analyse", + id: "start", + titel: "", eintraege: [ { - id: "uebersicht", - href: "/", - label: "Übersicht", + id: "dashboard", + label: "Dashboard", icon: "LayoutDashboard", - beschreibung: "Alle Kernkennzahlen auf einen Blick", + ziel: { art: "software" }, }, { - id: "finanzen", - href: "/finanzen", - label: "Finanzen", - icon: "Euro", - beschreibung: "Umsatz, Rechnungen, Zahlungseingang", - }, - { - id: "auftraege", - href: "/auftraege", - label: "Aufträge", - icon: "ClipboardList", - beschreibung: "Pipeline, Status, Termine", - }, - { - id: "kunden", - href: "/kunden", - label: "Kunden", - icon: "Users", - beschreibung: "Akquise, Quellen, Bestand", - }, - { - id: "team", - href: "/team", - label: "Team", - icon: "HardHat", - beschreibung: "Auslastung und Produktivität der Monteure", + // Der neue Menüpunkt. In der Software gibt es ihn noch nicht — die + // fünf Analyse-Seiten sind ausschließlich hier drin navigierbar, + // nicht als Unterpunkte in der Seitenleiste. + id: "analytics", + label: "Analytics", + icon: "BarChart", + ziel: { art: "route", href: ANALYTICS_START }, }, + { id: "aufgaben", label: "Aufgaben", icon: "Clipboard", ziel: { art: "software" } }, ], }, { - titel: "Entwicklung", + id: "kunden", + titel: "Kunden", eintraege: [ - { - id: "komponenten", - href: "/komponenten", - label: "Komponenten", - icon: "Component", - beschreibung: "Bausteine, Regeln und Übergabehinweise", - }, + { id: "kontakte", label: "Kontakte", icon: "Contact", ziel: { art: "software" } }, + { id: "leads", label: "Leads", icon: "TrendingUp", ziel: { art: "software" } }, + ], + }, + { + id: "projekte", + titel: "Projekte", + eintraege: [ + { id: "pipelines", label: "Pipelines", icon: "Trello", ziel: { art: "software" } }, + { id: "plantafel", label: "Plantafel", icon: "CalendarRange", ziel: { art: "software" } }, + { id: "dateien", label: "Dateien", icon: "FileText", ziel: { art: "software" } }, + ], + }, + { + id: "kommunikation", + titel: "Kommunikation", + eintraege: [ + { id: "postfach", label: "Postfach", icon: "Mail", ziel: { art: "software" } }, + { id: "formulare", label: "Formulare", icon: "Table", ziel: { art: "software" } }, + { id: "telefonie", label: "Telefonie", icon: "Phone", ziel: { art: "software" } }, + ], + }, + { + id: "organisation", + titel: "Organisation", + eintraege: [ + { id: "termine", label: "Termine", icon: "CalendarDays", ziel: { art: "software" } }, ], }, ]; -export const ALLE_SEITEN = NAV_GRUPPEN.flatMap((g) => g.eintraege); +/** Unterer Bereich, durch einen Trenner abgesetzt — wie in der Software. */ +export const NAV_SEKUNDAER: NavEintrag[] = [ + { + // Gehört nicht zur Software, sondern zum Prototyp: die Bausteingalerie + // für die Entwicklung. + id: "komponenten", + label: "Komponenten", + icon: "Component", + ziel: { art: "route", href: "/komponenten" }, + }, + { id: "einstellungen", label: "Einstellungen", icon: "Settings", ziel: { art: "software" } }, +]; + +/** + * Die fünf Analyse-Seiten. Sie erscheinen **nicht** in der Seitenleiste, + * sondern als Reiter innerhalb des Menüpunkts „Analytics" — dem Hausmuster + * für Unter-Navigation in HiveGrid (`SettingsHubShell.tsx`: ein `Link` um ein + * `Tabs.Item`, damit jeder Reiter eine echte Navigation ist). + */ +export interface AnalyseSeite { + id: SeitenId; + label: string; + href: string; + beschreibung: string; +} + +export const ANALYSE_SEITEN: AnalyseSeite[] = [ + { + id: "uebersicht", + label: "Übersicht", + href: "/analytics", + beschreibung: "Die Kennzahlen, nach denen der Betrieb gesteuert wird — Umsatz, Auslastung, offene Posten und Handlungsbedarf.", + }, + { + id: "finanzen", + label: "Finanzen", + href: "/analytics/finanzen", + beschreibung: "Umsatz, Zahlungseingang und offene Forderungen — die Sicht der Buchhaltung auf den Betrieb.", + }, + { + id: "auftraege", + 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", + label: "Kunden", + href: "/analytics/kunden", + beschreibung: "Woher Kunden kommen, wie viel sie bringen und wo Nachfassen lohnt.", + }, + { + id: "team", + label: "Team", + href: "/analytics/team", + beschreibung: "Auslastung, produktive Stunden und Abschlüsse der Monteure — die Kennzahl hinter der Terminplanung.", + }, +]; diff --git a/features/dashboard/pages/AuftraegeSeite.tsx b/features/dashboard/pages/AuftraegeSeite.tsx index 7e51726..aad1b2b 100644 --- a/features/dashboard/pages/AuftraegeSeite.tsx +++ b/features/dashboard/pages/AuftraegeSeite.tsx @@ -1,9 +1,7 @@ "use client"; import React from "react"; -import { PageHeader } from "../components/layout/PageHeader"; import { PageContainer, Raster } from "../components/layout/PageContainer"; -import { AnalyticsToolbar } from "../components/filters/AnalyticsToolbar"; import { KPICard } from "../components/cards/KPICard"; import { ChartCard } from "../components/cards/ChartCard"; import { StatZeile } from "../components/cards/StatZeile"; @@ -31,26 +29,10 @@ import type { Auftrag } from "../data/types"; import { SeitenZustand } from "../components/layout/SeitenZustand"; import type { VMState } from "../view-models"; import type { AuftraegeDaten } from "../view-models/auftraege.vm"; -import type { FilterOptionen } from "../view-models/filter-optionen"; -export function AuftraegeSeite({ - vm, - optionen, -}: { - vm: VMState; - optionen: FilterOptionen; -}) { +export function AuftraegeSeite({ vm }: { vm: VMState }) { if (vm.status !== "ready") { - return ( - <> - - - - - ); + return ; } const d = vm.data; @@ -113,11 +95,6 @@ export function AuftraegeSeite({ return ( <> - - diff --git a/features/dashboard/pages/FinanzenSeite.tsx b/features/dashboard/pages/FinanzenSeite.tsx index be3ed83..8dd0d1a 100644 --- a/features/dashboard/pages/FinanzenSeite.tsx +++ b/features/dashboard/pages/FinanzenSeite.tsx @@ -1,9 +1,7 @@ "use client"; import React from "react"; -import { PageHeader } from "../components/layout/PageHeader"; import { PageContainer, Raster } from "../components/layout/PageContainer"; -import { AnalyticsToolbar } from "../components/filters/AnalyticsToolbar"; import { KPICard } from "../components/cards/KPICard"; import { ChartCard } from "../components/cards/ChartCard"; import { StatZeile } from "../components/cards/StatZeile"; @@ -36,29 +34,13 @@ import type { Rechnung } from "../data/types"; import { SeitenZustand } from "../components/layout/SeitenZustand"; import type { VMState } from "../view-models"; import type { FinanzenDaten } from "../view-models/finanzen.vm"; -import type { FilterOptionen } from "../view-models/filter-optionen"; /** Ampelfarben für die Altersstruktur: je älter die Forderung, desto kritischer. */ const ALTER_FARBEN = [...RISIKO_RAMPE]; -export function FinanzenSeite({ - vm, - optionen, -}: { - vm: VMState; - optionen: FilterOptionen; -}) { +export function FinanzenSeite({ vm }: { vm: VMState }) { if (vm.status !== "ready") { - return ( - <> - - - - - ); + return ; } const d = vm.data; @@ -116,11 +98,6 @@ export function FinanzenSeite({ return ( <> - - diff --git a/features/dashboard/pages/KundenSeite.tsx b/features/dashboard/pages/KundenSeite.tsx index 6da15f3..e8e077b 100644 --- a/features/dashboard/pages/KundenSeite.tsx +++ b/features/dashboard/pages/KundenSeite.tsx @@ -1,9 +1,7 @@ "use client"; import React from "react"; -import { PageHeader } from "../components/layout/PageHeader"; import { PageContainer, Raster } from "../components/layout/PageContainer"; -import { AnalyticsToolbar } from "../components/filters/AnalyticsToolbar"; import { KPICard } from "../components/cards/KPICard"; import { ChartCard } from "../components/cards/ChartCard"; import { BalkenVertikal } from "../components/charts/BalkenVertikal"; @@ -24,26 +22,10 @@ import type { Kunde } from "../data/types"; import { SeitenZustand } from "../components/layout/SeitenZustand"; import type { VMState } from "../view-models"; import type { KundenDaten } from "../view-models/kunden.vm"; -import type { FilterOptionen } from "../view-models/filter-optionen"; -export function KundenSeite({ - vm, - optionen, -}: { - vm: VMState; - optionen: FilterOptionen; -}) { +export function KundenSeite({ vm }: { vm: VMState }) { if (vm.status !== "ready") { - return ( - <> - - - - - ); + return ; } const d = vm.data; @@ -109,11 +91,6 @@ export function KundenSeite({ return ( <> - - diff --git a/features/dashboard/pages/TeamSeite.tsx b/features/dashboard/pages/TeamSeite.tsx index 8daa23f..3f2bbf3 100644 --- a/features/dashboard/pages/TeamSeite.tsx +++ b/features/dashboard/pages/TeamSeite.tsx @@ -1,9 +1,7 @@ "use client"; import React from "react"; -import { PageHeader } from "../components/layout/PageHeader"; import { PageContainer, Raster } from "../components/layout/PageContainer"; -import { AnalyticsToolbar } from "../components/filters/AnalyticsToolbar"; import { KPICard } from "../components/cards/KPICard"; import { ChartCard } from "../components/cards/ChartCard"; import { AuslastungsBalken } from "../components/charts/AuslastungsBalken"; @@ -14,26 +12,10 @@ import { formatEuro, formatEuroKompakt, formatProzent, formatStunden, formatZahl import { SeitenZustand } from "../components/layout/SeitenZustand"; import type { VMState } from "../view-models"; import type { TeamDaten, TeamZeile } from "../view-models/team.vm"; -import type { FilterOptionen } from "../view-models/filter-optionen"; -export function TeamSeite({ - vm, - optionen, -}: { - vm: VMState; - optionen: FilterOptionen; -}) { +export function TeamSeite({ vm }: { vm: VMState }) { if (vm.status !== "ready") { - return ( - <> - - - - - ); + return ; } const d = vm.data; @@ -123,11 +105,6 @@ export function TeamSeite({ return ( <> - - diff --git a/features/dashboard/pages/UebersichtSeite.tsx b/features/dashboard/pages/UebersichtSeite.tsx index f4a3fdd..9be4bc3 100644 --- a/features/dashboard/pages/UebersichtSeite.tsx +++ b/features/dashboard/pages/UebersichtSeite.tsx @@ -1,9 +1,7 @@ "use client"; import React from "react"; -import { PageHeader } from "../components/layout/PageHeader"; import { PageContainer, Raster } from "../components/layout/PageContainer"; -import { AnalyticsToolbar } from "../components/filters/AnalyticsToolbar"; import { KPICard } from "../components/cards/KPICard"; import { ChartCard } from "../components/cards/ChartCard"; import { AlertList } from "../components/cards/AlertList"; @@ -34,26 +32,10 @@ import type { Auftrag } from "../data/types"; import { SeitenZustand } from "../components/layout/SeitenZustand"; import type { VMState } from "../view-models"; import type { UebersichtDaten } from "../view-models/uebersicht.vm"; -import type { FilterOptionen } from "../view-models/filter-optionen"; -export function UebersichtSeite({ - vm, - optionen, -}: { - vm: VMState; - optionen: FilterOptionen; -}) { +export function UebersichtSeite({ vm }: { vm: VMState }) { if (vm.status !== "ready") { - return ( - <> - - - - - ); + return ; } const d = vm.data; @@ -99,11 +81,6 @@ export function UebersichtSeite({ return ( <> - - diff --git a/features/dashboard/routes/AuftraegeRoute.tsx b/features/dashboard/routes/AuftraegeRoute.tsx index f271e17..1aae3ac 100644 --- a/features/dashboard/routes/AuftraegeRoute.tsx +++ b/features/dashboard/routes/AuftraegeRoute.tsx @@ -2,6 +2,7 @@ import { useAnalyseDaten } from "../hooks/useAnalyseDaten"; import { baueFilterOptionen } from "../view-models/filter-optionen"; +import { AnalyticsShell } from "../components/layout/AnalyticsShell"; import { baueAuftraegeVM } from "../view-models/auftraege.vm"; import { AuftraegeSeite } from "../pages/AuftraegeSeite"; @@ -22,9 +23,8 @@ import { AuftraegeSeite } from "../pages/AuftraegeSeite"; export function AuftraegeRoute() { const { daten, filter, gesamt } = useAnalyseDaten("auftraege"); return ( - + + + ); } diff --git a/features/dashboard/routes/FinanzenRoute.tsx b/features/dashboard/routes/FinanzenRoute.tsx index 492d3e5..96aea8b 100644 --- a/features/dashboard/routes/FinanzenRoute.tsx +++ b/features/dashboard/routes/FinanzenRoute.tsx @@ -2,6 +2,7 @@ import { useAnalyseDaten } from "../hooks/useAnalyseDaten"; import { baueFilterOptionen } from "../view-models/filter-optionen"; +import { AnalyticsShell } from "../components/layout/AnalyticsShell"; import { baueFinanzenVM } from "../view-models/finanzen.vm"; import { FinanzenSeite } from "../pages/FinanzenSeite"; @@ -22,9 +23,8 @@ import { FinanzenSeite } from "../pages/FinanzenSeite"; export function FinanzenRoute() { const { daten, filter, gesamt } = useAnalyseDaten("finanzen"); return ( - + + + ); } diff --git a/features/dashboard/routes/KundenRoute.tsx b/features/dashboard/routes/KundenRoute.tsx index 6dae285..601efcf 100644 --- a/features/dashboard/routes/KundenRoute.tsx +++ b/features/dashboard/routes/KundenRoute.tsx @@ -2,6 +2,7 @@ import { useAnalyseDaten } from "../hooks/useAnalyseDaten"; import { baueFilterOptionen } from "../view-models/filter-optionen"; +import { AnalyticsShell } from "../components/layout/AnalyticsShell"; import { baueKundenVM } from "../view-models/kunden.vm"; import { KundenSeite } from "../pages/KundenSeite"; @@ -22,9 +23,8 @@ import { KundenSeite } from "../pages/KundenSeite"; export function KundenRoute() { const { daten, filter, gesamt } = useAnalyseDaten("kunden"); return ( - + + + ); } diff --git a/features/dashboard/routes/TeamRoute.tsx b/features/dashboard/routes/TeamRoute.tsx index a007462..92e4328 100644 --- a/features/dashboard/routes/TeamRoute.tsx +++ b/features/dashboard/routes/TeamRoute.tsx @@ -2,6 +2,7 @@ import { useAnalyseDaten } from "../hooks/useAnalyseDaten"; import { baueFilterOptionen } from "../view-models/filter-optionen"; +import { AnalyticsShell } from "../components/layout/AnalyticsShell"; import { baueTeamVM } from "../view-models/team.vm"; import { TeamSeite } from "../pages/TeamSeite"; @@ -22,9 +23,8 @@ import { TeamSeite } from "../pages/TeamSeite"; export function TeamRoute() { const { daten, filter, gesamt } = useAnalyseDaten("team"); return ( - + + + ); } diff --git a/features/dashboard/routes/UebersichtRoute.tsx b/features/dashboard/routes/UebersichtRoute.tsx index 5501d0f..5abc737 100644 --- a/features/dashboard/routes/UebersichtRoute.tsx +++ b/features/dashboard/routes/UebersichtRoute.tsx @@ -2,6 +2,7 @@ import { useAnalyseDaten } from "../hooks/useAnalyseDaten"; import { baueFilterOptionen } from "../view-models/filter-optionen"; +import { AnalyticsShell } from "../components/layout/AnalyticsShell"; import { baueUebersichtVM } from "../view-models/uebersicht.vm"; import { UebersichtSeite } from "../pages/UebersichtSeite"; @@ -22,9 +23,8 @@ import { UebersichtSeite } from "../pages/UebersichtSeite"; export function UebersichtRoute() { const { daten, filter, gesamt } = useAnalyseDaten("uebersicht"); return ( - + + + ); } diff --git a/package-lock.json b/package-lock.json index a3a2179..6aef006 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "hivegrid-dashboard", "version": "0.1.0", "dependencies": { + "@radix-ui/react-slot": "^1.2.4", "@subframe/core": "^1.155.0", "clsx": "^2.1.1", "next": "16.2.6", @@ -22,6 +23,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.6", + "playwright-core": "^1.57.0", "tailwindcss": "^4", "typescript": "^5" } @@ -1440,7 +1442,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -2060,7 +2061,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", "license": "MIT", - "peer": true, "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" }, @@ -6927,6 +6927,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index 575c447..3d86581 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "verify:layout": "node scripts/verify-layout.mjs" }, "dependencies": { + "@radix-ui/react-slot": "^1.2.4", "@subframe/core": "^1.155.0", "clsx": "^2.1.1", "next": "16.2.6", diff --git a/scripts/verify-layout.mjs b/scripts/verify-layout.mjs index f79d604..58ce6a3 100644 --- a/scripts/verify-layout.mjs +++ b/scripts/verify-layout.mjs @@ -18,7 +18,14 @@ import { chromium } from "playwright-core"; const BASIS = process.env.DASHBOARD_URL ?? "http://localhost:4300"; -const SEITEN = ["/", "/finanzen", "/auftraege", "/kunden", "/team", "/komponenten"]; +const SEITEN = [ + "/analytics", + "/analytics/finanzen", + "/analytics/auftraege", + "/analytics/kunden", + "/analytics/team", + "/komponenten", +]; const BREITEN = [1024, 1152, 1280, 1366, 1440, 1600, 1920]; const MIN_ABSTAND = 8; @@ -101,7 +108,11 @@ for (const breite of BREITEN) { const ctx = await browser.newContext({ viewport: { width: breite, height: 1200 } }); const seite = await ctx.newPage(); for (const pfad of SEITEN) { - await seite.goto(BASIS + pfad, { waitUntil: "networkidle" }); + const antwort = await seite.goto(BASIS + pfad, { waitUntil: "networkidle" }); + if (!antwort || antwort.status() >= 400) { + console.error(`✗ ${pfad} antwortet mit ${antwort?.status() ?? "keiner Antwort"} — Route existiert nicht.`); + process.exit(1); + } await seite.waitForTimeout(500); const { treffer, ueberlauf } = await seite.evaluate(messen, MIN_ABSTAND); for (const t of treffer) { diff --git a/ui/components/IconButton/IconButton.tsx b/ui/components/IconButton/IconButton.tsx index 50ff7a7..3c69c05 100644 --- a/ui/components/IconButton/IconButton.tsx +++ b/ui/components/IconButton/IconButton.tsx @@ -7,10 +7,12 @@ import React from "react"; import { FeatherPlus } from "@subframe/core"; import * as SubframeCore from "@subframe/core"; +import { Slot, Slottable } from "@radix-ui/react-slot"; import * as SubframeUtils from "../../utils"; export interface IconButtonRootProps extends React.ButtonHTMLAttributes { + asChild?: boolean; disabled?: boolean; loading?: boolean; variant?: @@ -39,17 +41,30 @@ const IconButtonRoot = React.forwardRef( size = "medium", square = false, icon = , + children, className, type = "button", + asChild = false, ...otherProps }: IconButtonRootProps, ref ) { + // @subframe/sync-disable -- local accessibility fix: external links keep + // native link semantics without wrapping a button. + const Root = asChild ? Slot : "button"; return ( -
- + {asChild && children ? {children} : null} + ); } ); diff --git a/ui/components/Logo/Logo.tsx b/ui/components/Logo/Logo.tsx index d165849..88c25d7 100644 --- a/ui/components/Logo/Logo.tsx +++ b/ui/components/Logo/Logo.tsx @@ -14,65 +14,53 @@ export interface LogoRootProps extends React.HTMLAttributes { className?: string; } -const LogoRoot = React.forwardRef( - function LogoRoot( - { - size = "28", - showIcon = false, - hideText = false, - className, - ...otherProps - }: LogoRootProps, - ref - ) { - return ( -
- - -
- ); - } -); +const LogoRoot = React.forwardRef(function LogoRoot( + { size = "28", showIcon = false, hideText = false, className, ...otherProps }: LogoRootProps, + ref +) { + return ( +
+ {hideText + {showIcon +
+ ); +}); export const Logo = LogoRoot; diff --git a/ui/components/SidebarInside/SidebarInside.tsx b/ui/components/SidebarInside/SidebarInside.tsx index 485d1b0..37a51a4 100644 --- a/ui/components/SidebarInside/SidebarInside.tsx +++ b/ui/components/SidebarInside/SidebarInside.tsx @@ -1,17 +1,10 @@ "use client"; -/* - * Documentation: - * Icon Button — https://app.subframe.com/f023064a5ca0/library?component=Icon+Button_af9405b1-8c54-4e01-9786-5aad308224f6 - * Sidebar Inside — https://app.subframe.com/f023064a5ca0/library?component=Sidebar+Inside_f6538807-e4e9-4314-9b2f-aaed19760450 - */ import React from "react"; import { FeatherCircleDashed } from "@subframe/core"; -import { FeatherSidebarClose } from "@subframe/core"; -import { FeatherSidebarOpen } from "@subframe/core"; import * as SubframeCore from "@subframe/core"; import * as SubframeUtils from "../../utils"; -import { IconButton } from "../IconButton"; +import { useSidebarHoverSweep } from "../../sidebar-hover-sweep"; export interface NavItemProps extends React.HTMLAttributes { icon?: React.ReactNode; @@ -22,52 +15,44 @@ export interface NavItemProps extends React.HTMLAttributes { } const NavItem = React.forwardRef(function NavItem( - { - icon = , - children, - selected = false, - variant = "icon-and-text", - className, - ...otherProps - }: NavItemProps, + { icon = , children, selected = false, variant = "icon-and-text", className, ...otherProps }: NavItemProps, ref ) { return (
-
+ {/* @subframe/sync-disable -- local fix: no items-center flip on collapse; the icon column stays put. */} +
{icon ? ( - + {icon} ) : null} {children ? ( { className?: string; } -const NavSection = React.forwardRef( - function NavSection( - { - children, - label, - variant = "default", - showLabel = false, - showDivider = false, - className, - ...otherProps - }: NavSectionProps, - ref - ) { - return ( +const NavSection = React.forwardRef(function NavSection( + { children, label, variant = "default", showLabel = false, showDivider = false, className, ...otherProps }: NavSectionProps, + ref +) { + return ( +
-
- -
-
- -
- {children ? ( -
- {children} -
- ) : null} +
- ); - } -); +
+ +
+ {children ? ( +
+ {children} +
+ ) : null} +
+ ); +}); export interface NavLabelProps extends React.HTMLAttributes { label?: React.ReactNode; className?: string; } -const NavLabel = React.forwardRef( - function NavLabel({ label, className, ...otherProps }: NavLabelProps, ref) { - return ( -
- {label ? ( - - {label} - - ) : null} -
- ); - } -); +const NavLabel = React.forwardRef(function NavLabel({ label, className, ...otherProps }: NavLabelProps, ref) { + return ( +
+ {label ? {label} : null} +
+ ); +}); export interface NavDividerProps extends React.HTMLAttributes { className?: string; } -const NavDivider = React.forwardRef( - function NavDivider({ className, ...otherProps }: NavDividerProps, ref) { - return ( -
-
-
- ); - } -); +const NavDivider = React.forwardRef(function NavDivider({ className, ...otherProps }: NavDividerProps, ref) { + return ( +
+
+
+ ); +}); -export interface SubcomponentProps - extends React.HTMLAttributes { - sidebarButton?: "close" | "open"; - className?: string; -} - -const Subcomponent = React.forwardRef( - function Subcomponent( - { sidebarButton = "close", className, ...otherProps }: SubcomponentProps, - ref - ) { - return ( -
- - ) : ( - - ) - } - /> -
- ); - } -); - -export interface SidebarInsideRootProps - extends React.HTMLAttributes { +export interface SidebarInsideRootProps extends React.HTMLAttributes { variant?: "open" | "closed"; footer?: React.ReactNode; children?: React.ReactNode; @@ -236,64 +139,57 @@ export interface SidebarInsideRootProps className?: string; } -const SidebarInsideRoot = React.forwardRef( - function SidebarInsideRoot( - { - variant = "open", - footer, - children, - visible = false, - className, - ...otherProps - }: SidebarInsideRootProps, - ref - ) { - return ( - - ); - } -); +const SidebarInsideRoot = React.forwardRef(function SidebarInsideRoot( + { variant = "open", footer, children, visible = false, className, onPointerMove, onPointerLeave, ...otherProps }: SidebarInsideRootProps, + ref +) { + const { handlePointerMove, handlePointerLeave } = useSidebarHoverSweep({ onPointerMove, onPointerLeave }); + + return ( + + ); +}); export const SidebarInside = Object.assign(SidebarInsideRoot, { NavItem, NavSection, NavLabel, NavDivider, - Subcomponent, }); diff --git a/ui/components/SidebarWithSections/SidebarWithSections.tsx b/ui/components/SidebarWithSections/SidebarWithSections.tsx index 901e705..ace7400 100644 --- a/ui/components/SidebarWithSections/SidebarWithSections.tsx +++ b/ui/components/SidebarWithSections/SidebarWithSections.tsx @@ -12,6 +12,7 @@ import { FeatherSidebarClose } from "@subframe/core"; import { FeatherSidebarOpen } from "@subframe/core"; import * as SubframeCore from "@subframe/core"; import * as SubframeUtils from "../../utils"; +import { useSidebarHoverSweep } from "../../sidebar-hover-sweep"; import { IconButton } from "../IconButton"; import { Logo } from "../Logo"; @@ -24,57 +25,54 @@ export interface NavItemProps extends React.HTMLAttributes { } const NavItem = React.forwardRef(function NavItem( - { - icon = , - children, - selected = false, - variant = "icon-and-text", - className, - ...otherProps - }: NavItemProps, + { icon = , children, selected = false, variant = "icon-and-text", className, ...otherProps }: NavItemProps, ref ) { return (
{icon ? ( - + {icon} ) : null} {children ? ( { className?: string; } -const NavSection = React.forwardRef( - function NavSection( - { - children, - label, - variant = "default", - showLabel = false, - showDivider = false, - className, - ...otherProps - }: NavSectionProps, - ref - ) { - return ( +const NavSection = React.forwardRef(function NavSection( + { children, label, variant = "default", showLabel = false, showDivider = false, className, ...otherProps }: NavSectionProps, + ref +) { + return ( +
-
- -
-
- -
- {children ? ( -
- {children} -
- ) : null} +
- ); - } -); +
+ +
+ {children ? ( +
+ {children} +
+ ) : null} +
+ ); +}); export interface NavLabelProps extends React.HTMLAttributes { label?: React.ReactNode; className?: string; } -const NavLabel = React.forwardRef( - function NavLabel({ label, className, ...otherProps }: NavLabelProps, ref) { - return ( -
- {label ? ( - - {label} - - ) : null} -
- ); - } -); +const NavLabel = React.forwardRef(function NavLabel({ label, className, ...otherProps }: NavLabelProps, ref) { + return ( +
+ {label ? {label} : null} +
+ ); +}); export interface NavDividerProps extends React.HTMLAttributes { className?: string; } -const NavDivider = React.forwardRef( - function NavDivider({ className, ...otherProps }: NavDividerProps, ref) { - return ( -
-
-
- ); - } -); +const NavDivider = React.forwardRef(function NavDivider({ className, ...otherProps }: NavDividerProps, ref) { + return ( +
+
+
+ ); +}); -export interface SubcomponentProps - extends React.HTMLAttributes { +export interface SubcomponentProps extends React.HTMLAttributes { sidebarButton?: "close" | "open"; + buttonLabel?: string; + onToggle?: () => void; className?: string; } -const Subcomponent = React.forwardRef( - function Subcomponent( - { sidebarButton = "close", className, ...otherProps }: SubcomponentProps, - ref - ) { - return ( -
- - ) : ( - - ) - } - /> -
- ); - } -); +const Subcomponent = React.forwardRef(function Subcomponent( + { sidebarButton = "close", buttonLabel, onToggle, className, ...otherProps }: SubcomponentProps, + ref +) { + return ( +
+ : } + aria-label={buttonLabel} + onClick={onToggle} + /> +
+ ); +}); -export interface SidebarWithSectionsRootProps - extends React.HTMLAttributes { +export interface SidebarWithSectionsRootProps extends React.HTMLAttributes { variant?: "open" | "closed"; footer?: React.ReactNode; children?: React.ReactNode; className?: string; + // @subframe/sync-disable -- local fix: Collapse-Toggle verdrahtbar machen (App steuert open/closed). + onToggleCollapse?: () => void; + // @subframe/sync-disable -- local fix: native collapse button needs an accessible, translated name. + toggleLabel?: string; + // @subframe/sync-disable -- local fix: make the logo a link (click -> dashboard, same target as the Dashboard nav item). + logoHref?: string; + // @subframe/sync-disable -- logo navigation needs the caller's translated accessible name. + logoLabel?: string; + // @subframe/sync-disable -- branded surfaces can replace the default mark without forking the canonical sidebar. + logoContent?: React.ReactNode; + // @subframe/sync-disable -- callback-owned surfaces keep logo navigation client-side. + onLogoClick?: () => void; } -const SidebarWithSectionsRoot = React.forwardRef< - HTMLElement, - SidebarWithSectionsRootProps ->(function SidebarWithSectionsRoot( +const SidebarWithSectionsRoot = React.forwardRef(function SidebarWithSectionsRoot( { variant = "open", footer, children, className, + onToggleCollapse, + toggleLabel, + logoHref, + logoLabel, + logoContent, + onLogoClick, + onPointerMove, + onPointerLeave, ...otherProps }: SidebarWithSectionsRootProps, ref ) { + const { handlePointerMove, handlePointerLeave } = useSidebarHoverSweep({ onPointerMove, onPointerLeave }); + return (