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}
>
<MotionFade delay={0.1}>
<AppCard layer="single">
<AppCard>
<CardContent className="space-y-4 p-6">
<p className="text-sm text-muted-foreground">{copy.maintenanceText}</p>
<div className="flex flex-wrap items-center gap-3">
+1 -1
View File
@@ -104,7 +104,7 @@ export default async function AdminPortfolioProjectPage({
</MotionFade>
<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">
<div>
<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">
{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>
<Textarea
id={`${row.key}-de`}
+1 -1
View File
@@ -89,7 +89,7 @@ export function MediaFieldPicker({
}, [serializedValue]);
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} />
<div className="flex items-start justify-between gap-4">
+1 -1
View File
@@ -486,7 +486,7 @@ export function MediaLibraryManager({
)
) : (
<MotionFade delay={0.18}>
<AppCard layer="single">
<AppCard>
<CardContent className="p-6 text-sm text-muted-foreground">
{copy.empty}
</CardContent>
@@ -110,7 +110,7 @@ function CategoryLocaleFields({
const descriptionKey = `description${locale.key}` as const;
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>
<div className="space-y-2">
@@ -168,7 +168,7 @@ function CategoryStatusFields({
</div>
<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">
{isActive ? (
<ShieldCheck className="mt-0.5 h-4 w-4 text-status-success" />
@@ -395,7 +395,7 @@ export function PortfolioCategoriesManager({
<DialogDescription>{copy.modalDescription}</DialogDescription>
</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="space-y-1">
<p className="text-sm font-medium text-foreground">1. Basics</p>
@@ -426,7 +426,7 @@ export function PortfolioCategoriesManager({
</Dialog>
</div>
<AppCard level={3} layer="single">
<AppCard level={3}>
<CardContent className="space-y-4 p-6">
<div className="flex items-center gap-3">
<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) {
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-2">
<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={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-4 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-3">
@@ -625,7 +625,7 @@ export function PortfolioProjectForm({
<div className="space-y-6">
<div>
<AppCard level={3} layer="single" padding="lg">
<AppCard level={3} padding="lg">
<section className="space-y-6">
<SectionHeader
title="Basic Information"
@@ -787,7 +787,7 @@ export function PortfolioProjectForm({
</div>
<div>
<AppCard level={3} layer="single" padding="lg">
<AppCard level={3} padding="lg">
<section className="space-y-6">
<SectionHeader
title="Localized Content"
@@ -838,7 +838,7 @@ export function PortfolioProjectForm({
</div>
<div>
<AppCard level={3} layer="single" padding="lg">
<AppCard level={3} padding="lg">
<section className="space-y-6">
<SectionHeader
title="Sections"
@@ -1028,7 +1028,7 @@ export function PortfolioProjectForm({
</div>
<div>
<AppCard level={3} layer="single" padding="lg">
<AppCard level={3} padding="lg">
<div className="space-y-8">
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_280px]">
<div className="space-y-6">
@@ -1203,7 +1203,7 @@ export function PortfolioProjectForm({
</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-wrap items-center gap-3">
{firstIncompleteStep ? (
@@ -85,7 +85,7 @@ export async function PortfolioProjectsOverview({
<div className="grid gap-4 xl:grid-cols-2">
{projects.map((project, index) => (
<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">
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
@@ -122,7 +122,7 @@ export async function PortfolioProjectsOverview({
))}
{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">
{copy.empty}
</CardContent>
+9 -9
View File
@@ -270,7 +270,7 @@ function SiteSettingsMediaRow({
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="space-y-2">
<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">
{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>
<div className="space-y-2">
@@ -558,7 +558,7 @@ export function SiteSettingsForm({
</p>
</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="space-y-2">
<Label htmlFor="primaryColor" className="sr-only">Primary Color</Label>
@@ -679,7 +679,7 @@ export function SiteSettingsForm({
</p>
</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="space-y-2">
<Label htmlFor="defaultLocaleTrigger" className="sr-only">Default Locale</Label>
@@ -729,7 +729,7 @@ export function SiteSettingsForm({
<section className="space-y-4">
{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>
<div className="rounded-nested border border-border/70 bg-background p-3">
<div className="flex items-center gap-3">
@@ -750,7 +750,7 @@ export function SiteSettingsForm({
</div>
</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>
<div className="grid gap-3 sm:grid-cols-2">
<AppCard level={1} layer="single" padding="sm">
@@ -773,7 +773,7 @@ export function SiteSettingsForm({
</div>
</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>
<div className="overflow-hidden rounded-nested border border-border/70 bg-background">
<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>
<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">
@@ -810,7 +810,7 @@ export function SiteSettingsForm({
</div>
</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>
<div className="rounded-nested border border-border/70 bg-background">
<Table>
+2 -2
View File
@@ -25,7 +25,7 @@ export function SMTPSettingsForm({
return (
<form id="smtp-settings-form" action={action} className="space-y-6">
<div className="grid gap-6 xl:grid-cols-2">
<AppCard layer="single">
<AppCard>
<CardHeader>
<CardTitle>SMTP Connection</CardTitle>
<CardDescription>Server und Login.</CardDescription>
@@ -109,7 +109,7 @@ export function SMTPSettingsForm({
</CardContent>
</AppCard>
<AppCard layer="single">
<AppCard>
<CardHeader>
<CardTitle>Sender And Recipients</CardTitle>
<CardDescription>Absender und Ziele.</CardDescription>
+3 -3
View File
@@ -17,7 +17,7 @@ export function WorkspaceHero({
aside?: ReactNode;
}) {
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">
<div className="space-y-2">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
@@ -40,7 +40,7 @@ export function WorkspaceSidebarPanel({
children: ReactNode;
}) {
return (
<AppCard level={2} layer="single">
<AppCard level={2}>
<CardHeader className="pb-4">
<CardTitle className="text-base">{title}</CardTitle>
</CardHeader>
@@ -100,7 +100,7 @@ export function WorkspaceLocaleCard({
children: ReactNode;
}) {
return (
<AppCard level={2} layer="single">
<AppCard level={2}>
<CardHeader className="pb-4">
<CardTitle className="text-lg">{title}</CardTitle>
<CardDescription>{hint}</CardDescription>
+14 -1
View File
@@ -87,10 +87,21 @@ export interface AppCardProps
VariantProps<typeof appCardVariants>,
VariantProps<typeof appCardInnerVariants> {
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>(
({ className, level, padding, interactive, layer = "double", children, ...props }, ref) => {
(
{ className, contentClassName, level, padding, interactive, layer = "double", children, ...props },
ref,
) => {
if (layer === "single") {
return (
<Card
@@ -102,6 +113,7 @@ const AppCard = React.forwardRef<HTMLDivElement, AppCardProps>(
interactive,
}),
className,
contentClassName,
)}
{...props}
>
@@ -129,6 +141,7 @@ const AppCard = React.forwardRef<HTMLDivElement, AppCardProps>(
padding,
interactive,
}),
contentClassName,
)}
>
{children}