STYLED - Roll out the layered card system to the admin workspace

The double-layer AppCard look showcased in the UI Kit was only applied on
the public frontend; every admin surface was pinned to layer=single and
stayed flat. Convert the admin section/container cards to the default
layered look, keeping compact metric tiles, toolbars, image tiles and
nested leaf cards single.

Add AppCard contentClassName so content utilities (space-y-*, grid, flex)
keep applying to the inner shell after the switch to double layer.
This commit is contained in:
moh
2026-09-20 17:01:20 +02:00
parent 4b535694bc
commit d1245204f1
12 changed files with 46 additions and 33 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ export default async function AdminMaintenancePage({
headerDescription={copy.subtitle} headerDescription={copy.subtitle}
> >
<MotionFade delay={0.1}> <MotionFade delay={0.1}>
<AppCard layer="single"> <AppCard>
<CardContent className="space-y-4 p-6"> <CardContent className="space-y-4 p-6">
<p className="text-sm text-muted-foreground">{copy.maintenanceText}</p> <p className="text-sm text-muted-foreground">{copy.maintenanceText}</p>
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
+1 -1
View File
@@ -104,7 +104,7 @@ export default async function AdminPortfolioProjectPage({
</MotionFade> </MotionFade>
<MotionFade delay={0.2}> <MotionFade delay={0.2}>
<AppCard level={2} layer="single"> <AppCard level={2}>
<CardContent className="flex flex-col gap-4 p-6 lg:flex-row lg:items-center lg:justify-between"> <CardContent className="flex flex-col gap-4 p-6 lg:flex-row lg:items-center lg:justify-between">
<div> <div>
<p className="text-sm font-medium text-foreground">{copy.dangerZone}</p> <p className="text-sm font-medium text-foreground">{copy.dangerZone}</p>
+1 -1
View File
@@ -29,7 +29,7 @@ export function MarqueeSettingsForm({
<div className="grid gap-4 xl:grid-cols-4"> <div className="grid gap-4 xl:grid-cols-4">
{rowMeta.map((row) => ( {rowMeta.map((row) => (
<AppCard key={`de-${row.key}`} level={2} layer="single" padding="sm" className="space-y-2"> <AppCard key={`de-${row.key}`} level={2} padding="sm" contentClassName="space-y-2">
<Label htmlFor={`${row.key}-de`} className="text-sm font-semibold text-foreground">{row.label}</Label> <Label htmlFor={`${row.key}-de`} className="text-sm font-semibold text-foreground">{row.label}</Label>
<Textarea <Textarea
id={`${row.key}-de`} id={`${row.key}-de`}
+1 -1
View File
@@ -89,7 +89,7 @@ export function MediaFieldPicker({
}, [serializedValue]); }, [serializedValue]);
return ( return (
<AppCard level={2} layer="single" padding="sm" className="space-y-4"> <AppCard level={2} padding="sm" contentClassName="space-y-4">
<input ref={hiddenInputRef} type="hidden" name={inputName} value={serializedValue} /> <input ref={hiddenInputRef} type="hidden" name={inputName} value={serializedValue} />
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
+1 -1
View File
@@ -486,7 +486,7 @@ export function MediaLibraryManager({
) )
) : ( ) : (
<MotionFade delay={0.18}> <MotionFade delay={0.18}>
<AppCard layer="single"> <AppCard>
<CardContent className="p-6 text-sm text-muted-foreground"> <CardContent className="p-6 text-sm text-muted-foreground">
{copy.empty} {copy.empty}
</CardContent> </CardContent>
@@ -110,7 +110,7 @@ function CategoryLocaleFields({
const descriptionKey = `description${locale.key}` as const; const descriptionKey = `description${locale.key}` as const;
return ( return (
<AppCard key={`${idPrefix}-${locale.key}`} level={2} layer="single" padding="sm" className="space-y-4 rounded-nested"> <AppCard key={`${idPrefix}-${locale.key}`} level={2} padding="sm" className="rounded-nested" contentClassName="space-y-4">
<p className="text-sm font-medium text-foreground">{locale.label}</p> <p className="text-sm font-medium text-foreground">{locale.label}</p>
<div className="space-y-2"> <div className="space-y-2">
@@ -168,7 +168,7 @@ function CategoryStatusFields({
</div> </div>
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]"> <div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
<AppCard level={2} layer="single" padding="sm" className="rounded-nested"> <AppCard level={2} padding="sm" className="rounded-nested">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
{isActive ? ( {isActive ? (
<ShieldCheck className="mt-0.5 h-4 w-4 text-status-success" /> <ShieldCheck className="mt-0.5 h-4 w-4 text-status-success" />
@@ -395,7 +395,7 @@ export function PortfolioCategoriesManager({
<DialogDescription>{copy.modalDescription}</DialogDescription> <DialogDescription>{copy.modalDescription}</DialogDescription>
</DialogHeader> </DialogHeader>
<AppCard level={2} layer="single" padding="sm" className="rounded-nested"> <AppCard level={2} padding="sm" className="rounded-nested">
<div className="grid gap-3 sm:grid-cols-3"> <div className="grid gap-3 sm:grid-cols-3">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-sm font-medium text-foreground">1. Basics</p> <p className="text-sm font-medium text-foreground">1. Basics</p>
@@ -426,7 +426,7 @@ export function PortfolioCategoriesManager({
</Dialog> </Dialog>
</div> </div>
<AppCard level={3} layer="single"> <AppCard level={3}>
<CardContent className="space-y-4 p-6"> <CardContent className="space-y-4 p-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Layers3 className="h-5 w-5 text-brand-primary" /> <Layers3 className="h-5 w-5 text-brand-primary" />
+7 -7
View File
@@ -528,7 +528,7 @@ export function PortfolioProjectForm({
if (!project && availableCategories.length === 0) { if (!project && availableCategories.length === 0) {
return ( return (
<AppCard level={3} layer="single" padding="lg" className="rounded-surface"> <AppCard level={3} padding="lg" className="rounded-surface">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">Portfolio Setup</p> <p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">Portfolio Setup</p>
@@ -577,7 +577,7 @@ export function PortfolioProjectForm({
<input ref={assetsInputRef} type="hidden" name="assets" value={assetsPayload} /> <input ref={assetsInputRef} type="hidden" name="assets" value={assetsPayload} />
<input ref={intentRef} type="hidden" name="intent" defaultValue="save" /> <input ref={intentRef} type="hidden" name="intent" defaultValue="save" />
<AppCard level={3} layer="single" padding="lg" className="space-y-6"> <AppCard level={3} padding="lg" contentClassName="space-y-6">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between"> <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-3"> <div className="space-y-3">
@@ -625,7 +625,7 @@ export function PortfolioProjectForm({
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<AppCard level={3} layer="single" padding="lg"> <AppCard level={3} padding="lg">
<section className="space-y-6"> <section className="space-y-6">
<SectionHeader <SectionHeader
title="Basic Information" title="Basic Information"
@@ -787,7 +787,7 @@ export function PortfolioProjectForm({
</div> </div>
<div> <div>
<AppCard level={3} layer="single" padding="lg"> <AppCard level={3} padding="lg">
<section className="space-y-6"> <section className="space-y-6">
<SectionHeader <SectionHeader
title="Localized Content" title="Localized Content"
@@ -838,7 +838,7 @@ export function PortfolioProjectForm({
</div> </div>
<div> <div>
<AppCard level={3} layer="single" padding="lg"> <AppCard level={3} padding="lg">
<section className="space-y-6"> <section className="space-y-6">
<SectionHeader <SectionHeader
title="Sections" title="Sections"
@@ -1028,7 +1028,7 @@ export function PortfolioProjectForm({
</div> </div>
<div> <div>
<AppCard level={3} layer="single" padding="lg"> <AppCard level={3} padding="lg">
<div className="space-y-8"> <div className="space-y-8">
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_280px]"> <div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_280px]">
<div className="space-y-6"> <div className="space-y-6">
@@ -1203,7 +1203,7 @@ export function PortfolioProjectForm({
</div> </div>
</div> </div>
<AppCard level={3} layer="single" padding="lg"> <AppCard level={3} padding="lg">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-end"> <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-end">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
{firstIncompleteStep ? ( {firstIncompleteStep ? (
@@ -85,7 +85,7 @@ export async function PortfolioProjectsOverview({
<div className="grid gap-4 xl:grid-cols-2"> <div className="grid gap-4 xl:grid-cols-2">
{projects.map((project, index) => ( {projects.map((project, index) => (
<MotionFade key={project.id} delay={0.06 + index * 0.03}> <MotionFade key={project.id} delay={0.06 + index * 0.03}>
<AppCard interactive layer="single" className="h-full"> <AppCard interactive className="h-full">
<CardContent className="flex flex-col gap-4 p-5 lg:flex-row lg:items-center lg:justify-between"> <CardContent className="flex flex-col gap-4 p-5 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
@@ -122,7 +122,7 @@ export async function PortfolioProjectsOverview({
))} ))}
{projects.length === 0 ? ( {projects.length === 0 ? (
<AppCard layer="single" className="xl:col-span-2"> <AppCard className="xl:col-span-2">
<CardContent className="p-6 text-sm text-muted-foreground"> <CardContent className="p-6 text-sm text-muted-foreground">
{copy.empty} {copy.empty}
</CardContent> </CardContent>
+9 -9
View File
@@ -270,7 +270,7 @@ function SiteSettingsMediaRow({
return ( return (
<> <>
<AppCard level={2} layer="single" padding="sm"> <AppCard level={2} padding="sm">
<div className="grid gap-4 lg:grid-cols-[180px_minmax(0,1fr)]"> <div className="grid gap-4 lg:grid-cols-[180px_minmax(0,1fr)]">
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm font-semibold text-foreground">{title}</p> <p className="text-sm font-semibold text-foreground">{title}</p>
@@ -393,7 +393,7 @@ function LocalizedFieldsSection({
<div className="grid gap-5 lg:grid-cols-3"> <div className="grid gap-5 lg:grid-cols-3">
{localeFields.map((locale) => ( {localeFields.map((locale) => (
<AppCard key={locale.key} level={2} layer="single" padding="sm" className="space-y-4"> <AppCard key={locale.key} level={2} padding="sm" contentClassName="space-y-4">
<p className="text-sm font-semibold text-foreground">{locale.label}</p> <p className="text-sm font-semibold text-foreground">{locale.label}</p>
<div className="space-y-2"> <div className="space-y-2">
@@ -558,7 +558,7 @@ export function SiteSettingsForm({
</p> </p>
</div> </div>
<AppCard level={2} layer="single" padding="sm" className="space-y-4"> <AppCard level={2} padding="sm" contentClassName="space-y-4">
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]"> <div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="primaryColor" className="sr-only">Primary Color</Label> <Label htmlFor="primaryColor" className="sr-only">Primary Color</Label>
@@ -679,7 +679,7 @@ export function SiteSettingsForm({
</p> </p>
</div> </div>
<AppCard level={2} layer="single" padding="sm" className="space-y-4"> <AppCard level={2} padding="sm" contentClassName="space-y-4">
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]"> <div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="defaultLocaleTrigger" className="sr-only">Default Locale</Label> <Label htmlFor="defaultLocaleTrigger" className="sr-only">Default Locale</Label>
@@ -729,7 +729,7 @@ export function SiteSettingsForm({
<section className="space-y-4"> <section className="space-y-4">
{mode === "brand" ? ( {mode === "brand" ? (
<> <>
<AppCard level={2} layer="single" padding="sm" className="space-y-2"> <AppCard level={2} padding="sm" contentClassName="space-y-2">
<p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">Search Result</p> <p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">Search Result</p>
<div className="rounded-nested border border-border/70 bg-background p-3"> <div className="rounded-nested border border-border/70 bg-background p-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -750,7 +750,7 @@ export function SiteSettingsForm({
</div> </div>
</AppCard> </AppCard>
<AppCard level={2} layer="single" padding="sm" className="space-y-2"> <AppCard level={2} padding="sm" contentClassName="space-y-2">
<p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">Brand Assets</p> <p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">Brand Assets</p>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<AppCard level={1} layer="single" padding="sm"> <AppCard level={1} layer="single" padding="sm">
@@ -773,7 +773,7 @@ export function SiteSettingsForm({
</div> </div>
</AppCard> </AppCard>
<AppCard level={2} layer="single" padding="sm" className="space-y-2"> <AppCard level={2} padding="sm" contentClassName="space-y-2">
<p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">Social Preview</p> <p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">Social Preview</p>
<div className="overflow-hidden rounded-nested border border-border/70 bg-background"> <div className="overflow-hidden rounded-nested border border-border/70 bg-background">
<div className="h-1.5" style={{ backgroundColor: settings.brand.primaryColor }} /> <div className="h-1.5" style={{ backgroundColor: settings.brand.primaryColor }} />
@@ -797,7 +797,7 @@ export function SiteSettingsForm({
</> </>
) : ( ) : (
<> <>
<AppCard level={2} layer="single" padding="sm" className="space-y-2"> <AppCard level={2} padding="sm" contentClassName="space-y-2">
<p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">Visitor Routing</p> <p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">Visitor Routing</p>
<div className="rounded-nested border border-border/70 bg-background p-3"> <div className="rounded-nested border border-border/70 bg-background p-3">
<div className="flex items-center gap-2 text-sm font-medium text-foreground"> <div className="flex items-center gap-2 text-sm font-medium text-foreground">
@@ -810,7 +810,7 @@ export function SiteSettingsForm({
</div> </div>
</AppCard> </AppCard>
<AppCard level={2} layer="single" padding="sm" className="space-y-2"> <AppCard level={2} padding="sm" contentClassName="space-y-2">
<p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">Locale Summary</p> <p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">Locale Summary</p>
<div className="rounded-nested border border-border/70 bg-background"> <div className="rounded-nested border border-border/70 bg-background">
<Table> <Table>
+2 -2
View File
@@ -25,7 +25,7 @@ export function SMTPSettingsForm({
return ( return (
<form id="smtp-settings-form" action={action} className="space-y-6"> <form id="smtp-settings-form" action={action} className="space-y-6">
<div className="grid gap-6 xl:grid-cols-2"> <div className="grid gap-6 xl:grid-cols-2">
<AppCard layer="single"> <AppCard>
<CardHeader> <CardHeader>
<CardTitle>SMTP Connection</CardTitle> <CardTitle>SMTP Connection</CardTitle>
<CardDescription>Server und Login.</CardDescription> <CardDescription>Server und Login.</CardDescription>
@@ -109,7 +109,7 @@ export function SMTPSettingsForm({
</CardContent> </CardContent>
</AppCard> </AppCard>
<AppCard layer="single"> <AppCard>
<CardHeader> <CardHeader>
<CardTitle>Sender And Recipients</CardTitle> <CardTitle>Sender And Recipients</CardTitle>
<CardDescription>Absender und Ziele.</CardDescription> <CardDescription>Absender und Ziele.</CardDescription>
+3 -3
View File
@@ -17,7 +17,7 @@ export function WorkspaceHero({
aside?: ReactNode; aside?: ReactNode;
}) { }) {
return ( return (
<AppCard level={3} layer="single"> <AppCard level={3}>
<CardContent className="flex flex-col gap-4 p-6 lg:flex-row lg:items-end lg:justify-between lg:p-8"> <CardContent className="flex flex-col gap-4 p-6 lg:flex-row lg:items-end lg:justify-between lg:p-8">
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground"> <p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
@@ -40,7 +40,7 @@ export function WorkspaceSidebarPanel({
children: ReactNode; children: ReactNode;
}) { }) {
return ( return (
<AppCard level={2} layer="single"> <AppCard level={2}>
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<CardTitle className="text-base">{title}</CardTitle> <CardTitle className="text-base">{title}</CardTitle>
</CardHeader> </CardHeader>
@@ -100,7 +100,7 @@ export function WorkspaceLocaleCard({
children: ReactNode; children: ReactNode;
}) { }) {
return ( return (
<AppCard level={2} layer="single"> <AppCard level={2}>
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<CardTitle className="text-lg">{title}</CardTitle> <CardTitle className="text-lg">{title}</CardTitle>
<CardDescription>{hint}</CardDescription> <CardDescription>{hint}</CardDescription>
+14 -1
View File
@@ -87,10 +87,21 @@ export interface AppCardProps
VariantProps<typeof appCardVariants>, VariantProps<typeof appCardVariants>,
VariantProps<typeof appCardInnerVariants> { VariantProps<typeof appCardInnerVariants> {
layer?: "double" | "single"; layer?: "double" | "single";
/**
* Classes applied to the element that directly wraps `children` (the inner
* shell in `double` layer, the single shell in `single` layer). Use it for
* content utilities such as `space-y-*` / `grid` / `flex` so they keep
* working after switching a card to the layered (double) look, where
* `className` lands on the outer shell instead.
*/
contentClassName?: string;
} }
const AppCard = React.forwardRef<HTMLDivElement, AppCardProps>( const AppCard = React.forwardRef<HTMLDivElement, AppCardProps>(
({ className, level, padding, interactive, layer = "double", children, ...props }, ref) => { (
{ className, contentClassName, level, padding, interactive, layer = "double", children, ...props },
ref,
) => {
if (layer === "single") { if (layer === "single") {
return ( return (
<Card <Card
@@ -102,6 +113,7 @@ const AppCard = React.forwardRef<HTMLDivElement, AppCardProps>(
interactive, interactive,
}), }),
className, className,
contentClassName,
)} )}
{...props} {...props}
> >
@@ -129,6 +141,7 @@ const AppCard = React.forwardRef<HTMLDivElement, AppCardProps>(
padding, padding,
interactive, interactive,
}), }),
contentClassName,
)} )}
> >
{children} {children}