Files
Dashboard/ui/components/ProjectCard/ProjectCard.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

358 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @subframe/sync-disable
"use client";
/*
* Documentation:
* ProjectCard — https://app.subframe.com/f023064a5ca0/library?component=ProjectCard_f82a72d5-da31-4d61-b47b-6b6ebd18ff67
*/
import React from "react";
import { FeatherCalendarClock } from "@subframe/core";
import { FeatherFlag } from "@subframe/core";
import { FeatherPlayCircle } from "@subframe/core";
import * as SubframeUtils from "../../utils";
export interface PriorityIndicatorProps
extends React.HTMLAttributes<HTMLDivElement> {
variant?: "none" | "low" | "medium" | "high" | "critical";
className?: string;
}
const PriorityIndicator = React.forwardRef<
HTMLDivElement,
PriorityIndicatorProps
>(function PriorityIndicator(
{ variant = "none", className, ...otherProps }: PriorityIndicatorProps,
ref
) {
return (
<div
className={SubframeUtils.twClassNames(
"group/ba2d0229 flex h-8 items-center gap-0.5 rounded-md border-2 border-solid border-border-default bg-surface-neutral-secondary px-1.5 py-1.5",
{
"border-2 border-solid border-surface-ui-solid-purple-default":
variant === "critical",
"border-2 border-solid border-border-negative-solid":
variant === "high",
"border-2 border-solid border-border-warning-subtle":
variant === "medium",
"border-2 border-solid border-border-positive-subtle":
variant === "low",
},
className
)}
ref={ref}
{...otherProps}
>
<div
className={SubframeUtils.twClassNames(
"flex h-4 w-1.5 flex-none items-center rounded-sm bg-border-default",
{
"bg-surface-ui-solid-purple-default": variant === "critical",
"bg-surface-feedback-solid-negative-default": variant === "high",
"bg-surface-ui-subtle-yellow-active": variant === "medium",
"bg-surface-feedback-solid-positive-default": variant === "low",
}
)}
/>
<div
className={SubframeUtils.twClassNames(
"hidden h-4 w-1.5 flex-none items-center rounded-sm bg-surface-ui-subtle-yellow-active",
{ flex: variant === "medium" }
)}
/>
<div
className={SubframeUtils.twClassNames(
"flex h-4 w-1.5 flex-none items-start rounded-sm bg-border-default",
{
"bg-surface-ui-solid-purple-default": variant === "critical",
"bg-surface-ui-solid-red-default": variant === "high",
hidden: variant === "medium",
}
)}
/>
<div
className={SubframeUtils.twClassNames(
"flex h-4 w-1.5 flex-none items-start rounded-sm bg-border-default",
{
"bg-surface-ui-solid-purple-default": variant === "critical",
"bg-surface-ui-solid-red-default": variant === "high",
}
)}
/>
<div
className={SubframeUtils.twClassNames(
"flex h-4 w-1.5 flex-none items-start rounded-sm bg-border-default",
{
"bg-surface-ui-solid-purple-hover": variant === "critical",
"bg-brand-brand-surface-subtle-default": variant === "high",
}
)}
/>
</div>
);
});
export interface PriorityBadgeProps
extends React.HTMLAttributes<HTMLDivElement> {
variant?: "none" | "low" | "medium" | "high" | "critical";
className?: string;
}
const PriorityBadge = React.forwardRef<HTMLDivElement, PriorityBadgeProps>(
function PriorityBadge(
{ variant = "none", className, ...otherProps }: PriorityBadgeProps,
ref
) {
return (
<div
className={SubframeUtils.twClassNames(
"group/94453bd3 hidden items-center rounded-sm border-2 border-solid border-surface-ui-solid-purple-default bg-surface-ui-subtle-purple-default px-1.5 py-0.5",
{
flex: variant === "critical",
"flex border-2 border-solid border-border-negative-solid bg-surface-feedback-subtle-negative":
variant === "high",
"flex border-2 border-solid border-border-warning-subtle bg-surface-feedback-subtle-warning":
variant === "medium",
"flex border-2 border-solid border-border-positive-subtle bg-surface-feedback-subtle-positive":
variant === "low",
},
className
)}
ref={ref}
{...otherProps}
>
<span
className={SubframeUtils.twClassNames(
"whitespace-nowrap text-body-small-semibold-0 font-body-small-semibold-0 text-text-neutral-secondary",
{
"text-text-ui-purple": variant === "critical",
"text-text-feedback-negative": variant === "high",
"text-text-feedback-warning": variant === "medium",
"text-text-feedback-positive": variant === "low",
}
)}
>
{variant === "critical"
? "Kritisch"
: variant === "high"
? "Hoch"
: variant === "medium"
? "Mittel"
: variant === "low"
? "Niedrig"
: "–"}
</span>
</div>
);
}
);
export interface ProjectCardRootProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
title?: React.ReactNode;
subtitle?: React.ReactNode;
tags?: React.ReactNode;
price?: React.ReactNode;
priorityLabel?: React.ReactNode;
avatar?: React.ReactNode;
startDate?: React.ReactNode;
endDate?: React.ReactNode;
appointmentLabel?: React.ReactNode;
secondAppointmentLabel?: React.ReactNode;
variant?: "none" | "low" | "medium" | "high" | "critical";
hideBadge?: boolean;
hideIndicator?: boolean;
hideAvatar?: boolean;
hideDateRange?: boolean;
hideAppointment?: boolean;
hideSecondAppointment?: boolean;
className?: string;
}
const ProjectCardRoot = React.forwardRef<HTMLDivElement, ProjectCardRootProps>(
function ProjectCardRoot(
{
title,
subtitle,
tags,
price,
priorityLabel,
avatar,
startDate,
endDate,
appointmentLabel,
secondAppointmentLabel,
variant = "none",
hideBadge = false,
hideIndicator = false,
hideAvatar = false,
hideDateRange = false,
hideAppointment = false,
hideSecondAppointment = false,
className,
...otherProps
}: ProjectCardRootProps,
ref
) {
return (
<div
className={SubframeUtils.twClassNames(
"group/f82a72d5 flex w-80 cursor-pointer flex-col items-start overflow-hidden rounded-lg border border-solid border-border-default bg-surface-neutral-secondary shadow-sm transition-shadow duration-150 hover:shadow-md",
className
)}
ref={ref}
{...otherProps}
>
<div
className={SubframeUtils.twClassNames(
"flex w-full flex-col items-start px-3.5 pt-3.5"
)}
>
{title ? (
<span className="line-clamp-2 w-full text-body-default-semibold-0 font-body-default-semibold-0 text-text-neutral-primary">
{title}
</span>
) : null}
{subtitle ? (
<span className="line-clamp-1 w-full text-body-small-regular-0 font-body-small-regular-0 text-text-neutral-secondary">
{subtitle}
</span>
) : null}
</div>
{tags ? (
<div className="flex w-full flex-wrap items-center gap-1 px-3.5 pt-2">
<div className="flex max-h-[44px] flex-wrap items-start gap-1 overflow-hidden">
{tags}
</div>
</div>
) : null}
<div className="flex w-full items-center px-3.5 pt-2 pb-4 relative z-10">
{price ? (
<span className="text-subtitle-0 font-subtitle-0 text-text-neutral-primary">
{price}
</span>
) : null}
<div className="flex items-center gap-1.5 absolute -bottom-4 right-2">
<div className="flex items-center gap-1">
<PriorityBadge
className={SubframeUtils.twClassNames({ hidden: hideBadge })}
variant={
variant === "critical"
? "critical"
: variant === "high"
? "high"
: variant === "medium"
? "medium"
: variant === "low"
? "low"
: "none"
}
/>
<PriorityIndicator
className={SubframeUtils.twClassNames({
hidden: hideIndicator,
})}
variant={
variant === "critical"
? "critical"
: variant === "high"
? "high"
: variant === "medium"
? "medium"
: variant === "low"
? "low"
: "none"
}
/>
</div>
{avatar ? (
<div
className={SubframeUtils.twClassNames(
"flex items-center gap-1.5 rounded-[9999px] ring-2 ring-border-default",
{
hidden: hideAvatar,
"ring-surface-ui-solid-purple-default":
variant === "critical",
"ring-border-negative-solid": variant === "high",
"ring-border-warning-subtle": variant === "medium",
"ring-border-positive-subtle": variant === "low",
}
)}
>
{avatar}
</div>
) : null}
</div>
</div>
<div
className={SubframeUtils.twClassNames(
"flex w-full flex-col items-start gap-2 overflow-hidden rounded-md border-2 border-solid border-border-default bg-surface-neutral-tertiary px-3 pt-6 pb-2.5 relative z-0",
{
"border-2 border-solid border-surface-ui-subtle-purple-active bg-surface-ui-subtle-purple-default":
variant === "critical",
"border-2 border-solid border-border-negative-subtle bg-surface-feedback-subtle-negative":
variant === "high",
"border-2 border-solid border-border-warning-subtle bg-surface-feedback-subtle-warning":
variant === "medium",
"border-2 border-solid border-border-positive-subtle bg-surface-feedback-subtle-positive":
variant === "low",
}
)}
>
<div
className={SubframeUtils.twClassNames(
"flex w-full items-center gap-1.5",
{ hidden: hideDateRange }
)}
>
<FeatherPlayCircle className="text-body-default-regular-0 font-body-default-regular-0 text-icon-neutral-primary" />
{startDate ? (
<span className="whitespace-nowrap text-body-small-medium-0 font-body-small-medium-0 text-text-neutral-primary">
{startDate}
</span>
) : null}
<div className="flex h-px grow shrink-0 basis-0 items-start bg-text-neutral-primary" />
{endDate ? (
<span className="whitespace-nowrap text-body-small-medium-0 font-body-small-medium-0 text-text-neutral-primary">
{endDate}
</span>
) : null}
<FeatherFlag className="text-body-default-regular-0 font-body-default-regular-0 text-icon-neutral-primary" />
</div>
<div
className={SubframeUtils.twClassNames(
"flex w-full items-center justify-center gap-1.5 rounded-round border border-solid border-border-default bg-surface-neutral-secondary px-2 py-1",
{ hidden: hideAppointment }
)}
>
<FeatherCalendarClock className="text-body-extra-small-regular-0 font-body-extra-small-regular-0 text-icon-neutral-primary" />
{appointmentLabel ? (
<span className="whitespace-nowrap text-body-extra-small-medium-0 font-body-extra-small-medium-0 text-text-neutral-primary">
{appointmentLabel}
</span>
) : null}
</div>
{secondAppointmentLabel ? (
<div
className={SubframeUtils.twClassNames(
"flex w-full items-center justify-center gap-1.5 rounded-round border border-solid border-border-default bg-surface-neutral-secondary px-2 py-1",
{ hidden: hideSecondAppointment }
)}
>
<FeatherCalendarClock className="text-body-extra-small-regular-0 font-body-extra-small-regular-0 text-icon-neutral-primary" />
<span className="whitespace-nowrap text-body-extra-small-medium-0 font-body-extra-small-medium-0 text-text-neutral-primary">
{secondAppointmentLabel}
</span>
</div>
) : null}
</div>
</div>
);
}
);
export const ProjectCard = Object.assign(ProjectCardRoot, {
PriorityIndicator,
PriorityBadge,
});