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
+202
-103
No files matched your search
@@ -10,11 +10,14 @@ import {
|
|||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
MapPin,
|
MapPin,
|
||||||
Layers,
|
Layers,
|
||||||
|
PackageSearch,
|
||||||
|
ShieldAlert,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useAuth } from "@/lib/auth/auth-context";
|
||||||
import { fetchDashboardAlerts, fetchDashboardSummary } from "@/lib/dashboard/api";
|
import { fetchDashboardAlerts, fetchDashboardSummary } from "@/lib/dashboard/api";
|
||||||
import { DashboardAlert } from "@/lib/dashboard/types";
|
import { DashboardAlert, DashboardAlertType } from "@/lib/dashboard/types";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { HoneycombPattern } from "@/components/honeycomb-pattern";
|
||||||
|
|
||||||
function alertHref(alert: DashboardAlert): string | null {
|
function alertHref(alert: DashboardAlert): string | null {
|
||||||
if (alert.relatedType === "Colony") return `/colonies/${alert.relatedId}`;
|
if (alert.relatedType === "Colony") return `/colonies/${alert.relatedId}`;
|
||||||
@@ -23,26 +26,43 @@ function alertHref(alert: DashboardAlert): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALERT_STYLES: Record<string, string> = {
|
function greeting(): string {
|
||||||
OVERDUE_TASK: "border-destructive/30 bg-destructive/5",
|
const hour = new Date().getHours();
|
||||||
CONSPICUOUS_COLONY: "border-warning/40 bg-warning/10",
|
if (hour < 11) return "Guten Morgen";
|
||||||
LOW_STOCK: "border-warning/40 bg-warning/10",
|
if (hour < 18) return "Guten Tag";
|
||||||
};
|
return "Guten Abend";
|
||||||
|
}
|
||||||
|
|
||||||
interface StatTile {
|
interface StatTile {
|
||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
tone: "primary" | "secondary" | "warning";
|
gradient: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TONE_CLASSES: Record<StatTile["tone"], string> = {
|
const ALERT_META: Record<
|
||||||
primary: "bg-primary/10 text-primary",
|
DashboardAlertType,
|
||||||
secondary: "bg-secondary/10 text-secondary",
|
{ label: string; icon: LucideIcon; className: string }
|
||||||
warning: "bg-warning/15 text-warning-foreground",
|
> = {
|
||||||
|
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() {
|
export default function DashboardPage() {
|
||||||
|
const { user } = useAuth();
|
||||||
const { data: summary, isLoading: summaryLoading } = useQuery({
|
const { data: summary, isLoading: summaryLoading } = useQuery({
|
||||||
queryKey: ["dashboard", "summary"],
|
queryKey: ["dashboard", "summary"],
|
||||||
queryFn: fetchDashboardSummary,
|
queryFn: fetchDashboardSummary,
|
||||||
@@ -54,25 +74,62 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
const tiles: StatTile[] = summary
|
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 Bienenstände",
|
||||||
{ label: "Aktive Königinnen", value: summary.queensActive, icon: Crown, tone: "secondary" },
|
value: summary.apiariesActive,
|
||||||
{ label: "Offene Aufgaben", value: summary.openTasks, icon: ListTodo, tone: "secondary" },
|
icon: MapPin,
|
||||||
{ label: "Honigchargen", value: summary.honeyBatchesTotal, icon: Droplet, tone: "primary" },
|
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",
|
label: "Artikel unter Mindestbestand",
|
||||||
value: summary.stockItemsLowCount,
|
value: summary.stockItemsLowCount,
|
||||||
icon: AlertTriangle,
|
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 (
|
return (
|
||||||
<main className="flex flex-col gap-6 p-4 sm:p-6">
|
<main className="flex flex-col gap-6 p-4 sm:p-6">
|
||||||
<div>
|
<div
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Dashboard</h1>
|
className="relative overflow-hidden rounded-2xl px-5 py-6 text-white shadow-[var(--shadow-elevated)] sm:px-8 sm:py-8"
|
||||||
<p className="text-sm text-muted-foreground">Überblick über deine Imkerei</p>
|
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>
|
</div>
|
||||||
|
|
||||||
{summaryLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
{summaryLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||||
@@ -80,12 +137,13 @@ export default function DashboardPage() {
|
|||||||
{tiles.map((tile) => {
|
{tiles.map((tile) => {
|
||||||
const Icon = tile.icon;
|
const Icon = tile.icon;
|
||||||
return (
|
return (
|
||||||
<Card key={tile.label}>
|
<Card key={tile.label} interactive>
|
||||||
<CardContent className="flex flex-col gap-3">
|
<CardContent className="flex flex-col gap-3">
|
||||||
<div
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-2xl font-semibold tabular-nums">{tile.value}</p>
|
<p className="text-2xl font-semibold tabular-nums">{tile.value}</p>
|
||||||
@@ -98,7 +156,7 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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">
|
<div className="flex flex-col gap-2">
|
||||||
{alertsLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
{alertsLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||||
{!alertsLoading && (!alerts || alerts.length === 0) ? (
|
{!alertsLoading && (!alerts || alerts.length === 0) ? (
|
||||||
@@ -110,20 +168,23 @@ export default function DashboardPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
{alerts?.map((alert) => {
|
{alerts?.map((alert) => {
|
||||||
const href = alertHref(alert);
|
const href = alertHref(alert);
|
||||||
|
const meta = ALERT_META[alert.type];
|
||||||
|
const AlertIcon = meta?.icon ?? AlertTriangle;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${alert.type}-${alert.relatedId}`}
|
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>
|
<AlertIcon className="size-4.5 shrink-0 text-muted-foreground" />
|
||||||
<div className="flex items-center gap-2">
|
<span className="flex-1">{alert.message}</span>
|
||||||
<Badge variant="secondary">{alert.type}</Badge>
|
{href ? (
|
||||||
{href ? (
|
<Link
|
||||||
<Link className="font-medium text-primary underline-offset-4 hover:underline" href={href}>
|
className="shrink-0 font-medium text-primary underline-offset-4 hover:underline"
|
||||||
Ansehen
|
href={href}
|
||||||
</Link>
|
>
|
||||||
) : null}
|
Ansehen
|
||||||
</div>
|
</Link>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -30,12 +30,17 @@ export default function AppLayout({ children }: { children: ReactNode }) {
|
|||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
<Sidebar user={user} />
|
<Sidebar user={user} />
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<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
|
<Link
|
||||||
href="/dashboard"
|
href="/dashboard"
|
||||||
className="flex items-center gap-1.5 font-heading font-semibold sm:hidden"
|
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
|
ApiCore
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex shrink-0 items-center gap-2 text-sm">
|
<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}
|
href={item.href}
|
||||||
aria-current={isActive ? "page" : undefined}
|
aria-current={isActive ? "page" : undefined}
|
||||||
className={cn(
|
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
|
isActive
|
||||||
? "bg-primary/10 text-primary"
|
? "bg-primary text-primary-foreground shadow-[var(--shadow-soft)]"
|
||||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
: "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>
|
<span className="truncate">{item.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -110,19 +115,24 @@ export function Sidebar({ user }: { user: AuthUser }) {
|
|||||||
const groups = user.role === "ADMIN" ? [...NAV_GROUPS, ADMIN_GROUP] : NAV_GROUPS;
|
const groups = user.role === "ADMIN" ? [...NAV_GROUPS, ADMIN_GROUP] : NAV_GROUPS;
|
||||||
|
|
||||||
return (
|
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
|
<Link
|
||||||
href="/dashboard"
|
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
|
ApiCore
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex flex-1 flex-col gap-4">
|
<nav className="flex flex-1 flex-col gap-5">
|
||||||
{groups.map((group, index) => (
|
{groups.map((group, index) => (
|
||||||
<div key={group.label ?? `group-${index}`} className="flex flex-col gap-0.5">
|
<div key={group.label ?? `group-${index}`} className="flex flex-col gap-0.5">
|
||||||
{group.label ? (
|
{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}
|
{group.label}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useAuth } from "@/lib/auth/auth-context";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { HoneycombPattern } from "@/components/honeycomb-pattern";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -15,33 +16,6 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} 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() {
|
export default function LoginPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
@@ -66,11 +40,16 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background p-4">
|
<main className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background p-4">
|
||||||
<HoneycombBackdrop />
|
<HoneycombPattern />
|
||||||
<Card className="relative w-full max-w-sm">
|
<Card className="relative w-full max-w-sm shadow-[var(--shadow-elevated)]">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="mb-1 flex items-center gap-2">
|
<div className="mb-1 flex items-center gap-2.5">
|
||||||
<Hexagon className="size-6 fill-primary/15 text-primary" strokeWidth={2.25} />
|
<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>
|
<CardTitle className="text-lg">ApiCore</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>Digitale Imkereiverwaltung für Wabenhain</CardDescription>
|
<CardDescription>Digitale Imkereiverwaltung für Wabenhain</CardDescription>
|
||||||
|
|||||||
@@ -65,24 +65,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(0.98 0.012 85);
|
--background: oklch(0.955 0.017 80);
|
||||||
--foreground: oklch(0.24 0.025 55);
|
--foreground: oklch(0.22 0.028 55);
|
||||||
--card: oklch(0.995 0.006 85);
|
--card: oklch(0.995 0.004 90);
|
||||||
--card-foreground: oklch(0.24 0.025 55);
|
--card-foreground: oklch(0.22 0.028 55);
|
||||||
--popover: oklch(0.995 0.006 85);
|
--popover: oklch(0.995 0.004 90);
|
||||||
--popover-foreground: oklch(0.24 0.025 55);
|
--popover-foreground: oklch(0.22 0.028 55);
|
||||||
--primary: oklch(0.6 0.135 50);
|
--primary: oklch(0.6 0.15 48);
|
||||||
--primary-foreground: oklch(0.98 0.02 85);
|
--primary-foreground: oklch(0.99 0.02 85);
|
||||||
--secondary: oklch(0.46 0.06 145);
|
--secondary: oklch(0.44 0.07 145);
|
||||||
--secondary-foreground: oklch(0.98 0.015 100);
|
--secondary-foreground: oklch(0.98 0.015 100);
|
||||||
--muted: oklch(0.93 0.016 85);
|
--muted: oklch(0.9 0.02 82);
|
||||||
--muted-foreground: oklch(0.5 0.03 65);
|
--muted-foreground: oklch(0.48 0.032 65);
|
||||||
--accent: oklch(0.94 0.028 75);
|
--accent: oklch(0.93 0.032 74);
|
||||||
--accent-foreground: oklch(0.28 0.04 50);
|
--accent-foreground: oklch(0.26 0.045 50);
|
||||||
--destructive: oklch(0.5 0.16 30);
|
--destructive: oklch(0.5 0.16 30);
|
||||||
--border: oklch(0.88 0.02 78);
|
--border: oklch(0.85 0.024 78);
|
||||||
--input: oklch(0.88 0.02 78);
|
--input: oklch(0.85 0.024 78);
|
||||||
--ring: oklch(0.6 0.135 50);
|
--ring: oklch(0.6 0.15 48);
|
||||||
--warning: oklch(0.68 0.14 75);
|
--warning: oklch(0.68 0.14 75);
|
||||||
--warning-foreground: oklch(0.24 0.04 60);
|
--warning-foreground: oklch(0.24 0.04 60);
|
||||||
--success: oklch(0.46 0.06 145);
|
--success: oklch(0.46 0.06 145);
|
||||||
@@ -93,14 +93,19 @@
|
|||||||
--chart-4: oklch(0.5 0.16 30);
|
--chart-4: oklch(0.5 0.16 30);
|
||||||
--chart-5: oklch(0.55 0.05 250);
|
--chart-5: oklch(0.55 0.05 250);
|
||||||
--radius: 0.75rem;
|
--radius: 0.75rem;
|
||||||
--sidebar: oklch(0.98 0.012 85);
|
--sidebar: oklch(0.995 0.004 90);
|
||||||
--sidebar-foreground: oklch(0.24 0.025 55);
|
--sidebar-foreground: oklch(0.22 0.028 55);
|
||||||
--sidebar-primary: oklch(0.6 0.135 50);
|
--sidebar-primary: oklch(0.6 0.15 48);
|
||||||
--sidebar-primary-foreground: oklch(0.98 0.02 85);
|
--sidebar-primary-foreground: oklch(0.99 0.02 85);
|
||||||
--sidebar-accent: oklch(0.94 0.028 75);
|
--sidebar-accent: oklch(0.93 0.032 74);
|
||||||
--sidebar-accent-foreground: oklch(0.28 0.04 50);
|
--sidebar-accent-foreground: oklch(0.26 0.045 50);
|
||||||
--sidebar-border: oklch(0.88 0.02 78);
|
--sidebar-border: oklch(0.85 0.024 78);
|
||||||
--sidebar-ring: oklch(0.6 0.135 50);
|
--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 {
|
.dark {
|
||||||
@@ -139,6 +144,11 @@
|
|||||||
--sidebar-accent-foreground: oklch(0.94 0.02 80);
|
--sidebar-accent-foreground: oklch(0.94 0.02 80);
|
||||||
--sidebar-border: oklch(1 0 0 / 12%);
|
--sidebar-border: oklch(1 0 0 / 12%);
|
||||||
--sidebar-ring: oklch(0.72 0.13 55);
|
--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 {
|
@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({
|
function Card({
|
||||||
className,
|
className,
|
||||||
size = "default",
|
size = "default",
|
||||||
|
interactive = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
}: React.ComponentProps<"div"> & { size?: "default" | "sm"; interactive?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Reference in new issue
Block a user