app cards with action menu and tooltips

This commit is contained in:
2026-06-27 00:00:13 -05:00
parent 91ef732eeb
commit 69f269efc7
10 changed files with 236 additions and 16 deletions
+83
View File
@@ -0,0 +1,83 @@
import { Terminal, Pencil, FileTerminal } from "lucide-react";
import { motion } from "motion/react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type Props = {
id: string;
};
export default function ActionMenu({ id }: Props) {
const buttonClass =
"flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition-all duration-150 hover:bg-green-50 hover:text-green-600 active:scale-95";
return (
<TooltipProvider delayDuration={150}>
<motion.div
initial={{ opacity: 0, y: 8, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 8, scale: 0.95 }}
transition={{
type: "spring",
stiffness: 450,
damping: 28,
}}
className="flex justify-end"
>
<div className="flex flex-row justify-end gap-1.5">
<Tooltip>
<TooltipTrigger asChild>
<button
className={buttonClass}
onClick={() => {
// SSH
}}
>
<Terminal className="h-5 w-5" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>SSH</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
className={buttonClass}
onClick={() => {
// Blog
}}
>
<Pencil className="h-5 w-5" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Create Blog</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
className={buttonClass}
onClick={() => {
// Scripts
}}
>
<FileTerminal className="h-5 w-5" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Run Scripts</p>
</TooltipContent>
</Tooltip>
</div>
</motion.div>
</TooltipProvider>
);
}
+87
View File
@@ -0,0 +1,87 @@
"use client";
import { motion, AnimatePresence } from "motion/react";
import { useState } from "react";
import ActionMenu from "./action-menu";
type App = {
id: string;
name: string;
description?: string;
address: string;
ssh_addr?: string;
};
interface Props {
app: App;
}
export default function AppCard({ app }: Props) {
let pingStatus = true;
const [hovered, setHovered] = useState(false);
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className={`
relative
rounded-2xl
bg-gray-100
p-3
${hovered && "border bg-white"}
transition-all
duration-200
`}
>
<div className="absolute top-4 right-4">
<div className="relative">
<div className="absolute inset-0 rounded-full bg-green-500 animate-ping opacity-30" />
<div className="relative h-2.5 w-2.5 rounded-full bg-green-500 ring-2 ring-white" />
</div>
</div>
<h3 className="text-2xl font-bold">{app.name}</h3>
<a
className={`${hovered && "text-blue-600"} transition-colors duration-200`}
target="_blank"
href={`http://${app.address}`}
>
{app.address}
</a>
{/*<AnimatePresence>
{hovered && (
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.2 }}
>
{app.description && <span>{app.description}</span>}
<ActionMenu id={app.id} />
</motion.div>
)}
</AnimatePresence>*/}
<AnimatePresence>
{hovered && (
<motion.div
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.15 }}
className={`absolute bottom-3 left-3 right-3 ${app.description && "bg-white"} pt-2 flex flex-col gap-1`}
>
{app.description && (
<span className="text-sm text-muted-foreground">
{app.description}
</span>
)}
<ActionMenu id={app.id} />
</motion.div>
)}
</AnimatePresence>
</div>
);
}
+17 -15
View File
@@ -1,10 +1,5 @@
"use client";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
SidebarGroup,
SidebarGroupLabel,
@@ -12,6 +7,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import Link from "next/link";
export function NavMain({
items,
@@ -25,18 +21,24 @@ export function NavMain({
}) {
return (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarGroupLabel>Aperture</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem
key={item.title}
className="hover:bg-back hover:border-0"
>
<SidebarMenuButton tooltip={item.title}>
{item.icon}
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
<Link href={item.url} key={item.title}>
<SidebarMenuItem>
<SidebarMenuButton
tooltip={item.title}
className={
item.isActive
? "bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-900 data-[active=true]:bg-emerald-500/20"
: ""
}
>
{item.icon}
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
</Link>
))}
</SidebarMenu>
</SidebarGroup>
+1 -1
View File
@@ -19,7 +19,7 @@ export default function Sidebar({ children }: Props) {
<SidebarTrigger className="-ml-1" />
</div>
</header>
<main className="flex-1 overflow-y-auto">{children}</main>
<main className="flex-1 overflow-y-auto ml-4">{children}</main>
</SidebarInset>
</SidebarProvider>
);