mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-09-13 18:59:25 +00:00
✨ feat: add dark/light theme toggle
This commit is contained in:
@@ -6,6 +6,7 @@ import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { BrandLockup } from "@/components/brand";
|
||||
import { FullScreenLoader } from "@/components/page-layout";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -161,6 +162,7 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
<span className="truncate font-mono text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground">
|
||||
{currentPage ? `${currentPage.context} / ${currentPage.label}` : "Control plane"}
|
||||
</span>
|
||||
<ThemeToggle className="ml-auto" />
|
||||
</header>
|
||||
<main className="min-w-0 flex-1 overflow-auto p-4 sm:p-6 lg:p-8">{children}</main>
|
||||
</SidebarInset>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import type * as React from "react";
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { Monitor, Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
type ThemeToggleProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ThemeToggle({ className }: ThemeToggleProps) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
className={cn("relative", className)}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
<Sun className="size-4 scale-100 rotate-0 transition-transform dark:scale-0 dark:-rotate-90" />
|
||||
<Moon className="absolute size-4 scale-0 rotate-90 transition-transform dark:scale-100 dark:rotate-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-36">
|
||||
<DropdownMenuRadioGroup value={mounted ? theme : undefined} onValueChange={setTheme}>
|
||||
<DropdownMenuRadioItem value="light">
|
||||
<Sun />
|
||||
Light
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="dark">
|
||||
<Moon />
|
||||
Dark
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="system">
|
||||
<Monitor />
|
||||
System
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Background, BackgroundVariant, Controls, MiniMap, Panel, ReactFlow } from "@xyflow/react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { useGraphLayout } from "@/hooks/use-graph-layout";
|
||||
@@ -15,6 +16,7 @@ interface TopologyCanvasProps {
|
||||
}
|
||||
|
||||
export function TopologyCanvas({ graph }: TopologyCanvasProps) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const [selectedNode, setSelectedNode] = useState<TopologyNodeData | null>(null);
|
||||
const [filters, setFilters] = useState<TopologyFilters>({
|
||||
showServers: true,
|
||||
@@ -68,24 +70,25 @@ export function TopologyCanvas({ graph }: TopologyCanvasProps) {
|
||||
}}
|
||||
minZoom={0.1}
|
||||
maxZoom={1.5}
|
||||
colorMode={resolvedTheme === "dark" ? "dark" : "light"}
|
||||
defaultEdgeOptions={{
|
||||
animated: false,
|
||||
type: "smoothstep",
|
||||
style: {
|
||||
stroke: "#6f7565",
|
||||
stroke: "var(--muted-foreground)",
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: "5 5",
|
||||
},
|
||||
markerEnd: {
|
||||
type: "arrowclosed",
|
||||
color: "#6f7565",
|
||||
color: "var(--muted-foreground)",
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
}}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Lines} color="#d2d0c5" gap={32} size={1} />
|
||||
<Background variant={BackgroundVariant.Lines} color="var(--border)" gap={32} size={1} />
|
||||
<Controls
|
||||
showZoom
|
||||
showFitView
|
||||
@@ -103,7 +106,7 @@ export function TopologyCanvas({ graph }: TopologyCanvasProps) {
|
||||
};
|
||||
return colors[data.status] || "#94a3b8";
|
||||
}}
|
||||
maskColor="rgba(29, 31, 26, 0.08)"
|
||||
maskColor="color-mix(in oklch, var(--foreground) 8%, transparent)"
|
||||
className="border bg-card/95 shadow-md backdrop-blur-sm"
|
||||
/>
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export function HealthBadge({
|
||||
healthy: "bg-green-500",
|
||||
degraded: "bg-yellow-500",
|
||||
unhealthy: "bg-red-500",
|
||||
unknown: "bg-gray-400",
|
||||
unknown: "bg-muted-foreground",
|
||||
}[status];
|
||||
|
||||
return (
|
||||
@@ -100,10 +100,10 @@ export function TopologyNodeCard({
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border bg-card p-3 shadow-md hover:shadow-lg transition-all w-[300px]",
|
||||
selected && "ring-2 ring-primary ring-offset-2 shadow-xl"
|
||||
selected && "ring-2 ring-primary ring-offset-2 ring-offset-background shadow-xl"
|
||||
)}
|
||||
>
|
||||
<Handle type="target" position={Position.Top} className="w-3 h-3 !bg-gray-400" />
|
||||
<Handle type="target" position={Position.Top} className="h-3 w-3 !bg-muted-foreground" />
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
@@ -119,7 +119,7 @@ export function TopologyNodeCard({
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
<Handle type="source" position={Position.Bottom} className="w-3 h-3 !bg-gray-400" />
|
||||
<Handle type="source" position={Position.Bottom} className="h-3 w-3 !bg-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user