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
317 lines
9.1 KiB
TypeScript
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,
|
|
});
|