feat(ui): Marke und Grundkomponenten (#002)
Bildmarke als Inline-SVG und Wortmarke, dazu Karte, Button, Badge, Statuspunkt, Tabelle, Kennzahl, Diff-Box, Breadcrumb, Seitenkopf, Leerzustand und MonoKopieren. Die Bausteine kapseln die Designregeln (44 px Klickflaeche, Status nie nur ueber Farbe, Tabellen scrollen im eigenen Container), damit spaetere Seiten sie nicht neu erfinden. Nur MonoKopieren ist Client-Komponente, weil sie die Zwischenablage braucht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { clsx } from "clsx";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Karte: --surface, 1 px Rahmen, 16 px Radius, 22–24 px Innenabstand (#002).
|
||||
* Mit `titel` entsteht ein Kopf mit optionalen Aktionen rechts.
|
||||
*/
|
||||
export function Karte({
|
||||
titel,
|
||||
untertitel,
|
||||
aktionen,
|
||||
children,
|
||||
className,
|
||||
innen = "normal",
|
||||
as: Element = "section",
|
||||
}: {
|
||||
titel?: ReactNode;
|
||||
untertitel?: ReactNode;
|
||||
aktionen?: ReactNode;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
/** „ohne“ für Inhalte, die bis an den Rand reichen (z. B. Tabellen mit eigenem Abstand). */
|
||||
innen?: "normal" | "ohne";
|
||||
as?: "section" | "div" | "article";
|
||||
}) {
|
||||
return (
|
||||
<Element
|
||||
className={clsx(
|
||||
"border-border bg-surface min-w-0 rounded-karte border",
|
||||
innen === "normal" && "p-5 sm:p-6",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{(titel || aktionen) && (
|
||||
<KartenKopf titel={titel} untertitel={untertitel} aktionen={aktionen} rand={innen === "ohne"} />
|
||||
)}
|
||||
{children}
|
||||
</Element>
|
||||
);
|
||||
}
|
||||
|
||||
function KartenKopf({
|
||||
titel,
|
||||
untertitel,
|
||||
aktionen,
|
||||
rand,
|
||||
}: {
|
||||
titel?: ReactNode;
|
||||
untertitel?: ReactNode;
|
||||
aktionen?: ReactNode;
|
||||
rand: boolean;
|
||||
}) {
|
||||
return (
|
||||
<header
|
||||
className={clsx(
|
||||
"mb-4 flex flex-wrap items-center justify-between gap-x-4 gap-y-2",
|
||||
rand && "px-5 pt-5 sm:px-6 sm:pt-6",
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
{titel && <h2 className="text-text text-[17px] leading-snug font-semibold">{titel}</h2>}
|
||||
{untertitel && <p className="text-muted mt-0.5 text-sm">{untertitel}</p>}
|
||||
</div>
|
||||
{aktionen && <div className="flex shrink-0 flex-wrap items-center gap-2">{aktionen}</div>}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
/** Innere Box (--sidebar, 12 px Radius) – für Einträge innerhalb einer Karte. */
|
||||
export function InnereBox({ children, className }: { children: ReactNode; className?: string }) {
|
||||
return (
|
||||
<div className={clsx("border-border bg-sidebar rounded-box min-w-0 border p-4", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user