Refactor site header, heroes, and design system

This commit is contained in:
MOH
2026-03-08 00:29:51 +01:00
parent a18370d003
commit 0c2b67e378
56 changed files with 2714 additions and 1318 deletions
+63
View File
@@ -0,0 +1,63 @@
"use client";
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("rounded-surface border border-input bg-card", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between gap-4 px-5 py-4 text-left text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
"overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
className,
)}
{...props}
>
<div className="border-t border-border px-5 py-5">{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
+5 -5
View File
@@ -5,14 +5,14 @@ import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
const appCardVariants = cva(
"rounded-lg transition-colors",
"rounded-surface transition-colors",
{
variants: {
level: {
1: "border bg-card text-card-foreground shadow-sm",
2: "border bg-card text-card-foreground shadow-sm",
3: "border bg-card text-card-foreground shadow-md",
inverse: "border-transparent bg-foreground text-background shadow-sm",
1: "border border-border/80 bg-card text-card-foreground shadow-card",
2: "border border-border/80 bg-card text-card-foreground shadow-card",
3: "border border-border/80 bg-card text-card-foreground shadow-panel",
inverse: "border-transparent bg-foreground text-background shadow-card",
},
padding: {
none: "",
+4 -4
View File
@@ -5,13 +5,13 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-nested text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/92",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/90",
outline: "border border-input bg-background hover:border-border-strong hover:bg-accent hover:text-accent-foreground",
ghost: "text-muted-foreground hover:bg-muted hover:text-foreground",
link: "rounded-none text-primary underline-offset-4 hover:underline",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+1 -1
View File
@@ -7,7 +7,7 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
"rounded-surface border border-border/80 bg-card text-card-foreground shadow-card",
className,
)}
{...props}
+104
View File
@@ -0,0 +1,104 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-foreground/30 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:w-full sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
);
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)} {...props} />
);
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
};
+6 -6
View File
@@ -22,7 +22,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
"flex cursor-default select-none items-center rounded-nested px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className,
)}
@@ -42,7 +42,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"z-50 min-w-[8rem] overflow-hidden rounded-surface border border-border/80 bg-popover p-1 text-popover-foreground shadow-panel",
className,
)}
{...props}
@@ -60,7 +60,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"z-50 min-w-[12rem] overflow-hidden rounded-surface border border-border/80 bg-popover p-1 text-popover-foreground shadow-panel",
className,
)}
{...props}
@@ -79,7 +79,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-nested px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
@@ -96,7 +96,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-nested py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
@@ -121,7 +121,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-nested py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
+1 -1
View File
@@ -7,7 +7,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base text-foreground ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"flex h-10 w-full rounded-nested border border-input bg-background px-3 py-2 text-base text-foreground shadow-xs ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
+6 -6
View File
@@ -55,7 +55,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
"flex h-10 w-full items-center justify-between rounded-nested border border-input bg-background px-3 py-2 text-sm shadow-xs ring-offset-background placeholder:text-muted-foreground focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring/30 focus:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
@@ -105,9 +105,9 @@ const SelectContent = React.forwardRef<
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[10rem] overflow-y-auto overflow-x-hidden rounded-surface border border-input bg-background text-foreground shadow-panel data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
@@ -151,8 +151,8 @@ const SelectItem = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-nested py-2 pl-8 pr-3 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
+2 -2
View File
@@ -44,7 +44,7 @@ function TabsList({
return (
<div
className={cn(
"inline-flex h-auto flex-wrap items-center gap-2 rounded-lg border bg-muted/40 p-1.5",
"inline-flex h-auto flex-wrap items-center gap-2 rounded-surface border border-border/80 bg-muted/40 p-1.5",
className,
)}
{...props}
@@ -69,7 +69,7 @@ function TabsTrigger({
<button
type="button"
className={cn(
"inline-flex items-center justify-center rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center rounded-nested px-3 py-2 text-sm font-medium text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50",
isActive && "bg-background text-foreground shadow-sm",
className,
)}
+1 -1
View File
@@ -6,7 +6,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"tex
({ className, ...props }, ref) => (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base text-foreground ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"flex min-h-[80px] w-full rounded-nested border border-input bg-background px-3 py-2 text-base text-foreground shadow-xs ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
+57 -2
View File
@@ -53,6 +53,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
surfaces: "Surfaces",
typography: "Typography",
headers: "Headers",
workspace: "Workspace",
cardsDesc:
"Aussenflaechen, innere Karten und verschachtelte Ebenen mit einheitlicher Hierarchie.",
buttonsDesc:
@@ -67,6 +68,8 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
"Typografische Grundstufen fuer Seiten, Sektionen und Metainformationen.",
headersDesc:
"Header Muster fuer Seiten und Sektionen innerhalb des Systems.",
workspaceDesc:
"Editor Muster mit breitem Inhaltsbereich und schmaler Kontextspalte fuer Admin Oberflaechen.",
tabsDesc:
"Tabs bleiben in einer Route und verwenden dieselbe verschachtelte Steuerlogik.",
outerCard: "Aeussere Card",
@@ -128,7 +131,12 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
outerSurfaceHeaderText: "Dieses Muster wird fuer Seitensektionen mit staerkerer Hierarchie genutzt.",
sectionHeader: "Abschnittsheader",
nestedBlockHeading: "Ueberschrift des verschachtelten Blocks",
nestedBlockHeadingText: "Innerhalb einer aeusseren Card verwenden, wenn Inhalte bereits gruppiert sind."
nestedBlockHeadingText: "Innerhalb einer aeusseren Card verwenden, wenn Inhalte bereits gruppiert sind.",
workspaceHeader: "Editor Workspace",
workspaceMain: "Breiter Inhaltsbereich",
workspaceMainText: "Hier liegen Formschritte, Listen und eigentliche Bearbeitung.",
workspaceSide: "Kontextspalte",
workspaceSideText: "Status, Checklisten und Fokusdetails bleiben immer sichtbar."
}
: {
title: "UI Kit",
@@ -142,6 +150,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
surfaces: "Surfaces",
typography: "Typography",
headers: "Headers",
workspace: "Workspace",
cardsDesc:
"Outer surfaces, inner cards, and nested levels with one consistent hierarchy.",
buttonsDesc:
@@ -156,6 +165,8 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
"Typography foundations for pages, sections, and supporting copy.",
headersDesc:
"Header patterns for pages and sections inside the system.",
workspaceDesc:
"Editor pattern with a wide content area and a narrow context rail for admin workflows.",
tabsDesc:
"Tabs stay within one route and use the same nested control logic.",
outerCard: "Outer card",
@@ -217,7 +228,12 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
outerSurfaceHeaderText: "This pattern is used for page-level sections that need stronger hierarchy.",
sectionHeader: "Section header",
nestedBlockHeading: "Nested block heading",
nestedBlockHeadingText: "Use inside an outer card when content is already grouped."
nestedBlockHeadingText: "Use inside an outer card when content is already grouped.",
workspaceHeader: "Editor workspace",
workspaceMain: "Wide content area",
workspaceMainText: "This area carries form steps, lists, and direct editing work.",
workspaceSide: "Context rail",
workspaceSideText: "Status, checklists, and focus details stay visible at all times."
};
return (
@@ -249,6 +265,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
<TabsTrigger value="surfaces">{copy.surfaces}</TabsTrigger>
<TabsTrigger value="typography">{copy.typography}</TabsTrigger>
<TabsTrigger value="headers">{copy.headers}</TabsTrigger>
<TabsTrigger value="workspace">{copy.workspace}</TabsTrigger>
</TabsList>
<TabsContent value="cards">
@@ -516,6 +533,44 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
</div>
</UiKitSection>
</TabsContent>
<TabsContent value="workspace">
<UiKitSection title={copy.workspace} description={copy.workspaceDesc}>
<div className="grid gap-6 xl:grid-cols-3">
<AppCard level={2} className="xl:col-span-2">
<CardContent className="space-y-4 p-6">
<div>
<p className="text-sm font-medium text-muted-foreground">{copy.workspaceMain}</p>
<h3 className="mt-2 text-xl font-semibold text-foreground">{copy.workspaceHeader}</h3>
</div>
<p className="text-sm text-muted-foreground">{copy.workspaceMainText}</p>
<div className="grid gap-3 md:grid-cols-2">
<div className="rounded-lg border border-input bg-background p-4 text-sm text-muted-foreground">
Step 1
</div>
<div className="rounded-lg border border-input bg-background p-4 text-sm text-muted-foreground">
Step 2
</div>
</div>
</CardContent>
</AppCard>
<AppCard level={2}>
<CardContent className="space-y-4 p-6">
<p className="text-sm font-medium text-muted-foreground">{copy.workspaceSide}</p>
<p className="text-sm text-muted-foreground">{copy.workspaceSideText}</p>
<div className="space-y-3">
<Badge variant="success">Ready</Badge>
<Badge variant="warning">Open</Badge>
<div className="rounded-lg border border-input bg-background p-4 text-sm text-muted-foreground">
Checklist / status / focus details
</div>
</div>
</CardContent>
</AppCard>
</div>
</UiKitSection>
</TabsContent>
</Tabs>
</div>
);