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
300 lines
8.4 KiB
TypeScript
300 lines
8.4 KiB
TypeScript
"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";
|
|
|
|
export interface NavItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
icon?: React.ReactNode;
|
|
children?: React.ReactNode;
|
|
selected?: boolean;
|
|
variant?: "icon-and-text" | "icon-only";
|
|
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/7aa51c7e flex w-full cursor-pointer flex-col items-start gap-1 pt-2",
|
|
{ "w-auto items-center": variant === "icon-only" },
|
|
className
|
|
)}
|
|
ref={ref}
|
|
{...otherProps}
|
|
>
|
|
<div
|
|
className={SubframeUtils.twClassNames(
|
|
"flex w-full flex-col items-start gap-2",
|
|
{ "w-auto flex-none items-center": variant === "icon-only" }
|
|
)}
|
|
>
|
|
<div
|
|
className={SubframeUtils.twClassNames(
|
|
"flex w-full items-center gap-1.5 rounded-sm px-2.5 py-1.5 group-hover/7aa51c7e:bg-surface-neutral-tertiary",
|
|
{
|
|
"w-auto flex-none items-start justify-center px-1.5":
|
|
variant === "icon-only",
|
|
"bg-surface-neutral-tertiary group-hover/7aa51c7e:bg-surface-neutral-modal group-active/7aa51c7e:bg-surface-neutral-tertiary":
|
|
selected,
|
|
}
|
|
)}
|
|
>
|
|
{icon ? (
|
|
<SubframeCore.IconWrapper className="text-body-default-semibold-0 font-body-default-semibold-0 text-icon-neutral-primary group-active/7aa51c7e: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/7aa51c7e: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/c119f2b2 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,
|
|
})}
|
|
>
|
|
<SidebarInside.NavDivider />
|
|
</div>
|
|
<div
|
|
className={SubframeUtils.twClassNames("hidden w-full items-start", {
|
|
flex: showLabel,
|
|
})}
|
|
>
|
|
<SidebarInside.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/21460331 flex h-8 w-8 flex-col items-center justify-center gap-2",
|
|
className
|
|
)}
|
|
ref={ref}
|
|
{...otherProps}
|
|
>
|
|
<IconButton
|
|
variant="tertiary"
|
|
size="extra-small"
|
|
square={true}
|
|
icon={
|
|
sidebarButton === "open" ? (
|
|
<FeatherSidebarOpen />
|
|
) : (
|
|
<FeatherSidebarClose />
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
export interface SidebarInsideRootProps
|
|
extends React.HTMLAttributes<HTMLElement> {
|
|
variant?: "open" | "closed";
|
|
footer?: React.ReactNode;
|
|
children?: React.ReactNode;
|
|
visible?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
const SidebarInsideRoot = React.forwardRef<HTMLElement, SidebarInsideRootProps>(
|
|
function SidebarInsideRoot(
|
|
{
|
|
variant = "open",
|
|
footer,
|
|
children,
|
|
visible = false,
|
|
className,
|
|
...otherProps
|
|
}: SidebarInsideRootProps,
|
|
ref
|
|
) {
|
|
return (
|
|
<nav
|
|
className={SubframeUtils.twClassNames(
|
|
"group/f6538807 hidden h-full min-w-[208px] flex-col items-start rounded-l-md border-r border-solid border-border-default bg-surface-neutral-secondary relative",
|
|
{
|
|
flex: visible,
|
|
"w-12 items-center justify-center": variant === "closed",
|
|
},
|
|
className
|
|
)}
|
|
ref={ref}
|
|
{...otherProps}
|
|
>
|
|
<Subcomponent
|
|
className="absolute -right-[17px] top-[0px] z-10"
|
|
sidebarButton={variant === "closed" ? "open" : undefined}
|
|
/>
|
|
{children ? (
|
|
<div
|
|
className={SubframeUtils.twClassNames(
|
|
"flex w-full grow shrink-0 basis-0 flex-col items-start px-2 pt-1 pb-2 overflow-auto",
|
|
{ "items-center": 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 SidebarInside = Object.assign(SidebarInsideRoot, {
|
|
NavItem,
|
|
NavSection,
|
|
NavLabel,
|
|
NavDivider,
|
|
Subcomponent,
|
|
});
|