Files
ApiCore/ApiCore-Frontend/src/app/(app)/sidebar.tsx
T
Nick.RosenloecherandClaude Opus 5.5 cda3e02a31
Backend Image / build-and-push (push) Successful in 11m29s
Frontend Image / build-and-push (push) Successful in 23m35s
feat(t5): Bewegungs-Sprache mit Tokens, Stufen und Budget (T5a A11)
- src/lib/motion/tokens.ts und --motion-* in globals.css: Dauern,
  Kurven und Feder an einer Stelle; Beute-Bühne liest dieselben Werte.
- AppMotion: LazyMotion strict, Bewegungs-Code (domAnimation) als ein
  asynchroner Chunk, 28 kB gzip; check:motion-budget im Docker-Build
  neben dem 3D-Budget (Spec 17.6).
- PresenceList: abgehakte Zeilen blenden aus, die übrigen rücken nur bei
  „Voll“ nach; „Dezent“ blendet nur, „Aus“ sofort. Die Zeile geht nach
  der festen Verzögerung, auch wenn der Server schweigt (Spec 17.5);
  Fokus wandert zum nächsten Haken, auch ohne Bewegungs-Chunk.
- Overlays (Sheet, Dialog, Menü, Select) über Base-UI-Start/Ende-Stile
  mit Token-Zeiten; Verschiebung und Skalierung nur bei motion-full.
- Kein transition-all, kein Hover-Heben, keine Breiten-Animation von
  Daten; Skeleton erst nach 150 ms; Wächter-Regeln für alles davon.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 21:15:42 +02:00

73 lines
2.8 KiB
TypeScript

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { FileClock } from "lucide-react";
import { BrandMark } from "@/components/brand-mark";
import { cn } from "@/lib/utils";
import { NavCounter } from "@/components/suggestions/nav-counter";
import type { AuthUser } from "@/lib/auth/auth-context";
import { isNavActive, type NavItem, visibleNavGroups } from "./nav-config";
function SidebarLink({ item, pathname }: { item: NavItem; pathname: string }) {
const Icon = item.icon;
const isActive = isNavActive(pathname, item.href);
return (
<Link
href={item.href}
aria-current={isActive ? "page" : undefined}
className={cn(
"group relative flex items-center gap-2.5 rounded-xl px-3 py-2 text-sm font-medium transition-colors",
isActive
? "bg-primary text-primary-foreground shadow-[var(--shadow-soft)]"
: "text-muted-foreground hover:bg-accent/70 hover:text-foreground",
)}
>
<Icon className="size-4.5 shrink-0" />
<span className="truncate">{item.label}</span>
{item.counter ? <NavCounter counter={item.counter} active={isActive} /> : null}
</Link>
);
}
export function Sidebar({ user }: { user: AuthUser }) {
const pathname = usePathname();
const groups = visibleNavGroups(user);
return (
// The page scrolls on the window; the sidebar stays put beside it and
// scrolls on its own when the nav is taller than the screen. It is a
// sibling of the page content, so its overflow cannot capture sticky
// elements there (StickySubmit).
<aside className="sticky top-0 z-10 hidden h-dvh w-64 shrink-0 flex-col gap-1 self-start overflow-y-auto bg-sidebar px-3 py-5 shadow-[var(--shadow-soft)] sm:flex">
<Link
href="/dashboard"
className="mb-5 flex items-center gap-2.5 px-2 font-heading text-lg font-semibold"
>
<BrandMark size={32} withWordmark className="gap-2.5" />
</Link>
<nav className="flex flex-1 flex-col gap-5">
{groups.map((group, index) => (
<div
key={group.label ?? `group-${index}`}
className="flex flex-col gap-0.5"
>
{group.label ? (
<p className="px-3 pb-1 text-xs font-semibold tracking-wide text-muted-foreground/80 uppercase">
{group.label}
</p>
) : null}
{group.items.map((item) => (
<SidebarLink key={item.href} item={item} pathname={pathname} />
))}
</div>
))}
</nav>
<div className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground">
<FileClock className="size-3.5 shrink-0" />
<span className="truncate">v1.x</span>
</div>
</aside>
);
}