Refactor frontend system primitives and surfaces
This commit is contained in:
@@ -94,18 +94,18 @@ export default async function HomePage({ params: { locale } }: HomePageProps) {
|
|||||||
<Container id="home-content" className="relative z-10 -mt-6 flex scroll-mt-28 flex-col gap-section pb-12 lg:-mt-8 lg:pb-16">
|
<Container id="home-content" className="relative z-10 -mt-6 flex scroll-mt-28 flex-col gap-section pb-12 lg:-mt-8 lg:pb-16">
|
||||||
<MotionFade delay={0.05}>
|
<MotionFade delay={0.05}>
|
||||||
<AppCard
|
<AppCard
|
||||||
className="overflow-hidden border border-border/70 bg-[linear-gradient(145deg,rgba(255,255,255,0.82),rgba(236,240,251,0.72))] text-foreground shadow-[0_24px_64px_-40px_rgba(15,23,42,0.28)] backdrop-blur-xl dark:border-white/10 dark:bg-[linear-gradient(145deg,rgba(17,24,39,0.78),rgba(10,16,30,0.7))] dark:text-primary-foreground"
|
className="overflow-hidden border border-border/70 bg-card/90 text-card-foreground shadow-panel backdrop-blur-xl"
|
||||||
>
|
>
|
||||||
<CardContent className="p-6 lg:p-10">
|
<CardContent className="p-6 lg:p-10">
|
||||||
<div className="grid gap-8 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,1fr)] lg:items-start">
|
<div className="grid gap-8 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,1fr)] lg:items-start">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium tracking-[0.08em] text-foreground/56 dark:text-primary-foreground/62">
|
<p className="text-sm font-medium tracking-[0.08em] text-foreground/56">
|
||||||
{t("servicesEyebrow")}
|
{t("servicesEyebrow")}
|
||||||
</p>
|
</p>
|
||||||
<h2 className="mt-4 max-w-[12ch] text-balance text-4xl font-semibold leading-[0.92] tracking-[-0.06em] text-foreground sm:text-5xl lg:text-6xl dark:text-primary-foreground">
|
<h2 className="mt-4 max-w-[12ch] text-balance text-4xl font-semibold leading-[0.92] tracking-[-0.06em] text-foreground sm:text-5xl lg:text-6xl">
|
||||||
{t("servicesTitle")}
|
{t("servicesTitle")}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-5 max-w-[34rem] text-sm leading-7 text-muted-foreground sm:text-base dark:text-primary-foreground/76">
|
<p className="mt-5 max-w-[34rem] text-sm leading-7 text-muted-foreground sm:text-base">
|
||||||
{t("servicesText")}
|
{t("servicesText")}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8 flex flex-wrap gap-3">
|
<div className="mt-8 flex flex-wrap gap-3">
|
||||||
@@ -119,7 +119,7 @@ export default async function HomePage({ params: { locale } }: HomePageProps) {
|
|||||||
asChild
|
asChild
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="min-w-[11rem] border-border/80 bg-background/70 hover:bg-background dark:border-white/16 dark:bg-white/6 dark:text-primary-foreground dark:hover:bg-white/10 dark:hover:text-primary-foreground"
|
className="min-w-[11rem] border-border/80 bg-background/70 hover:bg-background"
|
||||||
>
|
>
|
||||||
<Link href={getLocalizedPath(localeKey, "/portfolio")}>
|
<Link href={getLocalizedPath(localeKey, "/portfolio")}>
|
||||||
{t("servicesSecondaryCta")}
|
{t("servicesSecondaryCta")}
|
||||||
@@ -135,12 +135,12 @@ export default async function HomePage({ params: { locale } }: HomePageProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item}
|
key={item}
|
||||||
className="rounded-nested border border-border/70 bg-white/54 p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.5)] transition-colors hover:bg-white/78 dark:border-white/10 dark:bg-white/6 dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] dark:hover:bg-white/8"
|
className="rounded-nested border border-border/70 bg-background/80 p-4 shadow-xs transition-colors hover:bg-accent/40"
|
||||||
>
|
>
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-[linear-gradient(135deg,hsl(var(--hero-left)/0.42),hsl(var(--hero-right)/0.26))] dark:bg-[linear-gradient(135deg,rgba(99,125,214,0.18),rgba(192,137,170,0.16))]">
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-[linear-gradient(135deg,hsl(var(--hero-left)/0.42),hsl(var(--hero-right)/0.26))]">
|
||||||
<Icon className="h-5 w-5 text-foreground/78 dark:text-primary-foreground/78" />
|
<Icon className="h-5 w-5 text-foreground/78" />
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-4 text-sm font-medium leading-6 text-foreground/88 sm:text-base dark:text-primary-foreground/88">
|
<p className="mt-4 text-sm font-medium leading-6 text-foreground/88 sm:text-base">
|
||||||
{item}
|
{item}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -173,11 +173,11 @@ export default async function RootMediaPage({ searchParams }: RootMediaPageProps
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{asset.kind === "IMAGE" ? (
|
{asset.kind === "IMAGE" ? (
|
||||||
<div className="overflow-hidden rounded-lg border bg-card">
|
<div className="overflow-hidden rounded-nested border bg-card">
|
||||||
<img src={asset.url} alt={asset.label} className="h-48 w-full object-cover" />
|
<img src={asset.url} alt={asset.label} className="h-48 w-full object-cover" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg border bg-card px-4 py-6 text-sm text-muted-foreground">
|
<div className="rounded-nested border bg-card px-4 py-6 text-sm text-muted-foreground">
|
||||||
{asset.fileName}
|
{asset.fileName}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -193,7 +193,7 @@ export default async function RootMediaPage({ searchParams }: RootMediaPageProps
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{asset.usages.length > 0 ? (
|
{asset.usages.length > 0 ? (
|
||||||
<div className="space-y-2 rounded-md border bg-muted/40 px-4 py-3 text-xs text-muted-foreground">
|
<div className="space-y-2 rounded-nested border bg-muted/40 px-4 py-3 text-xs text-muted-foreground">
|
||||||
{asset.usages.map((usage) => (
|
{asset.usages.map((usage) => (
|
||||||
<p key={usage.id}>
|
<p key={usage.id}>
|
||||||
{usage.usageType} / {usage.entityType} / {usage.fieldKey}
|
{usage.usageType} / {usage.entityType} / {usage.fieldKey}
|
||||||
|
|||||||
+5
-5
@@ -171,25 +171,25 @@ export default async function RootPage({ searchParams }: RootPageProps) {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{!authConfigured ? (
|
{!authConfigured ? (
|
||||||
<p className="mb-4 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
<p className="mb-4 rounded-nested border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||||
{copy.configMissing}
|
{copy.configMissing}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!basicConfigured ? (
|
{!basicConfigured ? (
|
||||||
<p className="mb-4 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
<p className="mb-4 rounded-nested border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||||
{copy.basicAuthMissing}
|
{copy.basicAuthMissing}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{searchParams?.error === "invalid" ? (
|
{searchParams?.error === "invalid" ? (
|
||||||
<p className="mb-4 rounded-md border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning">
|
<p className="mb-4 rounded-nested border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning">
|
||||||
{copy.invalidLogin}
|
{copy.invalidLogin}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{searchParams?.error === "locked" || lockState.locked ? (
|
{searchParams?.error === "locked" || lockState.locked ? (
|
||||||
<p className="mb-4 rounded-md border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning">
|
<p className="mb-4 rounded-nested border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning">
|
||||||
{copy.lockedLogin}
|
{copy.lockedLogin}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -385,7 +385,7 @@ export default async function RootPage({ searchParams }: RootPageProps) {
|
|||||||
<CardDescription>{copy.systemStatusDescription}</CardDescription>
|
<CardDescription>{copy.systemStatusDescription}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="rounded-lg border border-border bg-muted/40 p-4">
|
<div className="rounded-nested border border-border bg-muted/40 p-4">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-foreground">{copy.authCard}</p>
|
<p className="text-sm font-medium text-foreground">{copy.authCard}</p>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function DashboardCard({
|
|||||||
<CardDescription>{title}</CardDescription>
|
<CardDescription>{title}</CardDescription>
|
||||||
<CardTitle className="text-3xl font-semibold tracking-tight">{value}</CardTitle>
|
<CardTitle className="text-3xl font-semibold tracking-tight">{value}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border border-border bg-muted/60 p-2 text-muted-foreground">
|
<div className="rounded-nested border border-border bg-muted/60 p-2 text-muted-foreground">
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function DashboardLayout({
|
|||||||
|
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||||
{Icon ? (
|
{Icon ? (
|
||||||
<div className="hidden rounded-lg border border-border/70 bg-muted/50 p-2 text-muted-foreground sm:flex">
|
<div className="hidden rounded-nested border border-border/70 bg-muted/50 p-2 text-muted-foreground sm:flex">
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ export function FloatingPreferences({
|
|||||||
return (
|
return (
|
||||||
<div className={cn("fixed inset-x-0 top-0 z-50 px-4 pt-4 sm:px-6", className)}>
|
<div className={cn("fixed inset-x-0 top-0 z-50 px-4 pt-4 sm:px-6", className)}>
|
||||||
<div className="mx-auto flex w-full justify-center">
|
<div className="mx-auto flex w-full justify-center">
|
||||||
<div className="inline-flex items-center justify-center gap-1 rounded-[var(--radius-pill)] border border-black/6 bg-white/24 p-1 shadow-[0_14px_34px_-24px_rgba(15,23,42,0.18)] backdrop-blur-[18px] dark:border-white/10 dark:bg-white/4 dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)]">
|
<div className="inline-flex items-center justify-center gap-1 rounded-pill border border-border/70 bg-background/80 p-1 shadow-panel backdrop-blur-chrome">
|
||||||
<ThemeToggle
|
<ThemeToggle
|
||||||
ariaLabel={t("themeToggle")}
|
ariaLabel={t("themeToggle")}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-9 w-9 rounded-[var(--radius-pill)] border border-transparent bg-transparent text-[hsl(var(--hero-ink)/0.76)] hover:border-black/5 hover:bg-white/36 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:border-white/10 dark:hover:bg-white/10"
|
className="h-9 w-9 rounded-pill border border-transparent bg-transparent text-foreground/80 hover:bg-accent hover:text-foreground"
|
||||||
/>
|
/>
|
||||||
<LocaleToggle
|
<LocaleToggle
|
||||||
locale={locale}
|
locale={locale}
|
||||||
className="h-9 w-9 rounded-[var(--radius-pill)] border border-transparent bg-transparent p-0 text-[hsl(var(--hero-ink)/0.76)] hover:bg-white/36 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:bg-white/10"
|
className="h-9 w-9 rounded-pill border border-transparent bg-transparent p-0 text-foreground/80 hover:bg-accent hover:text-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,18 +61,18 @@ function NavLinks({
|
|||||||
key={item.key}
|
key={item.key}
|
||||||
href={getLocalizedPath(locale, itemPath)}
|
href={getLocalizedPath(locale, itemPath)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-10 rounded-[var(--radius-pill)] transition-colors",
|
"relative z-10 rounded-pill transition-colors",
|
||||||
mobile ? "px-4 py-3 text-sm font-medium" : "px-3.5 py-2 text-sm font-medium xl:px-4",
|
mobile ? "px-4 py-3 text-sm font-medium" : "px-3.5 py-2 text-sm font-medium xl:px-4",
|
||||||
isActive
|
isActive
|
||||||
? "text-primary-foreground"
|
? "text-primary-foreground"
|
||||||
: "text-[hsl(var(--hero-ink)/0.62)] hover:text-[hsl(var(--hero-ink))] dark:text-muted-foreground dark:hover:text-foreground",
|
: "text-foreground/70 hover:text-foreground",
|
||||||
)}
|
)}
|
||||||
onClick={onNavigate}
|
onClick={onNavigate}
|
||||||
>
|
>
|
||||||
{isActive ? (
|
{isActive ? (
|
||||||
<motion.span
|
<motion.span
|
||||||
layoutId="site-header-active-tab"
|
layoutId="site-header-active-tab"
|
||||||
className="absolute inset-0 -z-10 rounded-[var(--radius-pill)] bg-primary shadow-[0_14px_34px_-22px_hsl(var(--brand-primary)/0.65)]"
|
className="absolute inset-0 -z-10 rounded-pill bg-primary shadow-xs"
|
||||||
transition={{ type: "spring", stiffness: 380, damping: 30 }}
|
transition={{ type: "spring", stiffness: 380, damping: 30 }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -96,11 +96,11 @@ export function SiteHeader({
|
|||||||
const t = useTranslations("navigation");
|
const t = useTranslations("navigation");
|
||||||
const isArabic = locale === "ar";
|
const isArabic = locale === "ar";
|
||||||
const desktopControlRailClassName =
|
const desktopControlRailClassName =
|
||||||
"inline-flex items-center gap-1 rounded-[var(--radius-pill)] border border-black/6 bg-white/24 p-1 shadow-[0_14px_34px_-24px_rgba(15,23,42,0.18)] backdrop-blur-[18px] dark:border-white/10 dark:bg-white/4 dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)]";
|
"inline-flex items-center gap-1 rounded-pill border border-border/70 bg-background/80 p-1 shadow-panel backdrop-blur-chrome";
|
||||||
const desktopControlButtonClassName =
|
const desktopControlButtonClassName =
|
||||||
"h-9 w-9 rounded-full border border-transparent p-0 text-[hsl(var(--hero-ink)/0.76)] hover:border-black/5 hover:bg-white/36 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:border-white/10 dark:hover:bg-white/10";
|
"h-9 w-9 rounded-pill border border-transparent p-0 text-foreground/80 hover:bg-accent hover:text-foreground";
|
||||||
const mobileControlButtonClassName =
|
const mobileControlButtonClassName =
|
||||||
"h-10 w-10 rounded-full border border-black/6 bg-white/32 p-0 text-[hsl(var(--hero-ink)/0.8)] hover:bg-white/42 hover:text-[hsl(var(--hero-ink))] dark:border-white/10 dark:bg-white/6 dark:text-foreground dark:hover:bg-white/10";
|
"h-10 w-10 rounded-pill border border-border/70 bg-background text-foreground/80 shadow-xs hover:bg-accent hover:text-foreground";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="fixed inset-x-0 top-3 z-40 bg-transparent">
|
<header className="fixed inset-x-0 top-3 z-40 bg-transparent">
|
||||||
@@ -109,7 +109,7 @@ export function SiteHeader({
|
|||||||
<div className="col-start-1 flex items-center justify-self-start">
|
<div className="col-start-1 flex items-center justify-self-start">
|
||||||
<Link
|
<Link
|
||||||
href={getLocalizedPath(locale)}
|
href={getLocalizedPath(locale)}
|
||||||
className="inline-flex items-center rounded-[var(--radius-pill)] px-1 py-1 sm:py-1.5 lg:py-2"
|
className="inline-flex items-center rounded-pill px-1 py-1 sm:py-1.5 lg:py-2"
|
||||||
>
|
>
|
||||||
<SiteLogo
|
<SiteLogo
|
||||||
lightLogoUrl={lightLogoUrl}
|
lightLogoUrl={lightLogoUrl}
|
||||||
@@ -127,7 +127,7 @@ export function SiteHeader({
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.28, ease: "easeOut" }}
|
transition={{ duration: 0.28, ease: "easeOut" }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-auto inline-flex items-center gap-1 rounded-[var(--radius-pill)] border border-black/6 bg-white/24 p-1.5 shadow-[0_14px_34px_-24px_rgba(15,23,42,0.18)] backdrop-blur-[18px] dark:border-white/10 dark:bg-white/4 dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)]",
|
"pointer-events-auto inline-flex items-center gap-1 rounded-pill border border-border/70 bg-background/80 p-1.5 shadow-panel backdrop-blur-chrome",
|
||||||
isArabic ? "w-auto max-w-none" : "min-w-[27rem] max-w-[42rem]",
|
isArabic ? "w-auto max-w-none" : "min-w-[27rem] max-w-[42rem]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -166,7 +166,7 @@ export function SiteHeader({
|
|||||||
onClick={() => setIsOpen((open) => !open)}
|
onClick={() => setIsOpen((open) => !open)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="col-start-3 ml-auto h-11 w-11 justify-self-end rounded-[var(--radius-pill)] border border-black/6 bg-white/24 text-[hsl(var(--hero-ink))] shadow-[0_14px_34px_-24px_rgba(15,23,42,0.18)] backdrop-blur-[18px] hover:bg-white/34 dark:border-white/10 dark:bg-white/4 dark:text-foreground dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)] dark:hover:bg-white/10 lg:hidden"
|
className="col-start-3 ml-auto h-11 w-11 justify-self-end rounded-pill border border-border/70 bg-background/80 text-foreground shadow-panel backdrop-blur-chrome hover:bg-accent lg:hidden"
|
||||||
aria-label={isOpen ? t("closeMenu") : t("openMenu")}
|
aria-label={isOpen ? t("closeMenu") : t("openMenu")}
|
||||||
>
|
>
|
||||||
<motion.span
|
<motion.span
|
||||||
@@ -195,7 +195,7 @@ export function SiteHeader({
|
|||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.98 }}
|
exit={{ opacity: 0, scale: 0.98 }}
|
||||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||||
className="overflow-hidden rounded-surface border border-black/6 bg-[linear-gradient(180deg,rgba(255,255,255,0.46),rgba(255,255,255,0.24))] shadow-[inset_0_1px_0_rgba(255,255,255,0.54),0_16px_34px_-24px_rgba(15,23,42,0.24)] backdrop-blur-[20px] dark:border-white/12 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.09),rgba(255,255,255,0.03))] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.12),inset_0_-1px_0_rgba(255,255,255,0.03),0_20px_48px_-28px_rgba(0,0,0,0.72)]"
|
className="overflow-hidden rounded-surface border border-border/70 bg-card/95 shadow-panel backdrop-blur-chrome"
|
||||||
>
|
>
|
||||||
<motion.nav
|
<motion.nav
|
||||||
initial="closed"
|
initial="closed"
|
||||||
@@ -235,8 +235,8 @@ export function SiteHeader({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-between rounded-nested px-4 py-3 text-sm font-medium transition-colors",
|
"flex items-center justify-between rounded-nested px-4 py-3 text-sm font-medium transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? "bg-white/72 text-[hsl(var(--hero-ink))] shadow-[0_10px_20px_-18px_rgba(15,23,42,0.24)] dark:bg-white/10 dark:text-foreground"
|
? "bg-accent text-foreground shadow-xs"
|
||||||
: "text-[hsl(var(--hero-ink)/0.78)] hover:bg-white/40 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:bg-white/6",
|
: "text-foreground/80 hover:bg-accent/70 hover:text-foreground",
|
||||||
)}
|
)}
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
>
|
>
|
||||||
@@ -255,7 +255,7 @@ export function SiteHeader({
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: 8 }}
|
exit={{ opacity: 0, y: 8 }}
|
||||||
transition={{ duration: 0.18, ease: "easeOut", delay: 0.04 }}
|
transition={{ duration: 0.18, ease: "easeOut", delay: 0.04 }}
|
||||||
className="border-t border-black/6 bg-[linear-gradient(180deg,rgba(255,255,255,0.2),rgba(255,255,255,0.08))] p-2 dark:border-white/8 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02))]"
|
className="border-t border-border/70 bg-background/70 p-2"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<LocaleToggle
|
<LocaleToggle
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function FlashMessage({
|
|||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-md border px-4 py-3 text-sm",
|
"rounded-nested border px-4 py-3 text-sm",
|
||||||
type === "success"
|
type === "success"
|
||||||
? "border-status-success/30 bg-status-success/10 text-status-success"
|
? "border-status-success/30 bg-status-success/10 text-status-success"
|
||||||
: "border-destructive/30 bg-destructive/10 text-destructive",
|
: "border-destructive/30 bg-destructive/10 text-destructive",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|||||||
|
|
||||||
import type { MediaOption } from "@/lib/media";
|
import type { MediaOption } from "@/lib/media";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { AppCard } from "@/components/ui/app-card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ export function MediaFieldPicker({
|
|||||||
}, [serializedValue]);
|
}, [serializedValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3 rounded-surface border border-border/80 bg-card p-4 shadow-card">
|
<AppCard padding="sm" className="space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="sr-only">{title}</Label>
|
<Label className="sr-only">{title}</Label>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -238,16 +239,16 @@ export function MediaFieldPicker({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center gap-3 rounded-md border px-3 py-2 text-left transition-colors",
|
"flex w-full items-center gap-3 rounded-nested border px-3 py-2 text-left transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? "border-input bg-accent/40"
|
? "border-input bg-accent/40"
|
||||||
: "border-input bg-background hover:bg-accent/20",
|
: "border-input bg-background hover:bg-accent/20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{option.url ? (
|
{option.url ? (
|
||||||
<img src={option.url} alt={option.label} className="h-12 w-12 rounded-md object-cover" />
|
<img src={option.url} alt={option.label} className="h-12 w-12 rounded-nested object-cover" />
|
||||||
) : (
|
) : (
|
||||||
<div className="h-12 w-12 rounded-md border border-border bg-background" />
|
<div className="h-12 w-12 rounded-nested border border-border bg-background" />
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate text-sm font-medium text-foreground">{option.label}</p>
|
<p className="truncate text-sm font-medium text-foreground">{option.label}</p>
|
||||||
@@ -258,7 +259,7 @@ export function MediaFieldPicker({
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-md border border-dashed border-input px-3 py-4 text-sm text-muted-foreground">
|
<div className="rounded-nested border border-dashed border-input px-3 py-4 text-sm text-muted-foreground">
|
||||||
No media found.
|
No media found.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -269,15 +270,15 @@ export function MediaFieldPicker({
|
|||||||
|
|
||||||
{previewUrl ? (
|
{previewUrl ? (
|
||||||
value.kind === "IMAGE" ? (
|
value.kind === "IMAGE" ? (
|
||||||
<div className="overflow-hidden rounded-md border bg-card">
|
<AppCard className="overflow-hidden rounded-nested">
|
||||||
<img src={previewUrl} alt={value.label || title} className="h-40 w-full object-cover" />
|
<img src={previewUrl} alt={value.label || title} className="h-40 w-full object-cover" />
|
||||||
</div>
|
</AppCard>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-md border bg-card px-4 py-3 text-sm text-muted-foreground">
|
<AppCard className="rounded-nested px-4 py-3 text-sm text-muted-foreground">
|
||||||
{previewUrl}
|
{previewUrl}
|
||||||
</div>
|
</AppCard>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</AppCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ function CategoryLocaleFields({
|
|||||||
const descriptionKey = `description${locale.key}` as const;
|
const descriptionKey = `description${locale.key}` as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`${idPrefix}-${locale.key}`} className="space-y-4 rounded-lg border border-input bg-background p-4">
|
<AppCard key={`${idPrefix}-${locale.key}`} level={2} padding="sm" className="space-y-4 rounded-nested">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-medium text-foreground">{locale.label}</p>
|
<p className="text-sm font-medium text-foreground">{locale.label}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,7 +115,7 @@ function CategoryLocaleFields({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AppCard>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -172,7 +172,7 @@ function EditCategoryDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="flex items-center gap-3 rounded-md border border-input bg-card px-4 py-3 text-sm">
|
<label className="flex items-center gap-3 rounded-nested border border-input bg-card px-4 py-3 text-sm">
|
||||||
<Checkbox name="isActive" defaultChecked={category.isActive} />
|
<Checkbox name="isActive" defaultChecked={category.isActive} />
|
||||||
{copy.active}
|
{copy.active}
|
||||||
</label>
|
</label>
|
||||||
@@ -234,21 +234,21 @@ export function PortfolioCategoriesManager({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<AppCard level={3} className="bg-secondary">
|
<AppCard level={3}>
|
||||||
<CardContent className="flex flex-col gap-6 p-6 lg:flex-row lg:items-end lg:justify-between lg:p-8">
|
<CardContent className="flex flex-col gap-6 p-6 lg:flex-row lg:items-end lg:justify-between lg:p-8">
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
<div className="rounded-lg border border-input bg-background px-4 py-4">
|
<AppCard level={2} padding="sm">
|
||||||
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Total</p>
|
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Total</p>
|
||||||
<p className="mt-2 text-3xl font-semibold text-foreground">{categories.length}</p>
|
<p className="mt-2 text-3xl font-semibold text-foreground">{categories.length}</p>
|
||||||
</div>
|
</AppCard>
|
||||||
<div className="rounded-lg border border-input bg-background px-4 py-4">
|
<AppCard level={2} padding="sm">
|
||||||
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Active</p>
|
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Active</p>
|
||||||
<p className="mt-2 text-3xl font-semibold text-foreground">{activeCount}</p>
|
<p className="mt-2 text-3xl font-semibold text-foreground">{activeCount}</p>
|
||||||
</div>
|
</AppCard>
|
||||||
<div className="rounded-lg border border-input bg-background px-4 py-4">
|
<AppCard level={2} padding="sm">
|
||||||
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Assigned</p>
|
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Assigned</p>
|
||||||
<p className="mt-2 text-3xl font-semibold text-foreground">{assignedProjects}</p>
|
<p className="mt-2 text-3xl font-semibold text-foreground">{assignedProjects}</p>
|
||||||
</div>
|
</AppCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||||
@@ -284,7 +284,7 @@ export function PortfolioCategoriesManager({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="flex items-center gap-3 rounded-md border border-input bg-card px-4 py-3 text-sm">
|
<label className="flex items-center gap-3 rounded-nested border border-input bg-card px-4 py-3 text-sm">
|
||||||
<Checkbox name="isActive" defaultChecked />
|
<Checkbox name="isActive" defaultChecked />
|
||||||
{copy.active}
|
{copy.active}
|
||||||
</label>
|
</label>
|
||||||
@@ -301,7 +301,7 @@ export function PortfolioCategoriesManager({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
|
||||||
<AppCard className="bg-secondary">
|
<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" />
|
||||||
@@ -314,7 +314,7 @@ export function PortfolioCategoriesManager({
|
|||||||
<Accordion type="single" collapsible className="space-y-3">
|
<Accordion type="single" collapsible className="space-y-3">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<AccordionItem key={category.id} value={category.id}>
|
<AccordionItem key={category.id} value={category.id}>
|
||||||
<AccordionTrigger className="bg-secondary hover:no-underline">
|
<AccordionTrigger className="bg-surface-2 hover:no-underline">
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-2 text-left lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex min-w-0 flex-1 flex-col gap-2 text-left lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
@@ -354,11 +354,11 @@ export function PortfolioCategoriesManager({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="grid gap-3 md:grid-cols-3">
|
<div className="grid gap-3 md:grid-cols-3">
|
||||||
{locales.map((locale) => (
|
{locales.map((locale) => (
|
||||||
<div key={`${category.id}-${locale.key}`} className="rounded-lg border border-input bg-card p-4">
|
<AppCard key={`${category.id}-${locale.key}`} level={1} padding="sm" className="rounded-nested">
|
||||||
<p className="text-sm font-medium text-foreground">{locale.label}</p>
|
<p className="text-sm font-medium text-foreground">{locale.label}</p>
|
||||||
<p className="mt-3 text-sm text-foreground">{category.name[locale.lowerKey]}</p>
|
<p className="mt-3 text-sm text-foreground">{category.name[locale.lowerKey]}</p>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">{category.description[locale.lowerKey]}</p>
|
<p className="mt-2 text-sm text-muted-foreground">{category.description[locale.lowerKey]}</p>
|
||||||
</div>
|
</AppCard>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ function PanelButton({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-between rounded-lg border px-4 py-3 text-left transition-colors",
|
"flex w-full items-center justify-between rounded-nested border px-4 py-3 text-left transition-colors",
|
||||||
active
|
active
|
||||||
? "border-input bg-accent/40"
|
? "border-input bg-accent/40"
|
||||||
: "border-input bg-background hover:bg-accent/20",
|
: "border-input bg-background hover:bg-accent/20",
|
||||||
@@ -265,17 +265,17 @@ function LocaleBlock({
|
|||||||
renderField: (locale: (typeof localeFieldConfig)[number]) => React.ReactNode;
|
renderField: (locale: (typeof localeFieldConfig)[number]) => React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3 rounded-lg border border-input bg-background p-4">
|
<AppCard level={2} padding="sm" className="space-y-3">
|
||||||
<p className="text-sm font-medium text-foreground">{title}</p>
|
<p className="text-sm font-medium text-foreground">{title}</p>
|
||||||
<div className="grid gap-4 xl:grid-cols-3">
|
<div className="grid gap-4 xl:grid-cols-3">
|
||||||
{localeFieldConfig.map((locale) => (
|
{localeFieldConfig.map((locale) => (
|
||||||
<div key={`${title}-${locale.key}`} className="space-y-2 rounded-lg border border-input bg-card p-4">
|
<AppCard key={`${title}-${locale.key}`} padding="sm" className="space-y-2 rounded-nested">
|
||||||
<p className="text-sm font-medium text-foreground">{locale.label}</p>
|
<p className="text-sm font-medium text-foreground">{locale.label}</p>
|
||||||
{renderField(locale)}
|
{renderField(locale)}
|
||||||
</div>
|
</AppCard>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AppCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,12 +540,12 @@ export function PortfolioProjectForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<label className="flex items-center gap-3 rounded-md border border-input bg-card px-4 py-3 text-sm">
|
<label className="flex items-center gap-3 rounded-nested border border-input bg-card px-4 py-3 text-sm">
|
||||||
<input type="checkbox" name="isFeatured" checked={projectState.isFeatured} onChange={(event) => setProjectField("isFeatured", event.target.checked)} />
|
<input type="checkbox" name="isFeatured" checked={projectState.isFeatured} onChange={(event) => setProjectField("isFeatured", event.target.checked)} />
|
||||||
Featured
|
Featured
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="flex items-center gap-3 rounded-md border border-input bg-card px-4 py-3 text-sm">
|
<label className="flex items-center gap-3 rounded-nested border border-input bg-card px-4 py-3 text-sm">
|
||||||
<input type="checkbox" name="isPublished" checked={projectState.isPublished} onChange={(event) => setProjectField("isPublished", event.target.checked)} />
|
<input type="checkbox" name="isPublished" checked={projectState.isPublished} onChange={(event) => setProjectField("isPublished", event.target.checked)} />
|
||||||
Published
|
Published
|
||||||
</label>
|
</label>
|
||||||
@@ -624,7 +624,7 @@ export function PortfolioProjectForm({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedSectionIndex(index)}
|
onClick={() => setSelectedSectionIndex(index)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full rounded-lg border p-4 text-left transition-colors",
|
"w-full rounded-nested border p-4 text-left transition-colors",
|
||||||
selectedSectionIndex === index
|
selectedSectionIndex === index
|
||||||
? "border-input bg-accent/40"
|
? "border-input bg-accent/40"
|
||||||
: "border-input bg-background hover:bg-accent/20",
|
: "border-input bg-background hover:bg-accent/20",
|
||||||
@@ -769,7 +769,7 @@ export function PortfolioProjectForm({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedAssetIndex(index)}
|
onClick={() => setSelectedAssetIndex(index)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full rounded-lg border p-4 text-left transition-colors",
|
"w-full rounded-nested border p-4 text-left transition-colors",
|
||||||
selectedAssetIndex === index
|
selectedAssetIndex === index
|
||||||
? "border-input bg-accent/40"
|
? "border-input bg-accent/40"
|
||||||
: "border-input bg-background hover:bg-accent/20",
|
: "border-input bg-background hover:bg-accent/20",
|
||||||
|
|||||||
@@ -51,20 +51,20 @@ export function PortfolioProjectsOverview({
|
|||||||
<AppCard level={3}>
|
<AppCard level={3}>
|
||||||
<CardContent className="flex flex-col gap-6 p-6 lg:flex-row lg:items-end lg:justify-between lg:p-8">
|
<CardContent className="flex flex-col gap-6 p-6 lg:flex-row lg:items-end lg:justify-between lg:p-8">
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
<div className="rounded-lg border border-input bg-background px-4 py-4">
|
<AppCard level={2} padding="sm">
|
||||||
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Projects</p>
|
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Projects</p>
|
||||||
<p className="mt-2 text-3xl font-semibold text-foreground">{projects.length}</p>
|
<p className="mt-2 text-3xl font-semibold text-foreground">{projects.length}</p>
|
||||||
</div>
|
</AppCard>
|
||||||
<div className="rounded-lg border border-input bg-background px-4 py-4">
|
<AppCard level={2} padding="sm">
|
||||||
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Categories</p>
|
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Categories</p>
|
||||||
<p className="mt-2 text-3xl font-semibold text-foreground">{categories.length}</p>
|
<p className="mt-2 text-3xl font-semibold text-foreground">{categories.length}</p>
|
||||||
</div>
|
</AppCard>
|
||||||
<div className="rounded-lg border border-input bg-background px-4 py-4">
|
<AppCard level={2} padding="sm">
|
||||||
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Published</p>
|
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Published</p>
|
||||||
<p className="mt-2 text-3xl font-semibold text-foreground">
|
<p className="mt-2 text-3xl font-semibold text-foreground">
|
||||||
{projects.filter((project) => project.isPublished).length}
|
{projects.filter((project) => project.isPublished).length}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</AppCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function PortfolioSubnav({ active }: PortfolioSubnavProps) {
|
|||||||
key={item.key}
|
key={item.key}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-md border px-4 py-2 text-sm transition-colors",
|
"rounded-nested border px-4 py-2 text-sm transition-colors",
|
||||||
active === item.key
|
active === item.key
|
||||||
? "border-input bg-primary text-primary-foreground"
|
? "border-input bg-primary text-primary-foreground"
|
||||||
: "border-input bg-background text-foreground/75 hover:bg-accent hover:text-accent-foreground",
|
: "border-input bg-background text-foreground/75 hover:bg-accent hover:text-accent-foreground",
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ export function SiteSettingsForm({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-4 lg:grid-cols-3">
|
<CardContent className="grid gap-4 lg:grid-cols-3">
|
||||||
{localeFields.map((locale) => (
|
{localeFields.map((locale) => (
|
||||||
<div key={locale.key} className="space-y-4 rounded-lg border bg-card p-4 shadow-sm">
|
<AppCard key={locale.key} padding="sm" className="space-y-4 rounded-nested">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-foreground">{locale.label}</p>
|
<p className="text-sm font-medium text-foreground">{locale.label}</p>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">Globale Standardwerte fuer diese Sprache.</p>
|
<p className="mt-1 text-xs text-muted-foreground">Globale Standardwerte fuer diese Sprache.</p>
|
||||||
@@ -312,7 +312,7 @@ export function SiteSettingsForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AppCard>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
@@ -429,7 +429,7 @@ export function SiteSettingsForm({
|
|||||||
: `${locale.sampleTitle} | ${localeSettings.siteName}`;
|
: `${locale.sampleTitle} | ${localeSettings.siteName}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={locale.key} className="rounded-md border bg-card p-3">
|
<AppCard key={locale.key} className="rounded-nested p-3">
|
||||||
<p className="font-medium text-foreground">{locale.label}</p>
|
<p className="font-medium text-foreground">{locale.label}</p>
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
<p className="mt-2 text-xs text-muted-foreground">
|
||||||
Example page title: {locale.sampleTitle}
|
Example page title: {locale.sampleTitle}
|
||||||
@@ -441,7 +441,7 @@ export function SiteSettingsForm({
|
|||||||
<p className="mt-2 text-xs text-muted-foreground">
|
<p className="mt-2 text-xs text-muted-foreground">
|
||||||
{localeSettings.subhead || "No subhead"}
|
{localeSettings.subhead || "No subhead"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</AppCard>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -451,7 +451,7 @@ export function SiteSettingsForm({
|
|||||||
Header Logo Preview
|
Header Logo Preview
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
<div className="rounded-md border bg-card p-4">
|
<AppCard padding="sm" className="rounded-nested">
|
||||||
<p className="mb-3 text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground">
|
<p className="mb-3 text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground">
|
||||||
Light
|
Light
|
||||||
</p>
|
</p>
|
||||||
@@ -466,8 +466,8 @@ export function SiteSettingsForm({
|
|||||||
Default light logo will be used
|
Default light logo will be used
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</AppCard>
|
||||||
<div className="rounded-md border border-border/70 bg-slate-950 p-4">
|
<div className="rounded-nested border border-border/70 bg-slate-950 p-4">
|
||||||
<p className="mb-3 text-xs font-medium uppercase tracking-[0.2em] text-white/55">
|
<p className="mb-3 text-xs font-medium uppercase tracking-[0.2em] text-white/55">
|
||||||
Dark
|
Dark
|
||||||
</p>
|
</p>
|
||||||
@@ -489,7 +489,7 @@ export function SiteSettingsForm({
|
|||||||
<Search className="h-4 w-4 text-brand-primary" />
|
<Search className="h-4 w-4 text-brand-primary" />
|
||||||
Search Preview
|
Search Preview
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border bg-card p-4">
|
<AppCard padding="sm" className="rounded-nested">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{faviconPreviewUrl ? (
|
{faviconPreviewUrl ? (
|
||||||
<img src={faviconPreviewUrl} alt="Favicon" className="h-5 w-5 rounded-sm" />
|
<img src={faviconPreviewUrl} alt="Favicon" className="h-5 w-5 rounded-sm" />
|
||||||
@@ -512,13 +512,13 @@ export function SiteSettingsForm({
|
|||||||
<p className="mt-2 text-xs text-muted-foreground/80">
|
<p className="mt-2 text-xs text-muted-foreground/80">
|
||||||
{settings.locales.de.subhead || "No subhead"}
|
{settings.locales.de.subhead || "No subhead"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</AppCard>
|
||||||
|
|
||||||
<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">
|
||||||
<ImageIcon className="h-4 w-4 text-brand-secondary" />
|
<ImageIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
Social Preview
|
Social Preview
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-hidden rounded-md border bg-card">
|
<AppCard className="overflow-hidden rounded-nested">
|
||||||
{defaultOgImagePreviewUrl ? (
|
{defaultOgImagePreviewUrl ? (
|
||||||
<img src={defaultOgImagePreviewUrl} alt="Default OG" className="h-32 w-full object-cover" />
|
<img src={defaultOgImagePreviewUrl} alt="Default OG" className="h-32 w-full object-cover" />
|
||||||
) : (
|
) : (
|
||||||
@@ -538,7 +538,7 @@ export function SiteSettingsForm({
|
|||||||
{settings.locales.en.siteDescription || "No description"}
|
{settings.locales.en.siteDescription || "No description"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AppCard>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ const appCardVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
level: {
|
level: {
|
||||||
1: "border border-border/80 bg-card text-card-foreground shadow-card",
|
1: "border border-border/80 bg-card text-card-foreground shadow-card",
|
||||||
2: "border border-border/80 bg-card text-card-foreground shadow-card",
|
2: "border border-input bg-background text-foreground shadow-xs",
|
||||||
3: "border border-border/80 bg-card text-card-foreground shadow-panel",
|
3: "border border-border/80 bg-surface-2 text-foreground shadow-panel",
|
||||||
inverse: "border-transparent bg-foreground text-background shadow-card",
|
inverse: "border-transparent bg-foreground text-background shadow-card",
|
||||||
},
|
},
|
||||||
padding: {
|
padding: {
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ const DialogContent = React.forwardRef<
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-surface border border-border/80 bg-card p-6 text-card-foreground shadow-panel 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",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{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">
|
<DialogPrimitive.Close className="absolute right-4 top-4 inline-flex h-8 w-8 items-center justify-center rounded-nested text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
@@ -56,7 +56,7 @@ const DialogHeader = ({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
<div className={cn("flex flex-col gap-1.5 text-center sm:text-left", className)} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const DialogFooter = ({
|
const DialogFooter = ({
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const SheetOverlay = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out",
|
"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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -29,13 +29,13 @@ SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
|||||||
|
|
||||||
const sheetVariants = {
|
const sheetVariants = {
|
||||||
right:
|
right:
|
||||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right sm:max-w-sm",
|
"inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm sm:rounded-l-surface data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right",
|
||||||
left:
|
left:
|
||||||
"inset-y-0 left-0 h-full w-3/4 border-r data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left sm:max-w-sm",
|
"inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm sm:rounded-r-surface data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left",
|
||||||
top:
|
top:
|
||||||
"inset-x-0 top-0 border-b data-[state=open]:slide-in-from-top data-[state=closed]:slide-out-to-top",
|
"inset-x-0 top-0 border-b rounded-b-surface data-[state=open]:slide-in-from-top data-[state=closed]:slide-out-to-top",
|
||||||
bottom:
|
bottom:
|
||||||
"inset-x-0 bottom-0 border-t data-[state=open]:slide-in-from-bottom data-[state=closed]:slide-out-to-bottom",
|
"inset-x-0 bottom-0 border-t rounded-t-surface data-[state=open]:slide-in-from-bottom data-[state=closed]:slide-out-to-bottom",
|
||||||
};
|
};
|
||||||
|
|
||||||
type SheetContentProps = React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> & {
|
type SheetContentProps = React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> & {
|
||||||
@@ -51,14 +51,14 @@ const SheetContent = React.forwardRef<
|
|||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out",
|
"fixed z-50 gap-4 border-border/80 bg-card p-6 text-card-foreground shadow-panel transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||||
sheetVariants[side],
|
sheetVariants[side],
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<SheetPrimitive.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">
|
<SheetPrimitive.Close className="absolute right-4 top-4 inline-flex h-8 w-8 items-center justify-center rounded-nested text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
@@ -72,7 +72,7 @@ const SheetHeader = ({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div className={cn("flex flex-col space-y-2 text-left", className)} {...props} />
|
<div className={cn("flex flex-col gap-2 text-left", className)} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const SheetTitle = React.forwardRef<
|
const SheetTitle = React.forwardRef<
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function TabsTrigger({
|
|||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"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",
|
"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",
|
isActive && "border border-border/80 bg-background text-foreground shadow-xs",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
@@ -101,7 +101,7 @@ function TabsContent({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"mt-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -545,10 +545,10 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">{copy.workspaceMainText}</p>
|
<p className="text-sm text-muted-foreground">{copy.workspaceMainText}</p>
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<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">
|
<div className="rounded-nested border border-input bg-background p-4 text-sm text-muted-foreground">
|
||||||
Step 1
|
Step 1
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-input bg-background p-4 text-sm text-muted-foreground">
|
<div className="rounded-nested border border-input bg-background p-4 text-sm text-muted-foreground">
|
||||||
Step 2
|
Step 2
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -562,7 +562,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Badge variant="success">Ready</Badge>
|
<Badge variant="success">Ready</Badge>
|
||||||
<Badge variant="warning">Open</Badge>
|
<Badge variant="warning">Open</Badge>
|
||||||
<div className="rounded-lg border border-input bg-background p-4 text-sm text-muted-foreground">
|
<div className="rounded-nested border border-input bg-background p-4 text-sm text-muted-foreground">
|
||||||
Checklist / status / focus details
|
Checklist / status / focus details
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,581 @@
|
|||||||
|
# Frontend System Audit
|
||||||
|
|
||||||
|
## 1. UI libraries actually used
|
||||||
|
|
||||||
|
- `next` App Router is the app shell and routing layer.
|
||||||
|
- `tailwindcss` is the styling system.
|
||||||
|
- `@radix-ui/react-*` primitives are used through local wrappers in:
|
||||||
|
- `components/ui/accordion.tsx`
|
||||||
|
- `components/ui/checkbox.tsx`
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
- `components/ui/dropdown-menu.tsx`
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
- `components/ui/sheet.tsx`
|
||||||
|
- `class-variance-authority` is used for variants in:
|
||||||
|
- `components/ui/button.tsx`
|
||||||
|
- `components/ui/badge.tsx`
|
||||||
|
- `components/ui/app-card.tsx`
|
||||||
|
- `components/layout/container.tsx`
|
||||||
|
- `clsx` + `tailwind-merge` are used through `cn()` in `lib/utils.ts`.
|
||||||
|
- `next-themes` is used for theme switching in:
|
||||||
|
- `components/theme-provider.tsx`
|
||||||
|
- `components/theme-toggle.tsx`
|
||||||
|
- `framer-motion` is used for custom motion in:
|
||||||
|
- `components/motion-fade.tsx`
|
||||||
|
- `components/layout/site-header.tsx`
|
||||||
|
- `components/layout/site-logo.tsx`
|
||||||
|
- `components/layout/hero-motion-backdrop.tsx`
|
||||||
|
- `components/layout/hero-atmosphere.tsx`
|
||||||
|
- `components/layout/site-ambient-backdrop.tsx`
|
||||||
|
- `components/layout/ascii-water-ripple.tsx`
|
||||||
|
- `components/layout/animated-logo.tsx`
|
||||||
|
- `components/layout/stacked-marquee-section.tsx`
|
||||||
|
- `tailwindcss-animate` is enabled in `tailwind.config.ts` and is used by state classes in `dialog`, `dropdown-menu`, `select`, `sheet`, and `accordion`.
|
||||||
|
|
||||||
|
## 2. Which library is the primary source of UI primitives
|
||||||
|
|
||||||
|
The primary primitive source is the local
|
||||||
|
`components/ui`
|
||||||
|
layer.
|
||||||
|
|
||||||
|
That layer is mostly built on top of
|
||||||
|
`@radix-ui/react-*`
|
||||||
|
plus Tailwind classes.
|
||||||
|
|
||||||
|
The app code imports local primitives like:
|
||||||
|
|
||||||
|
- `@/components/ui/button`
|
||||||
|
- `@/components/ui/card`
|
||||||
|
- `@/components/ui/input`
|
||||||
|
- `@/components/ui/dialog`
|
||||||
|
- `@/components/ui/select`
|
||||||
|
|
||||||
|
It does not import Radix primitives directly from route files and feature components.
|
||||||
|
|
||||||
|
## 3. Whether shadcn/ui is really used or only partially used
|
||||||
|
|
||||||
|
`shadcn/ui`
|
||||||
|
is present, but partially.
|
||||||
|
|
||||||
|
Evidence that it is present:
|
||||||
|
|
||||||
|
- `components.json` exists and points to `app/globals.css`, `tailwind.config.ts`, and `@/components/ui`.
|
||||||
|
- The local primitive structure matches the common shadcn pattern:
|
||||||
|
- `components/ui/button.tsx`
|
||||||
|
- `components/ui/card.tsx`
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
- `components/ui/dropdown-menu.tsx`
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
- `components/ui/sheet.tsx`
|
||||||
|
- `lib/utils.ts` exposes the standard `cn()` helper.
|
||||||
|
|
||||||
|
Evidence that it is partial/customized rather than stock:
|
||||||
|
|
||||||
|
- There is no runtime `shadcn-ui` package in `package.json`.
|
||||||
|
- Several components are customized to project tokens:
|
||||||
|
- `rounded-surface`
|
||||||
|
- `rounded-nested`
|
||||||
|
- `shadow-card`
|
||||||
|
- `shadow-panel`
|
||||||
|
- `components/ui/app-card.tsx` is a project-specific wrapper over `Card`.
|
||||||
|
- `components/ui/tabs.tsx` is a custom React context implementation, not a Radix wrapper.
|
||||||
|
- Usage is selective. Import counts in the current codebase show:
|
||||||
|
- `app-card`: `23`
|
||||||
|
- `card`: `26`
|
||||||
|
- `button`: `19`
|
||||||
|
- `dialog`: `1`
|
||||||
|
- `dropdown-menu`: `1`
|
||||||
|
- `sheet`: `1`
|
||||||
|
- `table`: `1`
|
||||||
|
- `tabs`: `1`
|
||||||
|
|
||||||
|
## 4. Repeated layout/container patterns found in code
|
||||||
|
|
||||||
|
### Global container pattern
|
||||||
|
|
||||||
|
The main reusable container is `components/layout/container.tsx`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
mx-auto w-full px-4 sm:px-6 lg:px-8
|
||||||
|
```
|
||||||
|
|
||||||
|
Container sizes come from Tailwind extensions:
|
||||||
|
|
||||||
|
- `max-w-layout`
|
||||||
|
- `max-w-narrow`
|
||||||
|
- `max-w-wide`
|
||||||
|
- `max-w-admin`
|
||||||
|
|
||||||
|
These map to CSS vars in `app/globals.css`:
|
||||||
|
|
||||||
|
- `--container-narrow: 48rem`
|
||||||
|
- `--container-default: 72rem`
|
||||||
|
- `--container-wide: 80rem`
|
||||||
|
- `--container-admin: 88rem`
|
||||||
|
|
||||||
|
### Public site shell pattern
|
||||||
|
|
||||||
|
Repeated in:
|
||||||
|
|
||||||
|
- `app/[locale]/(site)/layout.tsx`
|
||||||
|
- `components/layout/site-header.tsx`
|
||||||
|
- `components/layout/home-hero.tsx`
|
||||||
|
- `components/layout/page-hero.tsx`
|
||||||
|
- `app/[locale]/(site)/contact/page.tsx`
|
||||||
|
|
||||||
|
Patterns:
|
||||||
|
|
||||||
|
- `relative flex min-h-screen flex-col overflow-hidden`
|
||||||
|
- `max-w-[72rem] px-4 sm:px-6 lg:px-8`
|
||||||
|
- hero sections with:
|
||||||
|
- `pt-24`
|
||||||
|
- `sm:pt-28`
|
||||||
|
- `lg:pt-28`
|
||||||
|
- `pb-10`
|
||||||
|
- `lg:pb-12`
|
||||||
|
- `lg:pb-16`
|
||||||
|
|
||||||
|
### Admin layout pattern
|
||||||
|
|
||||||
|
Repeated in:
|
||||||
|
|
||||||
|
- `components/dashboard/dashboard-layout.tsx`
|
||||||
|
- `components/root/root-dashboard-shell.tsx`
|
||||||
|
- many `app/root/*/page.tsx`
|
||||||
|
|
||||||
|
Patterns:
|
||||||
|
|
||||||
|
- `min-h-screen bg-muted/30`
|
||||||
|
- `grid min-h-screen lg:grid-cols-[280px_minmax(0,1fr)]`
|
||||||
|
- sticky sidebar:
|
||||||
|
- `lg:sticky lg:top-0 lg:h-screen`
|
||||||
|
- sticky header:
|
||||||
|
- `sticky top-0 z-30 ... bg-background/95 ... backdrop-blur`
|
||||||
|
- main content spacing:
|
||||||
|
- `p-4 lg:p-6`
|
||||||
|
- page stack wrapper:
|
||||||
|
- `space-y-6`
|
||||||
|
|
||||||
|
### Editor / split workspace pattern
|
||||||
|
|
||||||
|
Repeated in:
|
||||||
|
|
||||||
|
- `components/root/site-settings-form.tsx`
|
||||||
|
- `components/root/portfolio-project-form.tsx`
|
||||||
|
- `app/root/page.tsx`
|
||||||
|
|
||||||
|
Patterns:
|
||||||
|
|
||||||
|
- `grid gap-6 xl:grid-cols-[minmax(0,3fr)_minmax(320px,1fr)]`
|
||||||
|
- `grid gap-6 xl:grid-cols-[280px_minmax(0,1fr)]`
|
||||||
|
- sticky side panel:
|
||||||
|
- `xl:sticky xl:top-6 xl:self-start`
|
||||||
|
|
||||||
|
## 5. Repeated radius values found in code
|
||||||
|
|
||||||
|
### Tokenized radius values
|
||||||
|
|
||||||
|
Defined in `app/globals.css`:
|
||||||
|
|
||||||
|
- `--radius-surface: 24px`
|
||||||
|
- `--radius-nested: 16px`
|
||||||
|
- `--radius-pill: 9999px`
|
||||||
|
|
||||||
|
Mapped in `tailwind.config.ts`:
|
||||||
|
|
||||||
|
- `rounded-surface`
|
||||||
|
- `rounded-nested`
|
||||||
|
- `rounded-pill`
|
||||||
|
|
||||||
|
### Repeated radius class patterns
|
||||||
|
|
||||||
|
Most repeated token usage:
|
||||||
|
|
||||||
|
- `rounded-surface`
|
||||||
|
- `components/ui/card.tsx`
|
||||||
|
- `components/ui/app-card.tsx`
|
||||||
|
- `components/ui/accordion.tsx`
|
||||||
|
- `components/ui/dropdown-menu.tsx`
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
- `components/ui/tabs.tsx`
|
||||||
|
- `rounded-nested`
|
||||||
|
- `components/ui/button.tsx`
|
||||||
|
- `components/ui/input.tsx`
|
||||||
|
- `components/ui/textarea.tsx`
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
- `components/dashboard/sidebar.tsx`
|
||||||
|
- `components/root/contact-protection-form.tsx`
|
||||||
|
- `rounded-[var(--radius-pill)]`
|
||||||
|
- `components/layout/site-header.tsx`
|
||||||
|
- `components/layout/floating-preferences.tsx`
|
||||||
|
- `rounded-full`
|
||||||
|
- used heavily for decorative circles and icon pills in site chrome:
|
||||||
|
- `components/layout/site-ambient-backdrop.tsx`
|
||||||
|
- `components/layout/hero-atmosphere.tsx`
|
||||||
|
- `components/layout/ascii-water-ripple.tsx`
|
||||||
|
|
||||||
|
### Non-token radius values also present
|
||||||
|
|
||||||
|
- `rounded-md`
|
||||||
|
- several content blocks and previews
|
||||||
|
- examples:
|
||||||
|
- `app/root/page.tsx`
|
||||||
|
- `components/root/site-settings-form.tsx`
|
||||||
|
- `components/root/media-field-picker.tsx`
|
||||||
|
- `rounded-lg`
|
||||||
|
- examples:
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
- `components/dashboard/dashboard-layout.tsx`
|
||||||
|
- `components/root/portfolio-project-form.tsx`
|
||||||
|
|
||||||
|
## 6. Repeated spacing patterns found in code
|
||||||
|
|
||||||
|
### Global repeated spacing
|
||||||
|
|
||||||
|
Most repeated stack and gap patterns:
|
||||||
|
|
||||||
|
- `space-y-6`
|
||||||
|
- `gap-6`
|
||||||
|
- `space-y-2`
|
||||||
|
- `space-y-3`
|
||||||
|
- `p-6`
|
||||||
|
- `p-4`
|
||||||
|
- `px-4 py-3`
|
||||||
|
- `px-4 py-4`
|
||||||
|
- `px-3 py-2`
|
||||||
|
|
||||||
|
### Core primitive spacing
|
||||||
|
|
||||||
|
- `CardHeader`: `flex flex-col gap-1.5 p-6`
|
||||||
|
- `components/ui/card.tsx`
|
||||||
|
- `CardContent`: `p-6 pt-0`
|
||||||
|
- `components/ui/card.tsx`
|
||||||
|
- `Button` sizes:
|
||||||
|
- `default`: `h-10 px-4 py-2`
|
||||||
|
- `sm`: `h-9 px-3`
|
||||||
|
- `lg`: `h-11 px-6`
|
||||||
|
- `icon`: `h-10 w-10`
|
||||||
|
- `components/ui/button.tsx`
|
||||||
|
- `Input` and `Textarea`:
|
||||||
|
- `px-3 py-2`
|
||||||
|
- `components/ui/input.tsx`
|
||||||
|
- `components/ui/textarea.tsx`
|
||||||
|
- `SelectTrigger`:
|
||||||
|
- `px-3 py-2`
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
|
||||||
|
### Layout spacing
|
||||||
|
|
||||||
|
- container padding:
|
||||||
|
- `px-4 sm:px-6 lg:px-8`
|
||||||
|
- admin page wrapper:
|
||||||
|
- `p-4 lg:p-6`
|
||||||
|
- admin section wrapper:
|
||||||
|
- `py-6 lg:py-8`
|
||||||
|
- `components/layout/app-shell.tsx`
|
||||||
|
- hero wrappers:
|
||||||
|
- `pt-24`
|
||||||
|
- `sm:pt-28`
|
||||||
|
- `lg:pt-28`
|
||||||
|
|
||||||
|
## 7. Repeated color/token usage found in code
|
||||||
|
|
||||||
|
### Semantic tokens defined
|
||||||
|
|
||||||
|
Defined in `app/globals.css` and exposed in `tailwind.config.ts`:
|
||||||
|
|
||||||
|
- base semantic:
|
||||||
|
- `background`
|
||||||
|
- `foreground`
|
||||||
|
- `card`
|
||||||
|
- `popover`
|
||||||
|
- `primary`
|
||||||
|
- `secondary`
|
||||||
|
- `muted`
|
||||||
|
- `accent`
|
||||||
|
- `destructive`
|
||||||
|
- `border`
|
||||||
|
- `border-strong`
|
||||||
|
- `input`
|
||||||
|
- `ring`
|
||||||
|
- custom semantic:
|
||||||
|
- `surface-1`
|
||||||
|
- `surface-2`
|
||||||
|
- `surface-3`
|
||||||
|
- `surface-inverse`
|
||||||
|
- `brand-primary`
|
||||||
|
- `brand-secondary`
|
||||||
|
- `status-success`
|
||||||
|
- `status-success-soft`
|
||||||
|
- `status-warning`
|
||||||
|
- `status-warning-soft`
|
||||||
|
- `sidebar-*`
|
||||||
|
- `hero-*`
|
||||||
|
|
||||||
|
### Repeated utility combinations
|
||||||
|
|
||||||
|
Very common combinations:
|
||||||
|
|
||||||
|
- `border border-border/80 bg-card text-card-foreground shadow-card`
|
||||||
|
- `components/ui/card.tsx`
|
||||||
|
- `border border-input bg-background`
|
||||||
|
- `components/ui/input.tsx`
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
- `components/root/portfolio-categories-manager.tsx`
|
||||||
|
- `components/root/portfolio-projects-overview.tsx`
|
||||||
|
- `bg-primary text-primary-foreground`
|
||||||
|
- active nav and badges/buttons:
|
||||||
|
- `components/ui/button.tsx`
|
||||||
|
- `components/ui/badge.tsx`
|
||||||
|
- `components/dashboard/sidebar.tsx`
|
||||||
|
- `components/root/portfolio-subnav.tsx`
|
||||||
|
- `app/[locale]/(site)/portfolio/page.tsx`
|
||||||
|
- `text-muted-foreground`
|
||||||
|
- repeated throughout forms, cards, table headers, descriptions
|
||||||
|
- `hover:bg-accent`
|
||||||
|
- repeated in `button`, `dropdown-menu`, select items, and selection cards
|
||||||
|
- `bg-muted/40` and `bg-muted/50`
|
||||||
|
- repeated in admin surfaces and tables
|
||||||
|
|
||||||
|
### Site-specific color usage outside semantic surface tokens
|
||||||
|
|
||||||
|
Public-facing chrome also uses many arbitrary values:
|
||||||
|
|
||||||
|
- `border-black/6`
|
||||||
|
- `bg-white/24`
|
||||||
|
- `bg-white/32`
|
||||||
|
- `text-[hsl(var(--hero-ink)/0.76)]`
|
||||||
|
- custom shadow values with raw `rgba(...)`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `components/layout/site-header.tsx`
|
||||||
|
- `components/layout/floating-preferences.tsx`
|
||||||
|
- `app/[locale]/(site)/page.tsx`
|
||||||
|
|
||||||
|
## 8. Motion/animation approach actually used
|
||||||
|
|
||||||
|
There are two real motion layers.
|
||||||
|
|
||||||
|
### `framer-motion`
|
||||||
|
|
||||||
|
Used for custom movement and page chrome:
|
||||||
|
|
||||||
|
- reveal-on-scroll wrapper:
|
||||||
|
- `components/motion-fade.tsx`
|
||||||
|
- header nav active pill and mobile menu:
|
||||||
|
- `components/layout/site-header.tsx`
|
||||||
|
- logo animation:
|
||||||
|
- `components/layout/site-logo.tsx`
|
||||||
|
- `components/layout/animated-logo.tsx`
|
||||||
|
- ambient / backdrop motion:
|
||||||
|
- `components/layout/hero-motion-backdrop.tsx`
|
||||||
|
- `components/layout/hero-atmosphere.tsx`
|
||||||
|
- `components/layout/site-ambient-backdrop.tsx`
|
||||||
|
- `components/layout/ascii-water-ripple.tsx`
|
||||||
|
- `components/layout/stacked-marquee-section.tsx`
|
||||||
|
|
||||||
|
### `tailwindcss-animate`
|
||||||
|
|
||||||
|
Used through state classes for Radix-style overlays and disclosures:
|
||||||
|
|
||||||
|
- `data-[state=open]:animate-in`
|
||||||
|
- `data-[state=closed]:animate-out`
|
||||||
|
- `fade-in-0`
|
||||||
|
- `zoom-in-95`
|
||||||
|
- `slide-in-from-top-2`
|
||||||
|
- `animate-accordion-down`
|
||||||
|
- `animate-accordion-up`
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
- `components/ui/dropdown-menu.tsx`
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
- `components/ui/sheet.tsx`
|
||||||
|
- `components/ui/accordion.tsx`
|
||||||
|
|
||||||
|
## 9. Icon system actually used
|
||||||
|
|
||||||
|
The icon system is
|
||||||
|
`lucide-react`
|
||||||
|
.
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
|
||||||
|
- dependency in `package.json`
|
||||||
|
- broad usage across app and components
|
||||||
|
- examples:
|
||||||
|
- `components/root/root-dashboard-shell.tsx`
|
||||||
|
- `components/layout/site-header.tsx`
|
||||||
|
- `components/theme-toggle.tsx`
|
||||||
|
- `app/root/page.tsx`
|
||||||
|
- `app/[locale]/(site)/page.tsx`
|
||||||
|
|
||||||
|
No second React icon library is present in `package.json`.
|
||||||
|
|
||||||
|
## 10. Component architecture currently used
|
||||||
|
|
||||||
|
### Routing structure
|
||||||
|
|
||||||
|
- `app` is the real route layer.
|
||||||
|
- public site routes live under:
|
||||||
|
- `app/[locale]/(site)/*`
|
||||||
|
- admin routes live under:
|
||||||
|
- `app/root/*`
|
||||||
|
- `src` exists but is empty in the current codebase.
|
||||||
|
|
||||||
|
### Component folders
|
||||||
|
|
||||||
|
- `components/ui`
|
||||||
|
- primitive and near-primitive reusable controls
|
||||||
|
- `components/layout`
|
||||||
|
- site shell, hero, header, footer, container, backdrops
|
||||||
|
- `components/root`
|
||||||
|
- admin feature components and forms
|
||||||
|
- `components/dashboard`
|
||||||
|
- admin navigation and dashboard layout shell
|
||||||
|
- `components/site`
|
||||||
|
- site feature forms such as contact form
|
||||||
|
|
||||||
|
### Helper layer
|
||||||
|
|
||||||
|
- `lib/utils.ts`
|
||||||
|
- `cn()`
|
||||||
|
- `lib/root-navigation.ts`
|
||||||
|
- navigation config for admin shell
|
||||||
|
- other `lib/*`
|
||||||
|
- app settings, metadata, locale, media, portfolio, auth
|
||||||
|
|
||||||
|
### Wrappers around primitives
|
||||||
|
|
||||||
|
Confirmed custom wrappers around local primitive layer:
|
||||||
|
|
||||||
|
- `components/ui/app-card.tsx`
|
||||||
|
- wraps `Card`
|
||||||
|
- `components/theme-toggle.tsx`
|
||||||
|
- wraps `Button`
|
||||||
|
- `components/layout/locale-toggle.tsx`
|
||||||
|
- uses `Button` and `DropdownMenu`
|
||||||
|
- `components/root/form-save-button.tsx`
|
||||||
|
- uses `Button`
|
||||||
|
|
||||||
|
## 11. Violations or inconsistencies found in the current codebase
|
||||||
|
|
||||||
|
These are current inconsistencies visible in the implementation.
|
||||||
|
|
||||||
|
### Token system is mixed with raw one-off styling
|
||||||
|
|
||||||
|
Admin and primitive components strongly use semantic tokens:
|
||||||
|
|
||||||
|
- `bg-card`
|
||||||
|
- `bg-background`
|
||||||
|
- `border-input`
|
||||||
|
- `shadow-card`
|
||||||
|
- `rounded-surface`
|
||||||
|
|
||||||
|
But public chrome often bypasses them with raw arbitrary values:
|
||||||
|
|
||||||
|
- `components/layout/site-header.tsx`
|
||||||
|
- `components/layout/floating-preferences.tsx`
|
||||||
|
- `app/[locale]/(site)/page.tsx`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `bg-white/24`
|
||||||
|
- `border-black/6`
|
||||||
|
- `shadow-[0_14px_34px_-24px_rgba(15,23,42,0.18)]`
|
||||||
|
|
||||||
|
### Radius usage is not fully normalized
|
||||||
|
|
||||||
|
Tokenized radius classes exist, but these also appear:
|
||||||
|
|
||||||
|
- `rounded-md`
|
||||||
|
- `rounded-lg`
|
||||||
|
- `rounded-full`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
- `components/dashboard/dashboard-layout.tsx`
|
||||||
|
- `components/root/site-settings-form.tsx`
|
||||||
|
- `components/root/portfolio-project-form.tsx`
|
||||||
|
|
||||||
|
### Some primitives follow the local system, some keep stock shadcn-style values
|
||||||
|
|
||||||
|
- tokenized:
|
||||||
|
- `components/ui/card.tsx`
|
||||||
|
- `components/ui/button.tsx`
|
||||||
|
- `components/ui/input.tsx`
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
- less tokenized:
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
- `sm:rounded-lg`
|
||||||
|
- `shadow-lg`
|
||||||
|
- less tokenized:
|
||||||
|
- `components/ui/sheet.tsx`
|
||||||
|
- `shadow-lg`
|
||||||
|
|
||||||
|
### `AppCard` surface levels are not all visually distinct
|
||||||
|
|
||||||
|
In `components/ui/app-card.tsx`:
|
||||||
|
|
||||||
|
- `level: 1` and `level: 2` both use:
|
||||||
|
- `border border-border/80 bg-card text-card-foreground shadow-card`
|
||||||
|
|
||||||
|
So the file defines multiple levels, but two of them are currently identical.
|
||||||
|
|
||||||
|
### Primitive implementation style is not uniform
|
||||||
|
|
||||||
|
Most interactive primitives use Radix wrappers, but `components/ui/tabs.tsx` is a custom React context implementation instead.
|
||||||
|
|
||||||
|
### Tailwind scans `src`, but `src` is empty
|
||||||
|
|
||||||
|
`tailwind.config.ts`
|
||||||
|
includes:
|
||||||
|
|
||||||
|
- `./src/**/*.{js,ts,jsx,tsx,mdx}`
|
||||||
|
|
||||||
|
Current project state:
|
||||||
|
|
||||||
|
- `src` contains no files.
|
||||||
|
|
||||||
|
## 12. Codex rules derived from the existing system
|
||||||
|
|
||||||
|
These rules are derived from what the codebase already does.
|
||||||
|
|
||||||
|
- Use local primitives from `components/ui` before introducing direct Radix usage.
|
||||||
|
- Use `cn()` from `lib/utils.ts` for class composition.
|
||||||
|
- Use `cva()` for reusable variant APIs where the project already does that:
|
||||||
|
- `Button`
|
||||||
|
- `Badge`
|
||||||
|
- `AppCard`
|
||||||
|
- `Container`
|
||||||
|
- For shared admin surfaces, prefer tokenized classes already present:
|
||||||
|
- `rounded-surface`
|
||||||
|
- `rounded-nested`
|
||||||
|
- `bg-card`
|
||||||
|
- `bg-background`
|
||||||
|
- `border-input`
|
||||||
|
- `border-border/80`
|
||||||
|
- `shadow-card`
|
||||||
|
- `shadow-panel`
|
||||||
|
- For main width constraints, follow the existing container system:
|
||||||
|
- `Container`
|
||||||
|
- `max-w-layout`
|
||||||
|
- `max-w-admin`
|
||||||
|
- `px-4 sm:px-6 lg:px-8`
|
||||||
|
- For admin pages and forms, the repeated layout rhythm is:
|
||||||
|
- `space-y-6`
|
||||||
|
- `gap-6`
|
||||||
|
- `space-y-2`
|
||||||
|
- `p-6`
|
||||||
|
- `p-4`
|
||||||
|
- For public site hero/layout sections, the repeated shell is:
|
||||||
|
- `hero-surface`
|
||||||
|
- `max-w-[72rem]`
|
||||||
|
- `px-4 sm:px-6 lg:px-8`
|
||||||
|
- `pt-24 sm:pt-28`
|
||||||
|
- Use `lucide-react` for icons.
|
||||||
|
- Use `framer-motion` for custom motion and `tailwindcss-animate` for Radix state transitions.
|
||||||
|
- Treat `shadcn/ui` in this repo as a local customized base, not as a stock untouched library.
|
||||||
@@ -0,0 +1,536 @@
|
|||||||
|
# Frontend System Current State
|
||||||
|
|
||||||
|
## 1. UI primitive source of truth
|
||||||
|
|
||||||
|
The current primitive source of truth is:
|
||||||
|
|
||||||
|
- `components/ui/button.tsx`
|
||||||
|
- `components/ui/card.tsx`
|
||||||
|
- `components/ui/app-card.tsx`
|
||||||
|
- `components/ui/input.tsx`
|
||||||
|
- `components/ui/textarea.tsx`
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
- `components/ui/sheet.tsx`
|
||||||
|
- `components/ui/tabs.tsx`
|
||||||
|
- `components/ui/dropdown-menu.tsx`
|
||||||
|
- `components/ui/accordion.tsx`
|
||||||
|
- `components/ui/badge.tsx`
|
||||||
|
- `components/ui/checkbox.tsx`
|
||||||
|
- `components/ui/table.tsx`
|
||||||
|
- `components/ui/label.tsx`
|
||||||
|
- `components/ui/separator.tsx`
|
||||||
|
|
||||||
|
Feature code currently reuses these primitives directly.
|
||||||
|
|
||||||
|
## 2. Libraries currently used for UI, motion, icons, and class composition
|
||||||
|
|
||||||
|
UI primitives and wrappers:
|
||||||
|
|
||||||
|
- `@radix-ui/react-accordion`
|
||||||
|
- `@radix-ui/react-checkbox`
|
||||||
|
- `@radix-ui/react-dialog`
|
||||||
|
- `@radix-ui/react-dropdown-menu`
|
||||||
|
- `@radix-ui/react-select`
|
||||||
|
- `@radix-ui/react-slot`
|
||||||
|
|
||||||
|
Motion:
|
||||||
|
|
||||||
|
- `framer-motion`
|
||||||
|
- `tailwindcss-animate`
|
||||||
|
|
||||||
|
Icons:
|
||||||
|
|
||||||
|
- `lucide-react`
|
||||||
|
|
||||||
|
Class composition:
|
||||||
|
|
||||||
|
- `class-variance-authority`
|
||||||
|
- `clsx`
|
||||||
|
- `tailwind-merge`
|
||||||
|
|
||||||
|
Shared helper:
|
||||||
|
|
||||||
|
- `lib/utils.ts`
|
||||||
|
- `cn(...inputs)`
|
||||||
|
|
||||||
|
Source files:
|
||||||
|
|
||||||
|
- `package.json`
|
||||||
|
- `lib/utils.ts`
|
||||||
|
|
||||||
|
## 3. Current token system in use
|
||||||
|
|
||||||
|
The active semantic token layer is defined in:
|
||||||
|
|
||||||
|
- `app/globals.css`
|
||||||
|
- `tailwind.config.ts`
|
||||||
|
|
||||||
|
Current color token groups exposed in Tailwind:
|
||||||
|
|
||||||
|
- `background`
|
||||||
|
- `foreground`
|
||||||
|
- `card`
|
||||||
|
- `card-foreground`
|
||||||
|
- `popover`
|
||||||
|
- `popover-foreground`
|
||||||
|
- `primary`
|
||||||
|
- `primary-foreground`
|
||||||
|
- `secondary`
|
||||||
|
- `secondary-foreground`
|
||||||
|
- `muted`
|
||||||
|
- `muted-foreground`
|
||||||
|
- `accent`
|
||||||
|
- `accent-foreground`
|
||||||
|
- `destructive`
|
||||||
|
- `destructive-foreground`
|
||||||
|
- `border`
|
||||||
|
- `border-strong`
|
||||||
|
- `input`
|
||||||
|
- `ring`
|
||||||
|
- `surface.1`
|
||||||
|
- `surface.2`
|
||||||
|
- `surface.3`
|
||||||
|
- `surface.inverse`
|
||||||
|
- `brand.primary`
|
||||||
|
- `brand.secondary`
|
||||||
|
- `status.success`
|
||||||
|
- `status.success-soft`
|
||||||
|
- `status.warning`
|
||||||
|
- `status.warning-soft`
|
||||||
|
- `sidebar.*`
|
||||||
|
|
||||||
|
Current tokenized utility usage in shared UI is based on:
|
||||||
|
|
||||||
|
- `bg-card`
|
||||||
|
- `bg-background`
|
||||||
|
- `bg-surface-2`
|
||||||
|
- `text-card-foreground`
|
||||||
|
- `text-foreground`
|
||||||
|
- `text-muted-foreground`
|
||||||
|
- `border-border/80`
|
||||||
|
- `border-border/70`
|
||||||
|
- `border-input`
|
||||||
|
- `hover:bg-accent`
|
||||||
|
|
||||||
|
## 4. Current radius system in use
|
||||||
|
|
||||||
|
Defined in:
|
||||||
|
|
||||||
|
- `app/globals.css`
|
||||||
|
- `tailwind.config.ts`
|
||||||
|
|
||||||
|
Current radius tokens:
|
||||||
|
|
||||||
|
- `--radius-surface: 24px`
|
||||||
|
- `--radius-nested: 16px`
|
||||||
|
- `--radius-pill: 9999px`
|
||||||
|
|
||||||
|
Current mapped utilities:
|
||||||
|
|
||||||
|
- `rounded-surface`
|
||||||
|
- `rounded-nested`
|
||||||
|
- `rounded-pill`
|
||||||
|
|
||||||
|
Current shared primitive usage:
|
||||||
|
|
||||||
|
- surface containers:
|
||||||
|
- `rounded-surface`
|
||||||
|
- `components/ui/card.tsx`
|
||||||
|
- `components/ui/app-card.tsx`
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
- `components/ui/tabs.tsx`
|
||||||
|
- controls and nested blocks:
|
||||||
|
- `rounded-nested`
|
||||||
|
- `components/ui/button.tsx`
|
||||||
|
- `components/ui/input.tsx`
|
||||||
|
- `components/ui/textarea.tsx`
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
- `components/ui/sheet.tsx`
|
||||||
|
- pill controls:
|
||||||
|
- `rounded-pill`
|
||||||
|
- `components/layout/site-header.tsx`
|
||||||
|
- `components/layout/floating-preferences.tsx`
|
||||||
|
|
||||||
|
## 5. Current surface/card/panel rules in use
|
||||||
|
|
||||||
|
### Base `Card`
|
||||||
|
|
||||||
|
Source:
|
||||||
|
|
||||||
|
- `components/ui/card.tsx`
|
||||||
|
|
||||||
|
Current base rule:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
rounded-surface border border-border/80 bg-card text-card-foreground shadow-card
|
||||||
|
```
|
||||||
|
|
||||||
|
### `AppCard`
|
||||||
|
|
||||||
|
Source:
|
||||||
|
|
||||||
|
- `components/ui/app-card.tsx`
|
||||||
|
|
||||||
|
Current visual levels:
|
||||||
|
|
||||||
|
- `level={1}`
|
||||||
|
- `border border-border/80 bg-card text-card-foreground shadow-card`
|
||||||
|
- `level={2}`
|
||||||
|
- `border border-input bg-background text-foreground shadow-xs`
|
||||||
|
- `level={3}`
|
||||||
|
- `border border-border/80 bg-surface-2 text-foreground shadow-panel`
|
||||||
|
- `level="inverse"`
|
||||||
|
- `border-transparent bg-foreground text-background shadow-card`
|
||||||
|
|
||||||
|
Current padding variants:
|
||||||
|
|
||||||
|
- `none`
|
||||||
|
- `sm`
|
||||||
|
- `p-4`
|
||||||
|
- `md`
|
||||||
|
- `p-6`
|
||||||
|
- `lg`
|
||||||
|
- `p-8`
|
||||||
|
|
||||||
|
Current interactive rule:
|
||||||
|
|
||||||
|
- `hover:bg-accent/30 hover:text-accent-foreground`
|
||||||
|
|
||||||
|
### Current surface usage in feature code
|
||||||
|
|
||||||
|
Nested surface blocks currently reuse `AppCard` directly in:
|
||||||
|
|
||||||
|
- `components/root/media-field-picker.tsx`
|
||||||
|
- `components/root/portfolio-projects-overview.tsx`
|
||||||
|
- `components/root/portfolio-project-form.tsx`
|
||||||
|
- `components/root/portfolio-categories-manager.tsx`
|
||||||
|
- `components/root/site-settings-form.tsx`
|
||||||
|
|
||||||
|
## 6. Current input/button/select/dialog/sheet rules in use
|
||||||
|
|
||||||
|
### Button
|
||||||
|
|
||||||
|
Source:
|
||||||
|
|
||||||
|
- `components/ui/button.tsx`
|
||||||
|
|
||||||
|
Base:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Current variants:
|
||||||
|
|
||||||
|
- `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`
|
||||||
|
|
||||||
|
Current sizes:
|
||||||
|
|
||||||
|
- `default`
|
||||||
|
- `h-10 px-4 py-2`
|
||||||
|
- `sm`
|
||||||
|
- `h-9 px-3`
|
||||||
|
- `lg`
|
||||||
|
- `h-11 px-6`
|
||||||
|
- `icon`
|
||||||
|
- `h-10 w-10`
|
||||||
|
|
||||||
|
### Input
|
||||||
|
|
||||||
|
Source:
|
||||||
|
|
||||||
|
- `components/ui/input.tsx`
|
||||||
|
|
||||||
|
Current rule:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
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 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Select
|
||||||
|
|
||||||
|
Source:
|
||||||
|
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
|
||||||
|
Current trigger rule:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
flex h-10 w-full items-center justify-between rounded-nested border border-input bg-background px-3 py-2 text-sm shadow-xs
|
||||||
|
```
|
||||||
|
|
||||||
|
Current content rule:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
rounded-surface border border-input bg-background text-foreground shadow-panel
|
||||||
|
```
|
||||||
|
|
||||||
|
Current item rule:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
rounded-nested ... focus:bg-accent focus:text-accent-foreground
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dialog
|
||||||
|
|
||||||
|
Source:
|
||||||
|
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
|
||||||
|
Current overlay rule:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
fixed inset-0 z-50 bg-foreground/30 backdrop-blur-sm
|
||||||
|
```
|
||||||
|
|
||||||
|
Current content rule:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
rounded-surface border border-border/80 bg-card p-6 text-card-foreground shadow-panel
|
||||||
|
```
|
||||||
|
|
||||||
|
Current close button rule:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
inline-flex h-8 w-8 items-center justify-center rounded-nested text-muted-foreground hover:bg-accent hover:text-foreground
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sheet
|
||||||
|
|
||||||
|
Source:
|
||||||
|
|
||||||
|
- `components/ui/sheet.tsx`
|
||||||
|
|
||||||
|
Current overlay rule matches `Dialog`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
fixed inset-0 z-50 bg-foreground/30 backdrop-blur-sm
|
||||||
|
```
|
||||||
|
|
||||||
|
Current content rule:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
border-border/80 bg-card p-6 text-card-foreground shadow-panel
|
||||||
|
```
|
||||||
|
|
||||||
|
Current side radius rules:
|
||||||
|
|
||||||
|
- `sm:rounded-l-surface`
|
||||||
|
- `sm:rounded-r-surface`
|
||||||
|
- `rounded-b-surface`
|
||||||
|
- `rounded-t-surface`
|
||||||
|
|
||||||
|
### Tabs
|
||||||
|
|
||||||
|
Source:
|
||||||
|
|
||||||
|
- `components/ui/tabs.tsx`
|
||||||
|
|
||||||
|
Current list rule:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
inline-flex ... gap-2 rounded-surface border border-border/80 bg-muted/40 p-1.5
|
||||||
|
```
|
||||||
|
|
||||||
|
Current trigger rule:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
rounded-nested px-3 py-2 text-sm font-medium text-muted-foreground
|
||||||
|
```
|
||||||
|
|
||||||
|
Current active rule:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
border border-border/80 bg-background text-foreground shadow-xs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Current layout/container rules in use
|
||||||
|
|
||||||
|
### Container
|
||||||
|
|
||||||
|
Source:
|
||||||
|
|
||||||
|
- `components/layout/container.tsx`
|
||||||
|
|
||||||
|
Current base container rule:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
mx-auto w-full px-4 sm:px-6 lg:px-8
|
||||||
|
```
|
||||||
|
|
||||||
|
Current size variants:
|
||||||
|
|
||||||
|
- `default`
|
||||||
|
- `max-w-layout`
|
||||||
|
- `narrow`
|
||||||
|
- `max-w-narrow`
|
||||||
|
- `wide`
|
||||||
|
- `max-w-wide`
|
||||||
|
- `admin`
|
||||||
|
- `max-w-admin`
|
||||||
|
|
||||||
|
### Shared public chrome
|
||||||
|
|
||||||
|
Current shared top controls use:
|
||||||
|
|
||||||
|
- `rounded-pill`
|
||||||
|
- `border-border/70`
|
||||||
|
- `bg-background/80`
|
||||||
|
- `shadow-panel`
|
||||||
|
- `backdrop-blur-chrome`
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- `components/layout/site-header.tsx`
|
||||||
|
- `components/layout/floating-preferences.tsx`
|
||||||
|
|
||||||
|
### Admin shell
|
||||||
|
|
||||||
|
Current admin layout still uses:
|
||||||
|
|
||||||
|
- `min-h-screen bg-muted/30`
|
||||||
|
- `grid min-h-screen lg:grid-cols-[280px_minmax(0,1fr)]`
|
||||||
|
- `sticky top-0`
|
||||||
|
- `p-4 lg:p-6`
|
||||||
|
|
||||||
|
Source:
|
||||||
|
|
||||||
|
- `components/dashboard/dashboard-layout.tsx`
|
||||||
|
|
||||||
|
## 8. Current spacing rhythm in use
|
||||||
|
|
||||||
|
Current repeated spacing values across primitives and layout:
|
||||||
|
|
||||||
|
- `p-4`
|
||||||
|
- `p-6`
|
||||||
|
- `p-8`
|
||||||
|
- `px-3 py-2`
|
||||||
|
- `px-4 py-2`
|
||||||
|
- `px-4 py-3`
|
||||||
|
- `px-4 py-4`
|
||||||
|
- `gap-2`
|
||||||
|
- `gap-3`
|
||||||
|
- `gap-4`
|
||||||
|
- `gap-6`
|
||||||
|
- `space-y-2`
|
||||||
|
- `space-y-3`
|
||||||
|
- `space-y-4`
|
||||||
|
- `space-y-6`
|
||||||
|
|
||||||
|
Current primitive spacing sources:
|
||||||
|
|
||||||
|
- `CardHeader`
|
||||||
|
- `gap-1.5 p-6`
|
||||||
|
- `CardContent`
|
||||||
|
- `p-6 pt-0`
|
||||||
|
- `AppCard padding="sm"`
|
||||||
|
- `p-4`
|
||||||
|
- `AppCard padding="md"`
|
||||||
|
- `p-6`
|
||||||
|
- `AppCard padding="lg"`
|
||||||
|
- `p-8`
|
||||||
|
- `Container`
|
||||||
|
- `px-4 sm:px-6 lg:px-8`
|
||||||
|
|
||||||
|
## 9. Current motion rules in use
|
||||||
|
|
||||||
|
Primary motion library:
|
||||||
|
|
||||||
|
- `framer-motion`
|
||||||
|
|
||||||
|
State animation utility layer:
|
||||||
|
|
||||||
|
- `tailwindcss-animate`
|
||||||
|
|
||||||
|
Current motion patterns:
|
||||||
|
|
||||||
|
- reveal-on-scroll wrapper
|
||||||
|
- `components/motion-fade.tsx`
|
||||||
|
- `initial: { opacity: 0, y: 20 }`
|
||||||
|
- `whileInView: { opacity: 1, y: 0 }`
|
||||||
|
- `transition: { duration: 0.45, ease: "easeOut" }`
|
||||||
|
- shared overlay open/close classes
|
||||||
|
- `dialog`
|
||||||
|
- `sheet`
|
||||||
|
- `select`
|
||||||
|
- `dropdown-menu`
|
||||||
|
- `accordion`
|
||||||
|
- animated nav active pill and mobile menu
|
||||||
|
- `components/layout/site-header.tsx`
|
||||||
|
|
||||||
|
## 10. Current icon rules in use
|
||||||
|
|
||||||
|
Current icon library:
|
||||||
|
|
||||||
|
- `lucide-react`
|
||||||
|
|
||||||
|
Current shared rule:
|
||||||
|
|
||||||
|
Icons are imported directly from `lucide-react` and passed through local primitives or feature components.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
- `components/layout/site-header.tsx`
|
||||||
|
- `components/theme-toggle.tsx`
|
||||||
|
- `components/root/portfolio-project-form.tsx`
|
||||||
|
|
||||||
|
No second React icon library is currently present in `package.json`.
|
||||||
|
|
||||||
|
## 11. Current forbidden drift or remaining transitional areas, if any
|
||||||
|
|
||||||
|
The shared primitive system is currently tokenized.
|
||||||
|
|
||||||
|
Remaining transitional areas still present in the codebase:
|
||||||
|
|
||||||
|
- public hero / atmospheric visuals still use raw gradients and `rgba(...)` decorative backgrounds
|
||||||
|
- `components/layout/home-hero.tsx`
|
||||||
|
- `components/layout/site-ambient-backdrop.tsx`
|
||||||
|
- `components/layout/hero-atmosphere.tsx`
|
||||||
|
- `components/layout/site-logo.tsx`
|
||||||
|
- `coming-soon` page still uses older raw translucent chrome styles
|
||||||
|
- `app/[locale]/coming-soon/page.tsx`
|
||||||
|
- some public route-level content blocks still use direct local classes instead of shared primitive wrappers
|
||||||
|
- `app/[locale]/(site)/portfolio/page.tsx`
|
||||||
|
- `app/[locale]/(site)/portfolio/[slug]/page.tsx`
|
||||||
|
- one preview block in site settings still uses a custom dark preview surface
|
||||||
|
- `components/root/site-settings-form.tsx`
|
||||||
|
- `bg-slate-950`
|
||||||
|
- `text-white/55`
|
||||||
|
- `text-white/60`
|
||||||
|
|
||||||
|
These files exist in the current codebase and should be treated as remaining transitional areas, not as shared-system rules.
|
||||||
|
|
||||||
|
## 12. Codex rules derived from the current system
|
||||||
|
|
||||||
|
- Use `components/ui` as the primitive source of truth.
|
||||||
|
- Use `cn()` from `lib/utils.ts` for class composition.
|
||||||
|
- Use `cva()` where the primitive already exposes variants.
|
||||||
|
- Use semantic tokens already defined in `app/globals.css` and mapped in `tailwind.config.ts`.
|
||||||
|
- Use `Card` for the base surface:
|
||||||
|
- `rounded-surface border border-border/80 bg-card text-card-foreground shadow-card`
|
||||||
|
- Use `AppCard` when feature code needs explicit shared surface levels.
|
||||||
|
- Use `AppCard level={1}` for primary card surfaces.
|
||||||
|
- Use `AppCard level={2}` for nested panels and inner blocks.
|
||||||
|
- Use `AppCard level={3}` for elevated grouped panels.
|
||||||
|
- Use `rounded-surface`, `rounded-nested`, and `rounded-pill` as the shared radius system.
|
||||||
|
- Use `Input`, `Textarea`, `Select`, `Dialog`, `Sheet`, and `Tabs` from `components/ui` instead of re-creating those patterns in feature code.
|
||||||
|
- Use `Container` for width and horizontal padding rules.
|
||||||
|
- Use `lucide-react` for icons.
|
||||||
|
- Use `framer-motion` for custom motion and `tailwindcss-animate` for primitive state transitions.
|
||||||
|
- Do not treat transitional decorative hero files or remaining route-local raw blocks as shared-system primitives.
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Frontend System Refactor Summary
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This refactor was applied directly in the codebase.
|
||||||
|
|
||||||
|
It focused on:
|
||||||
|
|
||||||
|
- radius normalization
|
||||||
|
- primitive normalization
|
||||||
|
- removing arbitrary visual drift from shared UI
|
||||||
|
- standardizing shared surface patterns
|
||||||
|
- fixing the false abstraction in `components/ui/app-card.tsx`
|
||||||
|
|
||||||
|
## Architecture Preserved
|
||||||
|
|
||||||
|
- `components/ui` remains the primitive source of truth
|
||||||
|
- `cn()` in `lib/utils.ts` remains unchanged and is still the shared class composition helper
|
||||||
|
- existing semantic token naming remains unchanged
|
||||||
|
- no new UI library was introduced
|
||||||
|
|
||||||
|
Current primitive layer remains in:
|
||||||
|
|
||||||
|
- `components/ui/button.tsx`
|
||||||
|
- `components/ui/card.tsx`
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
- `components/ui/select.tsx`
|
||||||
|
- `components/ui/sheet.tsx`
|
||||||
|
- `components/ui/tabs.tsx`
|
||||||
|
- `components/ui/app-card.tsx`
|
||||||
|
|
||||||
|
## What Changed
|
||||||
|
|
||||||
|
### 1. Radius normalization
|
||||||
|
|
||||||
|
Shared system areas were normalized toward:
|
||||||
|
|
||||||
|
- `rounded-surface`
|
||||||
|
- `rounded-nested`
|
||||||
|
- `rounded-pill`
|
||||||
|
|
||||||
|
This removed inconsistent `rounded-md` and `rounded-lg` usage from the shared admin and primitive system areas.
|
||||||
|
|
||||||
|
### 2. Primitive normalization
|
||||||
|
|
||||||
|
The following primitives were aligned with the project token system:
|
||||||
|
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
- `components/ui/sheet.tsx`
|
||||||
|
- `components/ui/tabs.tsx`
|
||||||
|
|
||||||
|
Changes included:
|
||||||
|
|
||||||
|
- tokenized surface colors
|
||||||
|
- tokenized borders
|
||||||
|
- tokenized radius
|
||||||
|
- tokenized shadows
|
||||||
|
- unified close button treatment in `dialog` and `sheet`
|
||||||
|
|
||||||
|
### 3. Shared UI drift removal
|
||||||
|
|
||||||
|
Raw visual values were replaced with semantic tokens in:
|
||||||
|
|
||||||
|
- `components/layout/site-header.tsx`
|
||||||
|
- `components/layout/floating-preferences.tsx`
|
||||||
|
- `app/[locale]/(site)/page.tsx`
|
||||||
|
|
||||||
|
This removed raw usage patterns such as:
|
||||||
|
|
||||||
|
- hardcoded white/black alpha surfaces
|
||||||
|
- raw `rgba(...)` shadows
|
||||||
|
- raw `text-[hsl(...)]` color usage for shared controls
|
||||||
|
|
||||||
|
### 4. Shared surface standardization
|
||||||
|
|
||||||
|
Repeated nested panels and stat blocks in feature code were moved toward shared local primitives, especially `AppCard`.
|
||||||
|
|
||||||
|
Refactored areas include:
|
||||||
|
|
||||||
|
- `components/root/media-field-picker.tsx`
|
||||||
|
- `components/root/portfolio-projects-overview.tsx`
|
||||||
|
- `components/root/portfolio-project-form.tsx`
|
||||||
|
- `components/root/portfolio-categories-manager.tsx`
|
||||||
|
- `components/root/site-settings-form.tsx`
|
||||||
|
|
||||||
|
### 5. False abstraction fixed
|
||||||
|
|
||||||
|
`components/ui/app-card.tsx` previously declared multiple levels where `level 1` and `level 2` were visually identical.
|
||||||
|
|
||||||
|
That was corrected.
|
||||||
|
|
||||||
|
Current `AppCard` levels are now meaningfully distinct:
|
||||||
|
|
||||||
|
- `level 1`
|
||||||
|
- `bg-card`
|
||||||
|
- `shadow-card`
|
||||||
|
- `level 2`
|
||||||
|
- `bg-background`
|
||||||
|
- `border-input`
|
||||||
|
- `shadow-xs`
|
||||||
|
- `level 3`
|
||||||
|
- `bg-surface-2`
|
||||||
|
- `shadow-panel`
|
||||||
|
- `inverse`
|
||||||
|
- preserved as contrast surface
|
||||||
|
|
||||||
|
## Files Updated
|
||||||
|
|
||||||
|
- `app/[locale]/(site)/page.tsx`
|
||||||
|
- `app/root/media/page.tsx`
|
||||||
|
- `app/root/page.tsx`
|
||||||
|
- `components/dashboard/dashboard-card.tsx`
|
||||||
|
- `components/dashboard/dashboard-layout.tsx`
|
||||||
|
- `components/layout/floating-preferences.tsx`
|
||||||
|
- `components/layout/site-header.tsx`
|
||||||
|
- `components/root/flash-message.tsx`
|
||||||
|
- `components/root/media-field-picker.tsx`
|
||||||
|
- `components/root/portfolio-categories-manager.tsx`
|
||||||
|
- `components/root/portfolio-project-form.tsx`
|
||||||
|
- `components/root/portfolio-projects-overview.tsx`
|
||||||
|
- `components/root/portfolio-subnav.tsx`
|
||||||
|
- `components/root/site-settings-form.tsx`
|
||||||
|
- `components/ui/app-card.tsx`
|
||||||
|
- `components/ui/dialog.tsx`
|
||||||
|
- `components/ui/sheet.tsx`
|
||||||
|
- `components/ui/tabs.tsx`
|
||||||
|
- `components/ui/ui-kit-showcase.tsx`
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Validation run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
- passed with no ESLint errors or warnings
|
||||||
Reference in New Issue
Block a user