Der Knopf ragt bauartbedingt 17 px aus der Seitenleiste in die Inhaltsfläche hinein. Dort wurde seine rechte Hälfte verdeckt — er sah abgeschnitten aus und war auf diesem Teil nicht anklickbar. Nicht abgeschnitten, sondern überdeckt: Gemessen war overflow überall visible, aber das oberste Element an der Knopfmitte war die Inhaltsspalte. Ursache ist ein Stapelkontext — 'position: sticky' erzeugt einen eigenen, wodurch das z-index:40 des Knopfes darin gefangen bleibt. Der Rahmen selbst hatte z-index:auto, also malte die später im Dokument stehende Inhaltsspalte darüber. Behoben mit z-50 am Sidebar-Rahmen; die Software setzt an derselben Stelle ebenfalls z-50. Nachgemessen: Am rechten Rand ist jetzt der Knopf selbst das oberste Element, und ein Klick genau dort schaltet um. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
150 lines
6.5 KiB
TypeScript
150 lines
6.5 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/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 { DATENSATZ_INFO } from "../../data/seed";
|
|
import { formatDatum, formatZahl } from "../../utils/formatters";
|
|
import { HEUTE } from "../../utils/dateUtils";
|
|
|
|
/**
|
|
* Rahmen der Anwendung — Seitenleiste nach dem Vorbild von HiveGrid Cloud.
|
|
*
|
|
* 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.
|
|
*
|
|
* Jeder Eintrag führt auf eine echte Seite. „Dashboard" landet auf einer
|
|
* Platzhalterseite, weil es ein eigenes Feature ist und hier nicht gestaltet
|
|
* wird — aber ein Menüpunkt, der nur einen Hinweis auslöst, lässt offen, ob
|
|
* etwas fehlt oder etwas kaputt ist.
|
|
*/
|
|
export function AppShell({ children }: { children: React.ReactNode }) {
|
|
const pathname = usePathname();
|
|
const [eingeklappt, setEingeklappt] = React.useState(false);
|
|
|
|
const istAktiv = (eintrag: NavEintrag) =>
|
|
eintrag.ziel.href === "/analytics"
|
|
? pathname.startsWith("/analytics")
|
|
: pathname === eintrag.ziel.href;
|
|
|
|
const renderEintrag = (eintrag: NavEintrag) => (
|
|
<Link
|
|
key={eintrag.id}
|
|
href={eintrag.ziel.href}
|
|
title={eingeklappt ? eintrag.label : undefined}
|
|
className="w-full no-underline"
|
|
>
|
|
<SidebarWithSections.NavItem
|
|
icon={<FeatherIcon name={eintrag.icon} />}
|
|
selected={istAktiv(eintrag)}
|
|
variant={eingeklappt ? "icon-only" : "icon-and-text"}
|
|
>
|
|
{eintrag.label}
|
|
</SidebarWithSections.NavItem>
|
|
</Link>
|
|
);
|
|
|
|
const aktiveAnalyse = ANALYSE_SEITEN.find((s) =>
|
|
s.href === "/analytics" ? pathname === "/analytics" : pathname.startsWith(s.href)
|
|
);
|
|
const bereich = pathname.startsWith("/komponenten")
|
|
? "Komponenten"
|
|
: pathname.startsWith("/dashboard")
|
|
? "Dashboard"
|
|
: "Analytics";
|
|
|
|
return (
|
|
<div className="flex min-h-screen w-full bg-neutral-50">
|
|
{/* z-50 ist nötig, nicht dekorativ: `position: sticky` erzeugt einen
|
|
eigenen Stapelkontext. Ohne z-Wert am Rahmen bleibt das z-index des
|
|
Einklapp-Knopfes darin gefangen, und die Inhaltsspalte — ein späteres
|
|
Geschwisterelement — malt über den Teil, der aus der Leiste
|
|
herausragt. Die Software setzt an derselben Stelle ebenfalls z-50. */}
|
|
<div className="dv-no-print sticky top-0 z-50 h-screen flex-none">
|
|
<SidebarWithSections
|
|
variant={eingeklappt ? "closed" : "open"}
|
|
onToggleCollapse={() => setEingeklappt((wert) => !wert)}
|
|
toggleLabel={eingeklappt ? "Seitenleiste ausklappen" : "Seitenleiste einklappen"}
|
|
>
|
|
<div className="flex h-full min-h-0 w-full flex-col">
|
|
<div className="flex min-h-0 w-full grow flex-col gap-1 overflow-x-hidden overflow-y-auto">
|
|
{NAV_GRUPPEN.map((gruppe) => (
|
|
<SidebarWithSections.NavSection
|
|
key={gruppe.id}
|
|
label={gruppe.titel}
|
|
showLabel={!eingeklappt && !!gruppe.titel}
|
|
>
|
|
{gruppe.eintraege.map(renderEintrag)}
|
|
</SidebarWithSections.NavSection>
|
|
))}
|
|
</div>
|
|
|
|
<div className="sticky bottom-0 z-10 mt-auto flex w-full flex-none flex-col gap-1 bg-surface-neutral-secondary pt-2">
|
|
<SidebarWithSections.NavDivider />
|
|
{NAV_SEKUNDAER.map(renderEintrag)}
|
|
{/* Einzeilig statt als Kasten: Die Seitenleiste soll die der
|
|
Software spiegeln, und dort steht kein Demo-Hinweis. Die
|
|
Information bleibt, nimmt aber keinen Navigationsplatz weg. */}
|
|
{!eingeklappt ? (
|
|
<p className="px-3 pb-2 pt-1 text-[11px] leading-tight text-text-neutral-tertiary">
|
|
Demo-Daten · {formatZahl(DATENSATZ_INFO.auftraege)} Aufträge ·{" "}
|
|
{formatDatum(HEUTE.toISOString())}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</SidebarWithSections>
|
|
</div>
|
|
|
|
<div className="flex min-w-0 grow flex-col">
|
|
<header className="dv-no-print sticky top-0 z-30 flex h-14 flex-none items-center gap-4 border-b border-solid border-border-default bg-white/95 px-6 backdrop-blur">
|
|
<div className="flex min-w-0 items-center gap-2 text-[13px]">
|
|
<span className="text-text-neutral-tertiary">{bereich}</span>
|
|
{aktiveAnalyse ? (
|
|
<>
|
|
<span className="text-text-neutral-subtle">/</span>
|
|
<span className="truncate font-semibold text-text-neutral-primary">
|
|
{aktiveAnalyse.label}
|
|
</span>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="ml-auto flex items-center gap-2">
|
|
<div className="hidden h-9 items-center gap-2 rounded-md border border-solid border-border-default px-3 text-[13px] text-text-neutral-subtle lg:flex">
|
|
<FeatherSearch className="text-[15px]" />
|
|
<span>Suchen</span>
|
|
<kbd className="ml-4 rounded border border-solid border-border-default bg-neutral-50 px-1.5 py-0.5 text-[11px] font-medium text-text-neutral-tertiary">
|
|
⌘K
|
|
</kbd>
|
|
</div>
|
|
<button
|
|
className="flex h-9 w-9 items-center justify-center rounded-md text-text-neutral-tertiary hover:bg-neutral-100"
|
|
aria-label="Benachrichtigungen"
|
|
>
|
|
<FeatherBell className="text-[17px]" />
|
|
</button>
|
|
<button
|
|
className="flex h-9 w-9 items-center justify-center rounded-md text-text-neutral-tertiary hover:bg-neutral-100"
|
|
aria-label="Hilfe"
|
|
>
|
|
<FeatherHelpCircle className="text-[17px]" />
|
|
</button>
|
|
<span className="ml-1 flex h-8 w-8 items-center justify-center rounded-full bg-neutral-800 text-[12px] font-semibold text-white">
|
|
MK
|
|
</span>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="min-w-0 grow">{children}</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|