Files
Dashboard/ui/components/SidebarWithSections/SidebarWithSections.tsx
T
dejanandClaude Opus 5 b8bf987d0e feat(dashboard): Analytics-Dashboard als Gestaltungsvorlage zur Übergabe
UI/UX-Prototyp der Analytics-Dashboards in HiveGrid, fachlich auf einen
SHK-/Handwerksbetrieb zugeschnitten. Ziel ist die Übergabe an die Entwicklung,
nicht der Produktivbetrieb.

Inhalt: sechs Seiten (Übersicht, Finanzen, Aufträge, Kunden, Team, Komponenten),
26 KPIs, neun Chart-Bausteine, Filtersystem mit seitenabhängiger Relevanz und ein
deterministischer Demo-Datensatz (420 Kunden, 1.900 Aufträge, Stichtag 30.07.2026).

Drei Quellen sind zusammengeführt: die fachliche Planung mit dem Branchenpartner
(Januar 2026), die Subframe-Komponentenbibliothek aus dem HiveGrid-Pitch und
IBM Carbon Data Visualization für die Diagrammregeln. Herkunft und bewusste
Abweichungen stehen in docs/HERKUNFT.md.

Dokumentation: README, HANDOVER (was echte Logik braucht), docs/DESIGN-SYSTEM,
KPI-KATALOG, DATENMODELL, NAVIGATION, HERKUNFT, ZIELSYSTEM (Einbauort in
HiveGrid Cloud) und LEITPLANKEN (welche Cloud-Screens als Vorbild taugen).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
2026-08-29 17:25:20 +02:00

317 lines
9.1 KiB
TypeScript

