feat(frontend): moderneres visuelles Redesign (Farben, Tiefe, Dashboard-Hero)
Kartenhintergrund und Seitenhintergrund waren fast farbgleich, wodurch alles flach wirkte trotz vorhandenem Schatten. Staerkerer Kontrast (--background/--card/--muted/--border), neue Gradient- und Schatten-Tokens (--gradient-honey/-sage/-sky, --shadow-soft/-elevated). Card-Komponente: rounded-2xl, echter sichtbarer Schatten, optionaler Hover-Lift (interactive-Prop). Sidebar: solide Gradient-Badge, kraeftiger Active-Pill-Zustand statt blassem Tint. Header: sticky mit Blur-Schatten statt hartem Rand. Dashboard: Gradient-Hero mit Begruessung und Honeycomb-Muster, farbige Gradient-Icon-Badges pro KPI-Kachel, Hinweise-Zeilen mit Icon statt rohem Enum-Badge (dabei toten CONSPICUOUS_COLONY-Style-Key gefunden und auf COLONY_ATTENTION korrigiert). Honeycomb-Hintergrund aus der Login-Seite in eine gemeinsame Komponente extrahiert und dort mit dem gleichen Gradient-Badge-Header konsistent gemacht.
This commit is contained in:
1 parent
6ab028d1be
commit
5cacc3c60d
7 files changed
+198
-99
No files matched your search
@@ -10,11 +10,14 @@ import {
|
||||
type LucideIcon,
|
||||
MapPin,
|
||||
Layers,
|
||||
PackageSearch,
|
||||
ShieldAlert,
|
||||
} from "lucide-react";
|
||||
import { useAuth } from "@/lib/auth/auth-context";
|
||||
import { fetchDashboardAlerts, fetchDashboardSummary } from "@/lib/dashboard/api";
|
||||
import { DashboardAlert } from "@/lib/dashboard/types";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DashboardAlert, DashboardAlertType } from "@/lib/dashboard/types";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { HoneycombPattern } from "@/components/honeycomb-pattern";
|
||||
|
||||
function alertHref(alert: DashboardAlert): string | null {
|
||||
if (alert.relatedType === "Colony") return `/colonies/${alert.relatedId}`;
|
||||
@@ -23,26 +26,43 @@ function alertHref(alert: DashboardAlert): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ALERT_STYLES: Record<string, string> = {
|
||||
OVERDUE_TASK: "border-destructive/30 bg-destructive/5",
|
||||
CONSPICUOUS_COLONY: "border-warning/40 bg-warning/10",
|
||||
LOW_STOCK: "border-warning/40 bg-warning/10",
|
||||
};
|
||||
function greeting(): string {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 11) return "Guten Morgen";
|
||||
if (hour < 18) return "Guten Tag";
|
||||
return "Guten Abend";
|
||||
}
|
||||
|
||||
interface StatTile {
|
||||
label: string;
|
||||
value: number;
|
||||
icon: LucideIcon;
|
||||
tone: "primary" | "secondary" | "warning";
|
||||
gradient: string;
|
||||
}
|
||||
|
||||
const TONE_CLASSES: Record<StatTile["tone"], string> = {
|
||||
primary: "bg-primary/10 text-primary",
|
||||
secondary: "bg-secondary/10 text-secondary",
|
||||
warning: "bg-warning/15 text-warning-foreground",
|
||||
const ALERT_META: Record<
|
||||
DashboardAlertType,
|
||||
{ label: string; icon: LucideIcon; className: string }
|
||||
> = {
|
||||
OVERDUE_TASK: {
|
||||
label: "Aufgabe überfällig",
|
||||
icon: ListTodo,
|
||||
className: "border-destructive/30 bg-destructive/5",
|
||||
},
|
||||
COLONY_ATTENTION: {
|
||||
label: "Volk auffällig",
|
||||
icon: ShieldAlert,
|
||||
className: "border-warning/40 bg-warning/10",
|
||||
},
|
||||
LOW_STOCK: {
|
||||
label: "Bestand niedrig",
|
||||
icon: PackageSearch,
|
||||
className: "border-warning/40 bg-warning/10",
|
||||
},
|
||||
};
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user } = useAuth();
|
||||
const { data: summary, isLoading: summaryLoading } = useQuery({
|
||||
queryKey: ["dashboard", "summary"],
|
||||
queryFn: fetchDashboardSummary,
|
||||
@@ -54,25 +74,62 @@ export default function DashboardPage() {
|
||||
|
||||
const tiles: StatTile[] = summary
|
||||
? [
|
||||
{ label: "Aktive Bienenstände", value: summary.apiariesActive, icon: MapPin, tone: "primary" },
|
||||
{ label: "Aktive Völker", value: summary.coloniesActive, icon: Layers, tone: "primary" },
|
||||
{ label: "Aktive Königinnen", value: summary.queensActive, icon: Crown, tone: "secondary" },
|
||||
{ label: "Offene Aufgaben", value: summary.openTasks, icon: ListTodo, tone: "secondary" },
|
||||
{ label: "Honigchargen", value: summary.honeyBatchesTotal, icon: Droplet, tone: "primary" },
|
||||
{
|
||||
label: "Aktive Bienenstände",
|
||||
value: summary.apiariesActive,
|
||||
icon: MapPin,
|
||||
gradient: "var(--gradient-honey)",
|
||||
},
|
||||
{
|
||||
label: "Aktive Völker",
|
||||
value: summary.coloniesActive,
|
||||
icon: Layers,
|
||||
gradient: "var(--gradient-honey)",
|
||||
},
|
||||
{
|
||||
label: "Aktive Königinnen",
|
||||
value: summary.queensActive,
|
||||
icon: Crown,
|
||||
gradient: "var(--gradient-sage)",
|
||||
},
|
||||
{
|
||||
label: "Offene Aufgaben",
|
||||
value: summary.openTasks,
|
||||
icon: ListTodo,
|
||||
gradient: "var(--gradient-sky)",
|
||||
},
|
||||
{
|
||||
label: "Honigchargen",
|
||||
value: summary.honeyBatchesTotal,
|
||||
icon: Droplet,
|
||||
gradient: "var(--gradient-honey)",
|
||||
},
|
||||
{
|
||||
label: "Artikel unter Mindestbestand",
|
||||
value: summary.stockItemsLowCount,
|
||||
icon: AlertTriangle,
|
||||
tone: summary.stockItemsLowCount > 0 ? "warning" : "secondary",
|
||||
gradient:
|
||||
summary.stockItemsLowCount > 0
|
||||
? "linear-gradient(135deg, oklch(0.72 0.15 70) 0%, oklch(0.56 0.15 50) 100%)"
|
||||
: "var(--gradient-sage)",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-6 p-4 sm:p-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Dashboard</h1>
|
||||
<p className="text-sm text-muted-foreground">Überblick über deine Imkerei</p>
|
||||
<div
|
||||
className="relative overflow-hidden rounded-2xl px-5 py-6 text-white shadow-[var(--shadow-elevated)] sm:px-8 sm:py-8"
|
||||
style={{ background: "var(--gradient-honey)" }}
|
||||
>
|
||||
<HoneycombPattern className="pointer-events-none absolute inset-0 h-full w-full text-white/[0.08]" />
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<h1 className="font-heading text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||
{greeting()}
|
||||
{user ? `, ${user.email.split("@")[0]}` : ""}
|
||||
</h1>
|
||||
<p className="text-sm text-white/85 sm:text-base">Überblick über deine Imkerei</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{summaryLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
@@ -80,12 +137,13 @@ export default function DashboardPage() {
|
||||
{tiles.map((tile) => {
|
||||
const Icon = tile.icon;
|
||||
return (
|
||||
<Card key={tile.label}>
|
||||
<Card key={tile.label} interactive>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<div
|
||||
className={`flex size-9 items-center justify-center rounded-lg ${TONE_CLASSES[tile.tone]}`}
|
||||
className="flex size-10 items-center justify-center rounded-xl text-white shadow-[var(--shadow-soft)]"
|
||||
style={{ background: tile.gradient }}
|
||||
>
|
||||
<Icon className="size-4.5" />
|
||||
<Icon className="size-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-semibold tabular-nums">{tile.value}</p>
|
||||
@@ -98,7 +156,7 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="mb-3 text-lg font-semibold tracking-tight">Hinweise</h2>
|
||||
<h2 className="mb-3 font-heading text-lg font-semibold tracking-tight">Hinweise</h2>
|
||||
<div className="flex flex-col gap-2">
|
||||
{alertsLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
{!alertsLoading && (!alerts || alerts.length === 0) ? (
|
||||
@@ -110,21 +168,24 @@ export default function DashboardPage() {
|
||||
) : null}
|
||||
{alerts?.map((alert) => {
|
||||
const href = alertHref(alert);
|
||||
const meta = ALERT_META[alert.type];
|
||||
const AlertIcon = meta?.icon ?? AlertTriangle;
|
||||
return (
|
||||
<div
|
||||
key={`${alert.type}-${alert.relatedId}`}
|
||||
className={`flex items-center justify-between rounded-xl border p-3 text-sm ${ALERT_STYLES[alert.type] ?? "border-border bg-card"}`}
|
||||
className={`flex items-center gap-3 rounded-xl border p-3 text-sm shadow-[var(--shadow-soft)] ${meta?.className ?? "border-border bg-card"}`}
|
||||
>
|
||||
<span>{alert.message}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">{alert.type}</Badge>
|
||||
<AlertIcon className="size-4.5 shrink-0 text-muted-foreground" />
|
||||
<span className="flex-1">{alert.message}</span>
|
||||
{href ? (
|
||||
<Link className="font-medium text-primary underline-offset-4 hover:underline" href={href}>
|
||||
<Link
|
||||
className="shrink-0 font-medium text-primary underline-offset-4 hover:underline"
|
||||
href={href}
|
||||
>
|
||||
Ansehen
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -30,12 +30,17 @@ export default function AppLayout({ children }: { children: ReactNode }) {
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar user={user} />
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<header className="flex items-center justify-between gap-3 border-b bg-card px-4 py-2.5 sm:justify-end sm:px-6">
|
||||
<header className="sticky top-0 z-10 flex items-center justify-between gap-3 bg-card/85 px-4 py-3 shadow-[var(--shadow-soft)] backdrop-blur-md sm:justify-end sm:px-6">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="flex items-center gap-1.5 font-heading font-semibold sm:hidden"
|
||||
>
|
||||
<Hexagon className="size-5 fill-primary/15 text-primary" strokeWidth={2.25} />
|
||||
<span
|
||||
className="flex size-7 items-center justify-center rounded-lg"
|
||||
style={{ background: "var(--gradient-honey)" }}
|
||||
>
|
||||
<Hexagon className="size-4 fill-white/25 text-white" strokeWidth={2.25} />
|
||||
</span>
|
||||
ApiCore
|
||||
</Link>
|
||||
<div className="flex shrink-0 items-center gap-2 text-sm">
|
||||
|
||||
@@ -93,13 +93,18 @@ function SidebarLink({ item, pathname }: { item: NavItem; pathname: string }) {
|
||||
href={item.href}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-150",
|
||||
"group relative flex items-center gap-2.5 rounded-xl px-3 py-2 text-sm font-medium transition-all duration-150",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
? "bg-primary text-primary-foreground shadow-[var(--shadow-soft)]"
|
||||
: "text-muted-foreground hover:bg-accent/70 hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4.5 shrink-0" />
|
||||
<Icon
|
||||
className={cn(
|
||||
"size-4.5 shrink-0 transition-transform duration-150",
|
||||
!isActive && "group-hover:scale-110",
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
@@ -110,19 +115,24 @@ export function Sidebar({ user }: { user: AuthUser }) {
|
||||
const groups = user.role === "ADMIN" ? [...NAV_GROUPS, ADMIN_GROUP] : NAV_GROUPS;
|
||||
|
||||
return (
|
||||
<aside className="hidden w-60 shrink-0 flex-col gap-1 overflow-y-auto border-r bg-card px-3 py-4 sm:flex">
|
||||
<aside className="relative z-10 hidden w-64 shrink-0 flex-col gap-1 overflow-y-auto bg-sidebar px-3 py-5 shadow-[var(--shadow-soft)] sm:flex">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="mb-4 flex items-center gap-2 px-2 font-heading text-lg font-semibold"
|
||||
className="mb-5 flex items-center gap-2.5 px-2 font-heading text-lg font-semibold"
|
||||
>
|
||||
<Hexagon className="size-5.5 fill-primary/15 text-primary" strokeWidth={2.25} />
|
||||
<span
|
||||
className="flex size-8 items-center justify-center rounded-xl shadow-[var(--shadow-soft)]"
|
||||
style={{ background: "var(--gradient-honey)" }}
|
||||
>
|
||||
<Hexagon className="size-4.5 fill-white/25 text-white" strokeWidth={2.25} />
|
||||
</span>
|
||||
ApiCore
|
||||
</Link>
|
||||
<nav className="flex flex-1 flex-col gap-4">
|
||||
<nav className="flex flex-1 flex-col gap-5">
|
||||
{groups.map((group, index) => (
|
||||
<div key={group.label ?? `group-${index}`} className="flex flex-col gap-0.5">
|
||||
{group.label ? (
|
||||
<p className="px-3 pb-1 text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
||||
<p className="px-3 pb-1 text-xs font-semibold tracking-wide text-muted-foreground/80 uppercase">
|
||||
{group.label}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAuth } from "@/lib/auth/auth-context";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { HoneycombPattern } from "@/components/honeycomb-pattern";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -15,33 +16,6 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
function HoneycombBackdrop() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 h-full w-full text-primary/[0.07]"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="honeycomb"
|
||||
width="56"
|
||||
height="97"
|
||||
patternUnits="userSpaceOnUse"
|
||||
patternTransform="scale(1)"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
d="M28 0 56 16.2 56 48.5 28 64.7 0 48.5 0 16.2Z M28 64.7 56 80.8 56 97 28 97 0 97 0 80.8Z"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#honeycomb)" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const { login } = useAuth();
|
||||
@@ -66,11 +40,16 @@ export default function LoginPage() {
|
||||
|
||||
return (
|
||||
<main className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background p-4">
|
||||
<HoneycombBackdrop />
|
||||
<Card className="relative w-full max-w-sm">
|
||||
<HoneycombPattern />
|
||||
<Card className="relative w-full max-w-sm shadow-[var(--shadow-elevated)]">
|
||||
<CardHeader>
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<Hexagon className="size-6 fill-primary/15 text-primary" strokeWidth={2.25} />
|
||||
<div className="mb-1 flex items-center gap-2.5">
|
||||
<span
|
||||
className="flex size-9 items-center justify-center rounded-xl shadow-[var(--shadow-soft)]"
|
||||
style={{ background: "var(--gradient-honey)" }}
|
||||
>
|
||||
<Hexagon className="size-5 fill-white/25 text-white" strokeWidth={2.25} />
|
||||
</span>
|
||||
<CardTitle className="text-lg">ApiCore</CardTitle>
|
||||
</div>
|
||||
<CardDescription>Digitale Imkereiverwaltung für Wabenhain</CardDescription>
|
||||
|
||||
@@ -65,24 +65,24 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(0.98 0.012 85);
|
||||
--foreground: oklch(0.24 0.025 55);
|
||||
--card: oklch(0.995 0.006 85);
|
||||
--card-foreground: oklch(0.24 0.025 55);
|
||||
--popover: oklch(0.995 0.006 85);
|
||||
--popover-foreground: oklch(0.24 0.025 55);
|
||||
--primary: oklch(0.6 0.135 50);
|
||||
--primary-foreground: oklch(0.98 0.02 85);
|
||||
--secondary: oklch(0.46 0.06 145);
|
||||
--background: oklch(0.955 0.017 80);
|
||||
--foreground: oklch(0.22 0.028 55);
|
||||
--card: oklch(0.995 0.004 90);
|
||||
--card-foreground: oklch(0.22 0.028 55);
|
||||
--popover: oklch(0.995 0.004 90);
|
||||
--popover-foreground: oklch(0.22 0.028 55);
|
||||
--primary: oklch(0.6 0.15 48);
|
||||
--primary-foreground: oklch(0.99 0.02 85);
|
||||
--secondary: oklch(0.44 0.07 145);
|
||||
--secondary-foreground: oklch(0.98 0.015 100);
|
||||
--muted: oklch(0.93 0.016 85);
|
||||
--muted-foreground: oklch(0.5 0.03 65);
|
||||
--accent: oklch(0.94 0.028 75);
|
||||
--accent-foreground: oklch(0.28 0.04 50);
|
||||
--muted: oklch(0.9 0.02 82);
|
||||
--muted-foreground: oklch(0.48 0.032 65);
|
||||
--accent: oklch(0.93 0.032 74);
|
||||
--accent-foreground: oklch(0.26 0.045 50);
|
||||
--destructive: oklch(0.5 0.16 30);
|
||||
--border: oklch(0.88 0.02 78);
|
||||
--input: oklch(0.88 0.02 78);
|
||||
--ring: oklch(0.6 0.135 50);
|
||||
--border: oklch(0.85 0.024 78);
|
||||
--input: oklch(0.85 0.024 78);
|
||||
--ring: oklch(0.6 0.15 48);
|
||||
--warning: oklch(0.68 0.14 75);
|
||||
--warning-foreground: oklch(0.24 0.04 60);
|
||||
--success: oklch(0.46 0.06 145);
|
||||
@@ -93,14 +93,19 @@
|
||||
--chart-4: oklch(0.5 0.16 30);
|
||||
--chart-5: oklch(0.55 0.05 250);
|
||||
--radius: 0.75rem;
|
||||
--sidebar: oklch(0.98 0.012 85);
|
||||
--sidebar-foreground: oklch(0.24 0.025 55);
|
||||
--sidebar-primary: oklch(0.6 0.135 50);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.02 85);
|
||||
--sidebar-accent: oklch(0.94 0.028 75);
|
||||
--sidebar-accent-foreground: oklch(0.28 0.04 50);
|
||||
--sidebar-border: oklch(0.88 0.02 78);
|
||||
--sidebar-ring: oklch(0.6 0.135 50);
|
||||
--sidebar: oklch(0.995 0.004 90);
|
||||
--sidebar-foreground: oklch(0.22 0.028 55);
|
||||
--sidebar-primary: oklch(0.6 0.15 48);
|
||||
--sidebar-primary-foreground: oklch(0.99 0.02 85);
|
||||
--sidebar-accent: oklch(0.93 0.032 74);
|
||||
--sidebar-accent-foreground: oklch(0.26 0.045 50);
|
||||
--sidebar-border: oklch(0.85 0.024 78);
|
||||
--sidebar-ring: oklch(0.6 0.15 48);
|
||||
--gradient-honey: linear-gradient(135deg, oklch(0.74 0.15 62) 0%, oklch(0.56 0.16 38) 100%);
|
||||
--gradient-sage: linear-gradient(135deg, oklch(0.64 0.09 150) 0%, oklch(0.4 0.075 145) 100%);
|
||||
--gradient-sky: linear-gradient(135deg, oklch(0.68 0.1 235) 0%, oklch(0.5 0.1 255) 100%);
|
||||
--shadow-elevated: 0 1px 2px -1px oklch(0.3 0.05 50 / 8%), 0 8px 24px -8px oklch(0.3 0.05 50 / 16%);
|
||||
--shadow-soft: 0 1px 2px -1px oklch(0.3 0.05 50 / 6%), 0 4px 12px -4px oklch(0.3 0.05 50 / 10%);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -139,6 +144,11 @@
|
||||
--sidebar-accent-foreground: oklch(0.94 0.02 80);
|
||||
--sidebar-border: oklch(1 0 0 / 12%);
|
||||
--sidebar-ring: oklch(0.72 0.13 55);
|
||||
--gradient-honey: linear-gradient(135deg, oklch(0.62 0.14 55) 0%, oklch(0.38 0.13 40) 100%);
|
||||
--gradient-sage: linear-gradient(135deg, oklch(0.5 0.08 148) 0%, oklch(0.3 0.06 145) 100%);
|
||||
--gradient-sky: linear-gradient(135deg, oklch(0.55 0.09 235) 0%, oklch(0.34 0.09 255) 100%);
|
||||
--shadow-elevated: 0 1px 2px -1px oklch(0 0 0 / 30%), 0 8px 24px -8px oklch(0 0 0 / 45%);
|
||||
--shadow-soft: 0 1px 2px -1px oklch(0 0 0 / 25%), 0 4px 12px -4px oklch(0 0 0 / 30%);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
interface HoneycombPatternProps {
|
||||
className?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export function HoneycombPattern({ className, id = "honeycomb" }: HoneycombPatternProps) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={className ?? "pointer-events-none absolute inset-0 h-full w-full text-primary/[0.07]"}
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id={id}
|
||||
width="56"
|
||||
height="97"
|
||||
patternUnits="userSpaceOnUse"
|
||||
patternTransform="scale(1)"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
d="M28 0 56 16.2 56 48.5 28 64.7 0 48.5 0 16.2Z M28 64.7 56 80.8 56 97 28 97 0 97 0 80.8Z"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill={`url(#${id})`} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -5,14 +5,17 @@ import { cn } from "@/lib/utils"
|
||||
function Card({
|
||||
className,
|
||||
size = "default",
|
||||
interactive = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm"; interactive?: boolean }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl border border-border/60 bg-card py-(--card-spacing) text-sm text-card-foreground shadow-[0_1px_2px_-1px_rgba(33,20,10,0.06),0_2px_8px_-2px_rgba(33,20,10,0.06)] transition-shadow [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-2xl border border-border/50 bg-card py-(--card-spacing) text-sm text-card-foreground shadow-[var(--shadow-soft)] transition-all duration-200 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-2xl *:[img:last-child]:rounded-b-2xl",
|
||||
interactive &&
|
||||
"motion-safe:hover:-translate-y-0.5 hover:shadow-[var(--shadow-elevated)] hover:border-border",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in new issue
Block a user