redesign: modern dark dashboard WOW effect - gradients, animations, new layout

This commit is contained in:
Cosmo
2026-04-16 08:06:13 +00:00
parent cd126135ef
commit 7aa02290d9
13 changed files with 296 additions and 315 deletions

View File

@@ -67,16 +67,16 @@ export default function BookmarksPage() {
]; ];
return ( return (
<div className="space-y-6 max-w-7xl mx-auto"> <div className="space-y-5 max-w-7xl mx-auto">
<div className="pt-2 pb-2"> <div className="pt-1 pb-1">
<h1 className="text-3xl font-bold text-white">Bookmarks</h1> <h1 className="text-2xl font-bold text-white">Bookmarks</h1>
<p className="text-slate-500 text-sm mt-1">Все ссылки в одном месте</p> <p className="text-slate-500 text-sm mt-0.5">Все ссылки в одном месте</p>
</div> </div>
{categories.map((cat) => ( {categories.map((cat) => (
<div key={cat.label}> <div key={cat.label}>
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<h3 className={`text-xs font-bold uppercase tracking-widest ${cat.accent}`}> <h3 className={`text-xs font-semibold uppercase tracking-widest ${cat.accent}`}>
{cat.emoji} {cat.label} {cat.emoji} {cat.label}
</h3> </h3>
<div className="flex-1 h-px bg-white/5" /> <div className="flex-1 h-px bg-white/5" />
@@ -88,11 +88,11 @@ export default function BookmarksPage() {
href={link.url} href={link.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="card p-4 hover:-translate-y-1 transition-all duration-200 group" className="card group p-4 hover:border-white/15"
> >
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="text-xl">{link.emoji}</span> <span className="text-xl">{link.emoji}</span>
<div className="text-sm font-medium text-white truncate group-hover:text-indigo-300 transition-colors"> <div className="text-sm font-medium text-slate-200 group-hover:gradient-text transition-all truncate">
{link.name} {link.name}
</div> </div>
</div> </div>

View File

@@ -1,22 +1,13 @@
import { auth } from "@/auth";
import { redirect } from "next/navigation";
import { Sidebar } from "@/components/layout/Sidebar"; import { Sidebar } from "@/components/layout/Sidebar";
export default async function DashboardLayout({ export default function DashboardLayout({ children }: { children: React.ReactNode }) {
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
if (!session) {
redirect("/auth/signin");
}
return ( return (
<div className="flex min-h-screen" style={{ background: "#0a0a0f" }}> <div className="flex h-screen bg-[#080810] overflow-hidden">
<Sidebar userName={session.user?.name} /> <Sidebar />
<main className="flex-1 ml-64 p-6 min-h-screen" style={{ background: "#0a0a0f" }}> <main className="flex-1 overflow-y-auto">
{children} <div className="p-6 max-w-[1600px] mx-auto">
{children}
</div>
</main> </main>
</div> </div>
); );

View File

@@ -1,34 +1,35 @@
export const dynamic = "force-dynamic";
import { WeatherWidget } from "@/components/widgets/WeatherWidget"; import { WeatherWidget } from "@/components/widgets/WeatherWidget";
import { TasksWidget } from "@/components/widgets/TasksWidget";
import { CalendarWidget } from "@/components/widgets/CalendarWidget"; import { CalendarWidget } from "@/components/widgets/CalendarWidget";
import { TasksWidget } from "@/components/widgets/TasksWidget";
import { ClaudeUsageWidget } from "@/components/widgets/ClaudeUsageWidget"; import { ClaudeUsageWidget } from "@/components/widgets/ClaudeUsageWidget";
import { ClaudeApiWidget } from "@/components/widgets/ClaudeApiWidget"; import { ClaudeApiWidget } from "@/components/widgets/ClaudeApiWidget";
import { ServicesGrid } from "@/components/widgets/ServicesGrid";
import { DashboardHeader } from "@/components/widgets/DashboardHeader"; import { DashboardHeader } from "@/components/widgets/DashboardHeader";
export default function DashboardPage() { export default function DashboardPage() {
return ( return (
<div className="space-y-6 max-w-7xl mx-auto"> <div className="space-y-5">
<DashboardHeader /> <DashboardHeader />
{/* Weather - full width */} {/* Weather - full width */}
<WeatherWidget /> <WeatherWidget />
{/* Calendar + Tasks */} {/* Calendar + Tasks + Claude */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<CalendarWidget /> <CalendarWidget />
</div> </div>
<div className="lg:col-span-1"> <div className="space-y-5">
<TasksWidget /> <TasksWidget />
<div className="grid grid-cols-2 gap-5 lg:grid-cols-1">
<ClaudeUsageWidget />
<ClaudeApiWidget />
</div>
</div> </div>
</div> </div>
{/* Claude row */} {/* Services */}
<div className="grid grid-cols-2 gap-5"> <ServicesGrid />
<ClaudeUsageWidget />
<ClaudeApiWidget />
</div>
</div> </div>
); );
} }

View File

@@ -6,7 +6,7 @@ const TABS = ["Openclaw", "Сервисы"] as const;
type TabName = typeof TABS[number]; type TabName = typeof TABS[number];
const MACHINE_MAP: Record<TabName, string> = { const MACHINE_MAP: Record<TabName, string> = {
"Openclaw": "openclaw", "Openclaw": "ocplatform",
"Сервисы": "services", "Сервисы": "services",
}; };
@@ -29,13 +29,20 @@ function CircularGauge({ value, size = 96, color, label }: { value: number; size
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<div className="relative" style={{ width: size, height: size }}> <div className="relative" style={{ width: size, height: size }}>
<svg width={size} height={size} className="-rotate-90"> <svg width={size} height={size} className="-rotate-90">
<defs>
<linearGradient id={`gauge-gradient-${label}`} x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor={color} stopOpacity="0.9" />
<stop offset="100%" stopColor={color} stopOpacity="0.4" />
</linearGradient>
</defs>
<circle cx={size/2} cy={size/2} r={radius} fill="none" stroke="rgba(255,255,255,0.05)" strokeWidth="10" /> <circle cx={size/2} cy={size/2} r={radius} fill="none" stroke="rgba(255,255,255,0.05)" strokeWidth="10" />
<circle <circle
cx={size/2} cy={size/2} r={radius} cx={size/2} cy={size/2} r={radius}
fill="none" stroke={color} fill="none"
stroke={`url(#gauge-gradient-${label})`}
strokeWidth="10" strokeLinecap="round" strokeWidth="10" strokeLinecap="round"
strokeDasharray={circumference} strokeDashoffset={offset} strokeDasharray={circumference} strokeDashoffset={offset}
style={{ transition: "stroke-dashoffset 0.8s ease", filter: `drop-shadow(0 0 6px ${color}60)` }} style={{ transition: "stroke-dashoffset 0.8s ease", filter: `drop-shadow(0 0 8px ${color}60)` }}
/> />
</svg> </svg>
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
@@ -54,7 +61,7 @@ function UsageBar({ label, value, color, detail }: { label: string; value: numbe
<span className="text-slate-400 font-medium">{label}</span> <span className="text-slate-400 font-medium">{label}</span>
<span className="text-white font-semibold">{detail || `${value}%`}</span> <span className="text-white font-semibold">{detail || `${value}%`}</span>
</div> </div>
<div className="h-1.5 rounded-full overflow-hidden" style={{ background: "rgba(255,255,255,0.05)" }}> <div className="h-1.5 rounded-full overflow-hidden bg-white/5">
<div <div
className="h-full rounded-full transition-all duration-700" className="h-full rounded-full transition-all duration-700"
style={{ width: `${value}%`, background: color, boxShadow: `0 0 8px ${color}80` }} style={{ width: `${value}%`, background: color, boxShadow: `0 0 8px ${color}80` }}
@@ -102,19 +109,19 @@ export default function SystemPage() {
}; };
return ( return (
<div className="space-y-6 max-w-7xl mx-auto"> <div className="space-y-5 max-w-7xl mx-auto">
<div className="flex items-center justify-between pt-2"> {/* Header */}
<div className="flex items-center justify-between pt-1">
<div> <div>
<h1 className="text-3xl font-bold text-white">System Monitor</h1> <h1 className="text-2xl font-bold text-white">System Monitor</h1>
<p className="text-slate-500 text-sm mt-1"> <p className="text-slate-500 text-sm mt-0.5">
{lastUpdated ? `Обновлено: ${lastUpdated.toLocaleTimeString("ru-RU")}` : "Загрузка..."} {lastUpdated ? `Обновлено: ${lastUpdated.toLocaleTimeString("ru-RU")}` : "Загрузка..."}
</p> </p>
</div> </div>
<button <button
onClick={fetchMetrics} onClick={fetchMetrics}
disabled={loading} disabled={loading}
className="flex items-center gap-2 px-4 py-2 rounded-xl border border-white/10 text-slate-300 text-sm transition-all hover:border-white/20 hover:bg-white/5 disabled:opacity-50" className="flex items-center gap-2 px-4 py-2 rounded-xl border border-white/10 text-slate-300 text-sm transition-all hover:border-white/20 hover:bg-white/5 disabled:opacity-50 bg-white/3"
style={{ background: "rgba(255,255,255,0.03)" }}
> >
<RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} /> <RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} />
Обновить Обновить
@@ -122,14 +129,14 @@ export default function SystemPage() {
</div> </div>
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 p-1 rounded-xl w-fit border border-white/5" style={{ background: "rgba(255,255,255,0.03)" }}> <div className="flex gap-1 p-1 rounded-2xl w-fit bg-white/5">
{TABS.map((tab) => ( {TABS.map((tab) => (
<button <button
key={tab} key={tab}
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
className={`px-5 py-2 rounded-lg text-sm font-medium transition-all ${ className={`px-5 py-2 rounded-xl text-sm font-medium transition-all ${
activeTab === tab activeTab === tab
? "text-white" ? "text-white shadow-lg shadow-indigo-500/20"
: "text-slate-500 hover:text-slate-300" : "text-slate-500 hover:text-slate-300"
}`} }`}
style={activeTab === tab ? { background: "linear-gradient(135deg, #6366f1, #8b5cf6)" } : {}} style={activeTab === tab ? { background: "linear-gradient(135deg, #6366f1, #8b5cf6)" } : {}}
@@ -150,9 +157,11 @@ export default function SystemPage() {
) : ( ) : (
<> <>
{/* Circular gauges */} {/* Circular gauges */}
<div className="card card-accent-violet p-6"> <div className="card card-violet p-6">
<div className="flex items-center gap-2 mb-6"> <div className="flex items-center gap-2 mb-6">
<Activity className="w-4 h-4 text-violet-400" /> <div className="w-7 h-7 rounded-lg bg-violet-500/15 flex items-center justify-center">
<Activity className="w-3.5 h-3.5 text-violet-400" />
</div>
<span className="text-sm font-semibold text-white">Загрузка системы</span> <span className="text-sm font-semibold text-white">Загрузка системы</span>
</div> </div>
{loading ? ( {loading ? (
@@ -171,10 +180,10 @@ export default function SystemPage() {
{/* Stats grid */} {/* Stats grid */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{[ {[
{ icon: Cpu, label: "CPU", value: loading ? "..." : `${metrics?.cpu ?? "—"}%`, sub: loading ? "" : `Load: ${metrics?.load1?.toFixed(2) ?? "—"} · ${metrics?.cpuCount ?? "?"} cores`, color: "text-indigo-400", accent: "card-accent-blue" }, { icon: Cpu, label: "CPU", value: loading ? "..." : `${metrics?.cpu ?? "—"}%`, sub: loading ? "" : `Load: ${metrics?.load1?.toFixed(2) ?? "—"} · ${metrics?.cpuCount ?? "?"} cores`, color: "text-indigo-400", accent: "card-blue" },
{ icon: MemoryStick, label: "RAM", value: loading ? "..." : `${metrics?.ram?.used ?? "—"} GB`, sub: loading ? "" : `из ${metrics?.ram?.total ?? "?"} GB`, color: "text-violet-400", accent: "card-accent-violet" }, { icon: MemoryStick, label: "RAM", value: loading ? "..." : `${metrics?.ram?.used ?? "—"} GB`, sub: loading ? "" : `из ${metrics?.ram?.total ?? "?"} GB`, color: "text-violet-400", accent: "card-violet" },
{ icon: HardDrive, label: "Disk", value: loading ? "..." : `${metrics?.disk?.used ?? "—"} GB`, sub: loading ? "" : `из ${metrics?.disk?.total ?? "?"} GB`, color: "text-emerald-400", accent: "card-accent-emerald" }, { icon: HardDrive, label: "Disk", value: loading ? "..." : `${metrics?.disk?.used ?? "—"} GB`, sub: loading ? "" : `из ${metrics?.disk?.total ?? "?"} GB`, color: "text-emerald-400", accent: "card-emerald" },
{ icon: Clock, label: "Uptime", value: loading ? "..." : (metrics?.uptime ?? "—"), sub: "Время работы", color: "text-amber-400", accent: "card-accent-amber" }, { icon: Clock, label: "Uptime", value: loading ? "..." : (metrics?.uptime ?? "—"), sub: "Время работы", color: "text-amber-400", accent: "card-amber" },
].map((item) => ( ].map((item) => (
<div key={item.label} className={`card ${item.accent} p-5`}> <div key={item.label} className={`card ${item.accent} p-5`}>
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
@@ -203,9 +212,11 @@ export default function SystemPage() {
{/* Network */} {/* Network */}
{!loading && metrics?.network && ( {!loading && metrics?.network && (
<div className="card card-accent-cyan p-5"> <div className="card card-cyan p-5">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Wifi className="w-4 h-4 text-cyan-400" /> <div className="w-7 h-7 rounded-lg bg-cyan-500/15 flex items-center justify-center">
<Wifi className="w-3.5 h-3.5 text-cyan-400" />
</div>
<span className="text-sm font-semibold text-white">Сеть (всего)</span> <span className="text-sm font-semibold text-white">Сеть (всего)</span>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">

View File

@@ -1,4 +1,4 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@@ -24,20 +24,15 @@
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--ring: 240 6% 12%; --ring: 240 6% 12%;
--radius: 0.75rem; --radius: 0.75rem;
--bg-base: #0a0a0f;
--bg-surface: #111118;
--bg-elevated: #1a1a24;
} }
} }
@layer base { @layer base {
* { * { @apply border-border; }
@apply border-border;
}
body { body {
background-color: var(--bg-base); background-color: #080810;
font-family: 'Inter', system-ui, sans-serif; font-family: 'Inter', system-ui, sans-serif;
color: #f8fafc; color: #f1f5f9;
min-height: 100vh; min-height: 100vh;
} }
} }
@@ -45,77 +40,79 @@
/* Scrollbar */ /* Scrollbar */
::-webkit-scrollbar { width: 4px; height: 4px; } ::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; } ::-webkit-scrollbar-thumb { background: rgba(99,102,241,0.3); border-radius: 2px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); } ::-webkit-scrollbar-thumb:hover { background: rgba(99,102,241,0.5); }
/* Card base style */ /* Card base */
.card { .card {
background: rgba(255,255,255,0.03); background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.07);
border-radius: 16px; border-radius: 20px;
backdrop-filter: blur(10px); transition: all 0.25s ease;
transition: all 0.2s ease;
} }
.card:hover { .card:hover {
background: rgba(255,255,255,0.05); background: rgba(255,255,255,0.05);
border-color: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.12);
box-shadow: 0 8px 32px rgba(0,0,0,0.4); transform: translateY(-1px);
box-shadow: 0 20px 60px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05);
} }
/* Legacy compat */ /* Legacy compat */
.glass-card { .glass-card {
background: rgba(255,255,255,0.03); background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.07);
border-radius: 16px; border-radius: 20px;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
transition: all 0.2s ease; transition: all 0.25s ease;
} }
.glass-card:hover { .glass-card:hover {
background: rgba(255,255,255,0.05); background: rgba(255,255,255,0.05);
border-color: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.12);
box-shadow: 0 8px 32px rgba(0,0,0,0.4); box-shadow: 0 20px 60px rgba(0,0,0,0.5);
} }
.card-accent-blue { border-top: 2px solid #3b82f6; } /* Accent borders — short names */
.card-accent-violet { border-top: 2px solid #8b5cf6; } .card-blue { border-top: 2px solid #3b82f6; }
.card-violet { border-top: 2px solid #8b5cf6; }
.card-emerald { border-top: 2px solid #10b981; }
.card-amber { border-top: 2px solid #f59e0b; }
.card-cyan { border-top: 2px solid #06b6d4; }
.card-rose { border-top: 2px solid #f43f5e; }
/* Legacy accent names */
.card-accent-blue { border-top: 2px solid #3b82f6; }
.card-accent-violet { border-top: 2px solid #8b5cf6; }
.card-accent-emerald { border-top: 2px solid #10b981; } .card-accent-emerald { border-top: 2px solid #10b981; }
.card-accent-amber { border-top: 2px solid #f59e0b; } .card-accent-amber { border-top: 2px solid #f59e0b; }
.card-accent-cyan { border-top: 2px solid #06b6d4; } .card-accent-cyan { border-top: 2px solid #06b6d4; }
.card-accent-rose { border-top: 2px solid #f43f5e; } .card-accent-rose { border-top: 2px solid #f43f5e; }
/* Gradient text */ /* Gradient text */
.gradient-text { .gradient-text {
background: linear-gradient(135deg, #6366f1, #a855f7); background: linear-gradient(135deg, #818cf8, #c084fc);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
/* Status indicators */
.status-online { background: #10b981; box-shadow: 0 0 6px rgba(16,185,129,0.5); }
.status-offline { background: #ef4444; box-shadow: 0 0 6px rgba(239,68,68,0.5); }
.status-checking { background: #6b7280; }
/* Pulsing status */
@keyframes ping-slow {
0%, 100% { opacity: 0.8; transform: scale(1); }
50% { opacity: 0.3; transform: scale(1.5); }
}
.ping-slow { animation: ping-slow 2s ease-in-out infinite; }
/* Glow effects */ /* Glow effects */
.glow-violet { box-shadow: 0 0 30px rgba(139,92,246,0.2); } .glow-violet { box-shadow: 0 0 40px rgba(139,92,246,0.15); }
.glow-blue { box-shadow: 0 0 30px rgba(59,130,246,0.2); } .glow-blue { box-shadow: 0 0 40px rgba(59,130,246,0.15); }
/* Sidebar */ /* Sidebar */
.sidebar { .sidebar {
background: #0d0d14; background: #0a0a14;
border-right: 1px solid rgba(255,255,255,0.05); border-right: 1px solid rgba(255,255,255,0.05);
} }
/* Status indicators */
.status-online {
background: #10b981;
box-shadow: 0 0 6px rgba(16,185,129,0.5);
}
.status-offline {
background: #ef4444;
box-shadow: 0 0 6px rgba(239,68,68,0.5);
}
.status-checking {
background: #6b7280;
}
@keyframes status-ring {
0%, 100% { opacity: 0.7; transform: scale(1); }
50% { opacity: 0; transform: scale(2.5); }
}

View File

@@ -1,84 +1,73 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { signOut } from "next-auth/react"; import { signOut, useSession } from "next-auth/react";
import { LayoutDashboard, Server, Bookmark, LogOut } from "lucide-react"; import { LayoutDashboard, Monitor, Bookmark, LogOut, Home } from "lucide-react";
import { cn } from "@/lib/utils";
const navItems = [ const NAV = [
{ href: "/", label: "Dashboard", icon: LayoutDashboard }, { href: "/", icon: LayoutDashboard, label: "Dashboard" },
{ href: "/system", label: "System", icon: Server }, { href: "/system", icon: Monitor, label: "System" },
{ href: "/bookmarks", label: "Bookmarks", icon: Bookmark }, { href: "/bookmarks", icon: Bookmark, label: "Bookmarks" },
]; ];
export function Sidebar({ userName }: { userName?: string | null }) { export function Sidebar() {
const pathname = usePathname(); const pathname = usePathname();
const { data: session } = useSession();
return ( return (
<aside className="sidebar w-64 min-h-screen flex flex-col fixed left-0 top-0 z-50"> <aside className="w-[220px] flex-shrink-0 flex flex-col h-screen bg-[#0a0a14] border-r border-white/5">
{/* Logo */} {/* Logo */}
<div className="p-5 border-b border-white/5"> <div className="px-5 py-6 border-b border-white/5">
<Link href="/" className="flex items-center gap-3 group"> <div className="flex items-center gap-3">
<div <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-indigo-500 to-violet-600 flex items-center justify-center shadow-lg shadow-indigo-500/20">
className="w-9 h-9 rounded-xl flex items-center justify-center text-sm font-bold text-white flex-shrink-0" <Home className="w-4 h-4 text-white" />
style={{ background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)" }}
>
DH
</div> </div>
<div> <div>
<div className="text-sm font-bold text-white">Digital Home</div> <div className="text-sm font-bold text-white">Digital Home</div>
<div className="text-[10px] text-slate-500 uppercase tracking-wider">Dashboard</div> <div className="text-[10px] text-slate-500">Dashboard</div>
</div> </div>
</Link> </div>
</div> </div>
{/* Navigation */} {/* Nav */}
<nav className="flex-1 p-3 space-y-0.5 mt-2"> <nav className="flex-1 px-3 py-4 space-y-1">
<div className="px-3 mb-2"> {NAV.map(({ href, icon: Icon, label }) => {
<span className="text-[10px] font-semibold text-slate-600 uppercase tracking-widest">Navigation</span> const active = href === "/" ? pathname === "/" : pathname.startsWith(href);
</div>
{navItems.map((item) => {
const Icon = item.icon;
const isActive = item.href === "/" ? pathname === "/" : pathname.startsWith(item.href);
return ( return (
<Link <Link
key={item.href} key={href}
href={item.href} href={href}
className={cn( className={`flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all ${
"flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all", active
isActive ? "bg-indigo-500/15 text-indigo-300 border border-indigo-500/20"
? "bg-gradient-to-r from-indigo-500/20 to-violet-500/10 border-l-2 border-indigo-500 text-white pl-[10px]" : "text-slate-500 hover:text-slate-300 hover:bg-white/5 border border-transparent"
: "text-slate-500 hover:text-slate-300 hover:bg-white/5" }`}
)}
> >
<Icon className={cn("w-4 h-4 shrink-0", isActive ? "text-indigo-400" : "")} /> <Icon className={`w-4 h-4 ${active ? "text-indigo-400" : ""}`} />
{item.label} {label}
</Link> </Link>
); );
})} })}
</nav> </nav>
{/* User section */} {/* User */}
<div className="p-3 border-t border-white/5"> <div className="px-3 py-4 border-t border-white/5">
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl mb-1"> <div className="flex items-center gap-3 px-3 py-2 rounded-xl" style={{ background: "rgba(255,255,255,0.03)" }}>
<div <div className="w-8 h-8 rounded-full bg-gradient-to-br from-indigo-500 to-violet-600 flex items-center justify-center text-xs font-bold text-white flex-shrink-0">
className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold text-white flex-shrink-0" D
style={{ background: "linear-gradient(135deg, #6366f1, #8b5cf6)" }}
>
{userName?.[0]?.toUpperCase() ?? "D"}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-sm font-medium text-white truncate">{userName ?? "Daniil"}</div> <div className="text-xs font-medium text-slate-300 truncate">{session?.user?.name ?? "Daniil"}</div>
<div className="text-[10px] text-slate-500">Administrator</div> <div className="text-[10px] text-slate-600 truncate">Admin</div>
</div> </div>
<button
onClick={() => signOut({ callbackUrl: "/auth/signin" })}
className="text-slate-600 hover:text-red-400 transition-colors"
title="Выйти"
>
<LogOut className="w-3.5 h-3.5" />
</button>
</div> </div>
<button
onClick={() => signOut({ callbackUrl: "/auth/signin" })}
className="w-full flex items-center gap-2 px-3 py-2 rounded-xl text-sm text-slate-500 hover:text-red-400 hover:bg-red-500/10 transition-all"
>
<LogOut className="w-4 h-4" />
Выйти
</button>
</div> </div>
</aside> </aside>
); );

View File

@@ -11,16 +11,15 @@ export function ClaudeApiWidget() {
}; };
return ( return (
<div className="card card-accent-violet p-5"> <div className="card card-violet p-5">
<div className="flex items-center justify-between mb-5"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-xl flex items-center justify-center text-sm font-bold text-white" <div className="w-7 h-7 rounded-lg bg-violet-500/15 flex items-center justify-center">
style={{ background: "linear-gradient(135deg, #8b5cf6, #6366f1)" }}> <span className="text-sm">🔮</span>
A
</div> </div>
<div> <div>
<div className="text-sm font-semibold text-white">Claude API</div> <div className="text-sm font-semibold text-white">Claude API</div>
<div className="text-[10px] text-slate-500">Usage & Cost</div> <div className="text-[10px] text-slate-500">Cost & Usage</div>
</div> </div>
</div> </div>
<button onClick={handleRefresh} className="text-slate-600 hover:text-slate-300 transition-colors p-1 rounded-lg hover:bg-white/5"> <button onClick={handleRefresh} className="text-slate-600 hover:text-slate-300 transition-colors p-1 rounded-lg hover:bg-white/5">
@@ -28,22 +27,22 @@ export function ClaudeApiWidget() {
</button> </button>
</div> </div>
<div className="grid grid-cols-3 gap-3"> <div className="space-y-3">
{[ {[
{ label: "Стоимость", value: "—", color: "text-violet-400" }, { label: "Стоимость", value: "—", color: "text-violet-400" },
{ label: "Запросы", value: "—", color: "text-white" }, { label: "Запросы", value: "—", color: "text-white" },
{ label: "Токены", value: "—", color: "text-white" }, { label: "Токены", value: "—", color: "text-white" },
].map((item) => ( ].map((item) => (
<div key={item.label} className="rounded-xl p-3 text-center border border-white/5" style={{ background: "rgba(255,255,255,0.03)" }}> <div key={item.label} className="flex justify-between items-center">
<div className={`text-xl font-bold ${item.color}`}>{item.value}</div> <span className="text-xs text-slate-500">{item.label}</span>
<div className="text-[10px] text-slate-500 mt-1">{item.label}</div> <span className={`text-xs font-medium ${item.color}`}>{item.value}</span>
</div> </div>
))} ))}
</div> </div>
<div className="mt-3 flex items-center gap-2 text-xs text-slate-600"> <div className="mt-4 flex items-center gap-2 text-xs text-slate-600">
<div className="w-1.5 h-1.5 rounded-full bg-violet-500/50" /> <div className="w-1.5 h-1.5 rounded-full bg-violet-500/50" />
<span>Автоматическое получение данных недоступно</span> <span>Автополучение недоступно</span>
</div> </div>
</div> </div>
); );

View File

@@ -11,15 +11,14 @@ export function ClaudeUsageWidget() {
}; };
return ( return (
<div className="card card-accent-amber p-5"> <div className="card card-amber p-5">
<div className="flex items-center justify-between mb-5"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-xl flex items-center justify-center text-sm font-bold text-white" <div className="w-7 h-7 rounded-lg bg-amber-500/15 flex items-center justify-center">
style={{ background: "linear-gradient(135deg, #f59e0b, #ef4444)" }}> <span className="text-sm"></span>
C
</div> </div>
<div> <div>
<div className="text-sm font-semibold text-white">Claude Subscription</div> <div className="text-sm font-semibold text-white">Claude Sub</div>
<div className="text-[10px] text-slate-500">Usage</div> <div className="text-[10px] text-slate-500">Usage</div>
</div> </div>
</div> </div>
@@ -28,22 +27,22 @@ export function ClaudeUsageWidget() {
</button> </button>
</div> </div>
<div className="grid grid-cols-3 gap-3"> <div className="space-y-3">
{[ {[
{ label: "Использовано", value: "—", color: "text-amber-400" }, { label: "Использовано", value: "—", color: "text-amber-400" },
{ label: "Лимит", value: "—", color: "text-white" }, { label: "Лимит", value: "—", color: "text-white" },
{ label: "Сброс", value: "—", color: "text-white" }, { label: "Сброс", value: "—", color: "text-white" },
].map((item) => ( ].map((item) => (
<div key={item.label} className="rounded-xl p-3 text-center border border-white/5" style={{ background: "rgba(255,255,255,0.03)" }}> <div key={item.label} className="flex justify-between items-center">
<div className={`text-xl font-bold ${item.color}`}>{item.value}</div> <span className="text-xs text-slate-500">{item.label}</span>
<div className="text-[10px] text-slate-500 mt-1">{item.label}</div> <span className={`text-xs font-medium ${item.color}`}>{item.value}</span>
</div> </div>
))} ))}
</div> </div>
<div className="mt-3 flex items-center gap-2 text-xs text-slate-600"> <div className="mt-4 flex items-center gap-2 text-xs text-slate-600">
<div className="w-1.5 h-1.5 rounded-full bg-amber-500/50" /> <div className="w-1.5 h-1.5 rounded-full bg-amber-500/50" />
<span>Автоматическое получение данных недоступно</span> <span>Автополучение недоступно</span>
</div> </div>
</div> </div>
); );

View File

@@ -1,48 +1,35 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
const MONTHS = ["января","февраля","марта","апреля","мая","июня","июля","августа","сентября","октября","ноября","декабря"];
const DAYS = ["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"];
export function DashboardHeader() { export function DashboardHeader() {
const [time, setTime] = useState(new Date()); const [now, setNow] = useState(new Date());
useEffect(() => { useEffect(() => {
const i = setInterval(() => setTime(new Date()), 1000); const i = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(i); return () => clearInterval(i);
}, []); }, []);
const hour = time.getHours(); const hour = now.getHours();
const greeting = const greeting = hour < 6 ? "Доброй ночи" : hour < 12 ? "Доброе утро" : hour < 17 ? "Добрый день" : "Добрый вечер";
hour < 6 ? "Доброй ночи" :
hour < 12 ? "Доброе утро" :
hour < 17 ? "Добрый день" :
hour < 22 ? "Добрый вечер" :
"Доброй ночи";
return ( return (
<div className="flex items-center justify-between pt-2 pb-2"> <div className="flex items-start justify-between pt-1">
<div> <div>
<h1 className="text-3xl font-bold text-white"> <h1 className="text-2xl font-bold text-white">
{greeting},{" "} {greeting}, <span className="gradient-text">Daniil</span> 👋
<span className="gradient-text">Daniil</span>{" "}
<span>👋</span>
</h1> </h1>
<p className="text-slate-500 mt-1 text-sm"> <p className="text-slate-500 text-sm mt-0.5">
{time.toLocaleDateString("ru-RU", { {DAYS[now.getDay()]}, {now.getDate()} {MONTHS[now.getMonth()]} {now.getFullYear()}
weekday: "long",
day: "numeric",
month: "long",
year: "numeric",
})}
{" · "}
{time.toLocaleTimeString("ru-RU", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})}
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="text-right">
<div className="px-3 py-1.5 rounded-full text-xs font-medium text-indigo-300 border border-indigo-500/20 bg-indigo-500/10"> <div className="text-2xl font-light text-white tabular-nums">
digital-home.site {now.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" })}
</div>
<div className="text-xs text-slate-600">
{now.toLocaleTimeString("ru-RU", { second: "2-digit" })} сек
</div> </div>
</div> </div>
</div> </div>

View File

@@ -85,36 +85,44 @@ function ServiceCard({ service }: { service: Service }) {
href={service.url} href={service.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="card p-4 flex items-center gap-3 hover:-translate-y-1 transition-all duration-200 group" className="card group flex items-center gap-3 p-4 hover:border-white/15"
> >
<div className="w-10 h-10 rounded-xl border border-white/5 flex items-center justify-center shrink-0 text-xl" {/* Icon */}
style={{ background: "rgba(255,255,255,0.05)" }}> <div className="w-10 h-10 rounded-xl bg-white/5 flex items-center justify-center text-xl flex-shrink-0 group-hover:bg-white/10 transition-colors">
{service.icon ? ( {service.icon ? (
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
<img src={service.icon} alt={service.name} className="w-6 h-6 object-contain" onError={(e) => { <img
(e.target as HTMLImageElement).style.display = "none"; src={service.icon}
const parent = (e.target as HTMLImageElement).parentElement; alt={service.name}
if (parent) parent.textContent = service.emoji ?? "🔗"; className="w-6 h-6 rounded"
}} /> onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
const parent = (e.target as HTMLImageElement).parentElement;
if (parent) parent.textContent = service.emoji ?? "🔗";
}}
/>
) : ( ) : (
<span>{service.emoji ?? "🔗"}</span> <span>{service.emoji ?? "🔗"}</span>
)} )}
</div> </div>
{/* Info */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="text-sm font-medium text-white truncate">{service.name}</span> <span className="text-sm font-medium text-slate-200 group-hover:text-white transition-colors truncate">
{service.name}
</span>
<ExternalLink className="w-3 h-3 text-slate-700 shrink-0 group-hover:text-slate-400 transition-colors" /> <ExternalLink className="w-3 h-3 text-slate-700 shrink-0 group-hover:text-slate-400 transition-colors" />
</div> </div>
<p className="text-xs text-slate-600 truncate mt-0.5">{service.desc}</p> <p className="text-xs text-slate-600 truncate mt-0.5">{service.desc}</p>
</div> </div>
<div className="shrink-0 relative"> {/* Status */}
<div className="relative flex-shrink-0">
{status === "online" && ( {status === "online" && (
<span className="absolute inset-0 rounded-full bg-emerald-400 opacity-40 animate-ping" /> <span className="absolute inset-0 rounded-full bg-emerald-400 ping-slow" />
)} )}
<span className={`relative block w-2 h-2 rounded-full ${ <span className={`relative block w-2 h-2 rounded-full ${
status === "online" ? "bg-emerald-500" : status === "online" ? "bg-emerald-400" :
status === "offline" ? "bg-red-500" : status === "offline" ? "bg-red-400" : "bg-slate-600 animate-pulse"
"bg-slate-600 animate-pulse"
}`} /> }`} />
</div> </div>
</a> </a>
@@ -123,15 +131,17 @@ function ServiceCard({ service }: { service: Service }) {
export function ServicesGrid() { export function ServicesGrid() {
return ( return (
<div className="space-y-6"> <div className="space-y-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3 mb-4">
<h2 className="text-lg font-bold text-white">Сервисы</h2> <h2 className="text-lg font-bold text-white">Сервисы</h2>
<div className="flex-1 h-px bg-white/5" /> <div className="flex-1 h-px bg-white/5" />
</div> </div>
{SERVICE_CATEGORIES.map((cat) => ( {SERVICE_CATEGORIES.map((cat) => (
<div key={cat.label}> <div key={cat.label}>
<h3 className={`text-xs font-bold uppercase tracking-widest mb-3 ${cat.accent}`}>{cat.label}</h3> <h3 className={`text-xs font-semibold uppercase tracking-widest mb-3 mt-6 first:mt-0 ${cat.accent}`}>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3"> {cat.label}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{cat.services.map((svc) => ( {cat.services.map((svc) => (
<ServiceCard key={svc.url} service={svc} /> <ServiceCard key={svc.url} service={svc} />
))} ))}

View File

@@ -36,10 +36,12 @@ export function TasksWidget() {
}; };
return ( return (
<div className="card card-accent-emerald p-5"> <div className="card card-emerald p-5">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CheckSquare className="w-4 h-4 text-emerald-400" /> <div className="w-7 h-7 rounded-lg bg-emerald-500/15 flex items-center justify-center">
<CheckSquare className="w-3.5 h-3.5 text-emerald-400" />
</div>
<span className="text-sm font-semibold text-white">Задачи</span> <span className="text-sm font-semibold text-white">Задачи</span>
</div> </div>
<button onClick={fetchTasks} className="text-slate-600 hover:text-slate-300 transition-colors p-1 rounded-lg hover:bg-white/5"> <button onClick={fetchTasks} className="text-slate-600 hover:text-slate-300 transition-colors p-1 rounded-lg hover:bg-white/5">
@@ -59,7 +61,8 @@ export function TasksWidget() {
) : ( ) : (
<div className="space-y-1.5 max-h-[320px] overflow-y-auto"> <div className="space-y-1.5 max-h-[320px] overflow-y-auto">
{tasks.map((task) => ( {tasks.map((task) => (
<div key={task.id} className="flex items-start gap-2.5 p-2.5 rounded-xl hover:bg-white/3 transition-colors group"> <div key={task.id} className="flex items-start gap-2.5 p-2.5 rounded-xl bg-white/3 hover:bg-white/5 transition-colors group">
<div className="w-0.5 self-stretch rounded-full bg-gradient-to-b from-emerald-400 to-teal-500 shrink-0" />
{task.done ? ( {task.done ? (
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0 mt-0.5" /> <CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0 mt-0.5" />
) : ( ) : (

View File

@@ -21,7 +21,7 @@ export function WeatherWidget() {
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [selected, setSelected] = useState(0); const [selected, setSelected] = useState(0);
const fetchData = async () => { const fetch_ = async () => {
setLoading(true); setError(false); setLoading(true); setError(false);
try { try {
const res = await fetch("/api/weather"); const res = await fetch("/api/weather");
@@ -31,38 +31,16 @@ export function WeatherWidget() {
finally { setLoading(false); } finally { setLoading(false); }
}; };
useEffect(() => { fetchData(); }, []); useEffect(() => { fetch_(); }, []);
const c = data?.current;
const day = data?.forecast?.[selected];
const formatDay = (dateStr: string, idx: number) => {
if (idx === 0) return "Сег";
const d = new Date(dateStr + "T12:00:00");
return DAY_NAMES[d.getDay()];
};
return ( return (
<div className="card card-accent-blue relative overflow-hidden"> <div className="card card-blue relative overflow-hidden">
{/* Decorative background */} <div className="absolute inset-0 bg-gradient-to-br from-blue-950/40 via-transparent to-indigo-950/30 pointer-events-none" />
<div className="absolute inset-0 bg-gradient-to-br from-blue-900/20 via-transparent to-violet-900/10 pointer-events-none" /> <div className="absolute top-0 right-0 w-64 h-64 bg-blue-500/5 rounded-full -translate-y-1/2 translate-x-1/2 pointer-events-none" />
<div className="relative p-6"> <div className="relative p-6">
{/* Header */} {loading ? (
<div className="flex items-center justify-between mb-5"> <div className="animate-pulse space-y-4">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-blue-400 uppercase tracking-wider">Погода</span>
<span className="text-xs text-slate-500">· Санкт-Петербург</span>
</div>
<button onClick={fetchData} className="text-slate-600 hover:text-slate-300 transition-colors p-1 rounded-lg hover:bg-white/5">
<RefreshCw className={`w-3.5 h-3.5 ${loading ? "animate-spin" : ""}`} />
</button>
</div>
{error ? (
<div className="text-slate-500 text-sm text-center py-8">Нет данных о погоде</div>
) : loading ? (
<div className="space-y-4 animate-pulse">
<div className="flex items-end gap-4"> <div className="flex items-end gap-4">
<div className="h-20 w-20 bg-white/5 rounded-2xl" /> <div className="h-20 w-20 bg-white/5 rounded-2xl" />
<div className="space-y-2"> <div className="space-y-2">
@@ -70,62 +48,78 @@ export function WeatherWidget() {
<div className="h-4 w-40 bg-white/5 rounded" /> <div className="h-4 w-40 bg-white/5 rounded" />
</div> </div>
</div> </div>
<div className="flex gap-2">{[...Array(7)].map((_,i) => <div key={i} className="flex-1 h-24 bg-white/5 rounded-xl" />)}</div> <div className="flex gap-2">{[...Array(7)].map((_,i) => <div key={i} className="flex-1 h-24 bg-white/5 rounded-2xl" />)}</div>
</div> </div>
) : c && day ? ( ) : error ? (
<> <div className="text-slate-500 text-sm py-8 text-center">Нет данных о погоде</div>
{/* Current weather */} ) : data && (
<div className="flex items-start justify-between mb-6"> <div className="space-y-5">
{/* Current */}
<div className="flex items-start justify-between">
<div> <div>
<div className="flex items-baseline gap-3 mb-1"> <p className="text-slate-400 text-xs font-medium uppercase tracking-wider mb-2">Санкт-Петербург</p>
<span className="text-7xl font-extralight text-white leading-none"> <div className="flex items-start gap-3">
{selected === 0 ? c.temp : day.maxTemp}° <span className="text-6xl font-extralight text-white leading-none">
{selected === 0 ? data.current.temp : data.forecast[selected].maxTemp}°
</span> </span>
<span className="text-4xl">{selected === 0 ? c.icon : day.icon}</span> <div className="mt-1">
<span className="text-3xl">{selected === 0 ? data.current.icon : data.forecast[selected].icon}</span>
</div>
</div> </div>
<p className="text-slate-300 mt-2">{selected === 0 ? c.desc : day.desc}</p> <p className="text-slate-300 text-sm mt-2">{selected === 0 ? data.current.desc : data.forecast[selected].desc}</p>
{selected === 0 && ( {selected !== 0 && (
<p className="text-slate-500 text-xs mt-0.5">Ощущается как {c.feelsLike}°</p> <p className="text-slate-500 text-xs mt-1">мин {data.forecast[selected].minTemp}°</p>
)} )}
</div> </div>
{selected === 0 && ( {selected === 0 && (
<div className="text-right space-y-2 mt-2"> <div className="flex flex-col gap-2 text-xs text-slate-400">
<div className="flex items-center gap-2 justify-end text-sm"> <div className="flex items-center gap-1.5 bg-white/5 rounded-lg px-2.5 py-1.5">
<span className="text-slate-500 text-xs">Влажность</span> <span>🌡</span>
<span className="text-blue-400 font-medium">💧 {c.humidity}%</span> <span>Ощущ. {data.current.feelsLike}°</span>
</div> </div>
<div className="flex items-center gap-2 justify-end text-sm"> <div className="flex items-center gap-1.5 bg-white/5 rounded-lg px-2.5 py-1.5">
<span className="text-slate-500 text-xs">Ветер</span> <span>💧</span>
<span className="text-cyan-400 font-medium">💨 {c.windKmh} км/ч</span> <span>{data.current.humidity}%</span>
</div> </div>
<div className="flex items-center gap-1.5 bg-white/5 rounded-lg px-2.5 py-1.5">
<span>💨</span>
<span>{data.current.windKmh} км/ч</span>
</div>
<button onClick={fetch_} className="flex items-center gap-1.5 bg-white/5 hover:bg-white/10 rounded-lg px-2.5 py-1.5 transition-colors">
<RefreshCw className={`w-3 h-3 ${loading ? "animate-spin" : ""}`} />
<span>Обновить</span>
</button>
</div> </div>
)} )}
</div> </div>
{/* 7-day forecast */} {/* 7-day forecast */}
<div className="grid grid-cols-7 gap-1.5"> <div className="grid grid-cols-7 gap-1.5">
{data!.forecast.map((f, i) => ( {data.forecast.map((day: DayForecast, i: number) => {
<button const d = new Date(day.date + "T12:00:00");
key={f.date} return (
onClick={() => setSelected(i)} <button
className={`flex flex-col items-center gap-1.5 p-2 rounded-xl transition-all ${ key={day.date}
selected === i onClick={() => setSelected(i)}
? "bg-white/10 ring-1 ring-white/20" className={`flex flex-col items-center gap-1.5 py-3 px-1 rounded-2xl transition-all ${
: "hover:bg-white/5" selected === i
}`} ? "bg-white/10 ring-1 ring-white/20 shadow-lg"
> : "hover:bg-white/5"
<span className="text-[10px] font-medium text-slate-400">{formatDay(f.date, i)}</span> }`}
<span className="text-xl">{f.icon}</span> >
<span className="text-xs font-semibold text-white">{f.maxTemp}°</span> <span className="text-[10px] font-medium text-slate-400">{i === 0 ? "Сег" : DAY_NAMES[d.getDay()]}</span>
<span className="text-[10px] text-slate-600">{f.minTemp}°</span> <span className="text-xl">{day.icon}</span>
{f.precipProb > 20 && ( <span className="text-xs font-semibold text-white">{day.maxTemp}°</span>
<span className="text-[9px] text-blue-400 font-medium">{f.precipProb}%</span> <span className="text-[10px] text-slate-600">{day.minTemp}°</span>
)} {day.precipProb > 20 && (
</button> <span className="text-[9px] text-blue-400 font-medium">{day.precipProb}%</span>
))} )}
</button>
);
})}
</div> </div>
</> </div>
) : null} )}
</div> </div>
</div> </div>
); );

View File

@@ -36,10 +36,10 @@ const config: Config = {
foreground: "hsl(var(--card-foreground))", foreground: "hsl(var(--card-foreground))",
}, },
dash: { dash: {
bg: "#0a0a0f", bg: "#080810",
surface: "#111118", surface: "#0f0f1a",
elevated: "#1a1a24", elevated: "#161624",
border: "rgba(255,255,255,0.06)", border: "rgba(255,255,255,0.07)",
}, },
}, },
backgroundImage: { backgroundImage: {
@@ -65,6 +65,10 @@ const config: Config = {
from: { height: "var(--radix-accordion-content-height)" }, from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" }, to: { height: "0" },
}, },
"ping-slow": {
"0%, 100%": { opacity: "0.8", transform: "scale(1)" },
"50%": { opacity: "0.3", transform: "scale(1.5)" },
},
glow: { glow: {
"0%": { boxShadow: "0 0 5px rgba(99,102,241,0.3)" }, "0%": { boxShadow: "0 0 5px rgba(99,102,241,0.3)" },
"100%": { boxShadow: "0 0 20px rgba(99,102,241,0.6)" }, "100%": { boxShadow: "0 0 20px rgba(99,102,241,0.6)" },
@@ -73,18 +77,14 @@ const config: Config = {
"0%, 100%": { transform: "translateY(0px)" }, "0%, 100%": { transform: "translateY(0px)" },
"50%": { transform: "translateY(-6px)" }, "50%": { transform: "translateY(-6px)" },
}, },
"status-pulse": {
"0%, 100%": { opacity: "1", transform: "scale(1)" },
"50%": { opacity: "0.4", transform: "scale(1.5)" },
},
}, },
animation: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", "accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out",
"ping-slow": "ping-slow 2s ease-in-out infinite",
"pulse-slow": "pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite", "pulse-slow": "pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite",
glow: "glow 2s ease-in-out infinite alternate", glow: "glow 2s ease-in-out infinite alternate",
float: "float 6s ease-in-out infinite", float: "float 6s ease-in-out infinite",
"status-pulse": "status-pulse 2s ease-in-out infinite",
}, },
}, },
}, },