"use client";
/*
* Documentation:
* Icon Button — https://app.subframe.com/f023064a5ca0/library?component=Icon+Button_af9405b1-8c54-4e01-9786-5aad308224f6
* Logo — https://app.subframe.com/f023064a5ca0/library?component=Logo_609493cb-7054-4018-bcb4-26d43de9ef41
* Sidebar with sections — https://app.subframe.com/f023064a5ca0/library?component=Sidebar+with+sections_f4047c8b-cfb4-4761-b9cf-fbcae8a9b9b5
*/
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 { Logo } from "../Logo";
export interface NavItemProps extends React.HTMLAttributes<HTMLDivElement> {
icon?: React.ReactNode;
children?: React.ReactNode;
selected?: boolean;
variant?: "icon-and-text" | "icon-only" | "subitem";
className?: string;
}
const NavItem = React.forwardRef<HTMLDivElement, NavItemProps>(function NavItem(
{
icon = <FeatherCircleDashed />,
children,
selected = false,
variant = "icon-and-text",
className,
...otherProps
}: NavItemProps,
ref
) {
return (
<div
className={SubframeUtils.twClassNames(
"group/2713e17b flex w-full cursor-pointer flex-col items-start gap-2 pt-2",
{ "items-center": variant === "icon-only" },
className
)}
ref={ref}
{...otherProps}
>
<div
className={SubframeUtils.twClassNames(
"flex w-full flex-col items-start gap-2",
{
"items-end justify-center": variant === "subitem",
"items-center": variant === "icon-only",
}
)}
>
<div
className={SubframeUtils.twClassNames(
"flex w-full items-center gap-2.5 rounded-md px-3 py-2 group-hover/2713e17b:bg-surface-neutral-tertiary",
{
"w-52 flex-none group-active/2713e17b:w-full group-active/2713e17b:max-w-[208px] group-active/2713e17b:flex-none group-active/2713e17b:justify-end":
variant === "subitem",
"w-auto flex-none items-start justify-center px-2.5 py-2.5":
variant === "icon-only",
"bg-surface-neutral-tertiary group-hover/2713e17b:bg-surface-neutral-modal group-active/2713e17b:bg-surface-neutral-tertiary":
selected,
}
)}
>
{icon ? (
<SubframeCore.IconWrapper className="text-body-large font-body-large text-icon-neutral-primary group-active/2713e17b:text-icon-neutral-on-subtle">
{icon}
</SubframeCore.IconWrapper>
) : null}
{children ? (
<span
className={SubframeUtils.twClassNames(
"line-clamp-1 grow shrink-0 basis-0 text-body-small-regular-0 font-body-small-regular-0 text-text-neutral-secondary group-active/2713e17b:text-text-neutral-on-subtle",
{
hidden: variant === "icon-only",
"text-text-neutral-on-subtle": selected,
}
)}
>
{children}
</span>
) : null}
</div>
</div>
</div>
);
});
export interface NavSectionProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
label?: React.ReactNode;
variant?: "default" | "subsection";
showLabel?: boolean;
showDivider?: boolean;
className?: string;
}
const NavSection = React.forwardRef<HTMLDivElement, NavSectionProps>(
function NavSection(
{
children,
label,
variant = "default",
showLabel = false,
showDivider = false,
className,
...otherProps
}: NavSectionProps,
ref
) {
return (
<div
className={SubframeUtils.twClassNames(
"group/5a4abcb2 flex w-full flex-col items-start gap-2 pt-2",
className
)}
ref={ref}
{...otherProps}
>
<div
className={SubframeUtils.twClassNames("hidden w-full items-start", {
flex: showDivider,
})}
>
<SidebarWithSections.NavDivider />
</div>
<div
className={SubframeUtils.twClassNames("hidden w-full items-start", {
flex: showLabel,
})}
>
<SidebarWithSections.NavLabel label={label} />
</div>
{children ? (
<div
className={SubframeUtils.twClassNames(
"flex w-full grow shrink-0 basis-0 flex-col items-center",
{ "pl-5 pr-0 py-0": variant === "subsection" }
)}
>
{children}
</div>
) : null}
</div>
);
}
);
export interface NavLabelProps extends React.HTMLAttributes<HTMLDivElement> {
label?: React.ReactNode;
className?: string;
}
const NavLabel = React.forwardRef<HTMLDivElement, NavLabelProps>(
function NavLabel({ label, className, ...otherProps }: NavLabelProps, ref) {
return (
<div
className={SubframeUtils.twClassNames(
"flex w-full flex-col items-start gap-4 px-3 py-1",
className
)}
ref={ref}
{...otherProps}
>
{label ? (
<span className="w-full text-body-extra-small-medium-0 font-body-extra-small-medium-0 text-text-neutral-secondary">
{label}
</span>
) : null}
</div>
);
}
);
export interface NavDividerProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
}
const NavDivider = React.forwardRef<HTMLDivElement, NavDividerProps>(
function NavDivider({ className, ...otherProps }: NavDividerProps, ref) {
return (
<div
className={SubframeUtils.twClassNames(
"flex w-full flex-col items-center justify-center gap-2 px-1 pt-2",
className
)}
ref={ref}
{...otherProps}
>
<div className="flex h-px w-full flex-none flex-col items-center justify-center gap-2 bg-border-default" />
</div>
);
}
);
export interface SubcomponentProps
extends React.HTMLAttributes<HTMLDivElement> {
sidebarButton?: "close" | "open";
className?: string;
}
const Subcomponent = React.forwardRef<HTMLDivElement, SubcomponentProps>(
function Subcomponent(
{ sidebarButton = "close", className, ...otherProps }: SubcomponentProps,
ref
) {
return (
<div
className={SubframeUtils.twClassNames(
"group/7ce7d712 flex flex-col items-start gap-2",
className
)}
ref={ref}
{...otherProps}
>
<IconButton
variant="tertiary"
size="small"
square={true}
icon={
sidebarButton === "open" ? (
<FeatherSidebarOpen />
) : (
<FeatherSidebarClose />
)
}
/>
</div>
);
}
);
export interface SidebarWithSectionsRootProps
extends React.HTMLAttributes<HTMLElement> {
variant?: "open" | "closed";
footer?: React.ReactNode;
children?: React.ReactNode;
className?: string;
}
const SidebarWithSectionsRoot = React.forwardRef<
HTMLElement,
SidebarWithSectionsRootProps
>(function SidebarWithSectionsRoot(
{
variant = "open",
footer,
children,
className,
...otherProps
}: SidebarWithSectionsRootProps,
ref
) {
return (
<nav
className={SubframeUtils.twClassNames(
"group/f4047c8b flex h-full w-60 flex-col items-start rounded-l-md border-r border-solid border-border-default bg-surface-neutral-secondary relative",
{ "w-auto max-w-[64px]": variant === "closed" },
className
)}
ref={ref}
{...otherProps}
>
<Subcomponent
className="absolute -right-[17px] top-[40px] z-10"
sidebarButton={variant === "closed" ? "open" : undefined}
/>
<div
className={SubframeUtils.twClassNames(
"flex h-14 w-full flex-none items-center justify-center gap-2 border-b border-solid border-border-default px-4",
{ "w-16 px-0 py-0": variant === "closed" }
)}
>
<div className="flex items-center justify-center gap-2">
<Logo
size="32"
showIcon={variant === "closed" ? true : undefined}
hideText={variant === "closed" ? true : undefined}
/>
</div>
</div>
{children ? (
<div
className={SubframeUtils.twClassNames(
"flex w-full grow shrink-0 basis-0 flex-col items-start px-2 py-2 overflow-auto",
{ "w-16": variant === "closed" }
)}
>
{children}
</div>
) : null}
{footer ? (
<div
className={SubframeUtils.twClassNames(
"hidden w-full items-center gap-4 border-t border-solid border-border-default px-6 py-6",
{ "justify-center": variant === "closed" }
)}
>
{footer}
</div>
) : null}
</nav>
);
});
export const SidebarWithSections = Object.assign(SidebarWithSectionsRoot, {
NavItem,
NavSection,
NavLabel,
NavDivider,
Subcomponent,
});