-

Resource Card

PreviousNext

A reusable card for agents, datasets, plugins, workspaces, and other ZGI resources.

Installation

pnpm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/resource-card.json
npm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/resource-card.json
yarn
yarn dlx shadcn@latest add https://ui.zgi.ai/r/resource-card.json

Component

components/ui/resource-card.tsx
"use client"

import * as React from "react"
import { MoreHorizontalIcon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export interface ResourceCardMetaItem {
  icon?: React.ReactNode
  label: React.ReactNode
}

export interface ResourceCardAction {
  icon?: React.ReactNode
  label: React.ReactNode
  onSelect?: () => void
}

export interface ResourceCardProps
  extends Omit<React.ComponentProps<typeof Card>, "title"> {
  icon?: React.ReactNode
  title: React.ReactNode
  description?: React.ReactNode
  status?: React.ReactNode
  meta?: ResourceCardMetaItem[]
  actions?: ResourceCardAction[]
  href?: string
  onOpen?: () => void
}

function ResourceCardInner({
  icon,
  title,
  description,
  status,
  meta = [],
  actions = [],
}: Pick<
  ResourceCardProps,
  "actions" | "description" | "icon" | "meta" | "status" | "title"
>) {
  return (
    <CardContent className="flex h-full min-h-40 flex-col gap-3 p-4">
      <div className="flex items-start gap-3">
        {icon ? (
          <div className="bg-muted text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-lg">
            {icon}
          </div>
        ) : null}
        <div className="min-w-0 flex-1">
          <div className="flex min-w-0 items-center gap-2">
            <h3 className="truncate text-sm font-medium">{title}</h3>
            {status ? <div className="shrink-0">{status}</div> : null}
          </div>
          {description ? (
            <p className="text-muted-foreground mt-1 line-clamp-2 text-xs leading-5">
              {description}
            </p>
          ) : null}
        </div>
        {actions.length ? (
          <DropdownMenu>
            <DropdownMenuTrigger asChild>
              <Button
                type="button"
                variant="ghost"
                size="icon"
                className="size-8 shrink-0 rounded-md"
                onClick={(event) => event.preventDefault()}
                aria-label="Open resource actions"
              >
                <MoreHorizontalIcon className="size-4" />
              </Button>
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end">
              {actions.map((action, index) => (
                <DropdownMenuItem
                  key={index}
                  onSelect={(event) => {
                    event.preventDefault()
                    action.onSelect?.()
                  }}
                >
                  {action.icon ? (
                    <span className="[&_svg]:size-4">{action.icon}</span>
                  ) : null}
                  {action.label}
                </DropdownMenuItem>
              ))}
            </DropdownMenuContent>
          </DropdownMenu>
        ) : null}
      </div>
      {meta.length ? (
        <div className="mt-auto flex flex-wrap gap-1.5">
          {meta.map((item, index) => (
            <Badge
              key={index}
              variant="secondary"
              className="h-6 gap-1 rounded-md px-2 font-normal"
            >
              {item.icon ? (
                <span className="[&_svg]:size-3.5">{item.icon}</span>
              ) : null}
              {item.label}
            </Badge>
          ))}
        </div>
      ) : null}
    </CardContent>
  )
}

export function ResourceCard({
  icon,
  title,
  description,
  status,
  meta,
  actions,
  href,
  onOpen,
  className,
  ...props
}: ResourceCardProps) {
  const content = (
    <ResourceCardInner
      icon={icon}
      title={title}
      description={description}
      status={status}
      meta={meta}
      actions={actions}
    />
  )

  return (
    <Card
      className={cn(
        "hover:border-primary/40 hover:bg-accent/20 relative h-full overflow-hidden rounded-lg transition-colors",
        (href || onOpen) && "cursor-pointer",
        className
      )}
      onClick={onOpen}
      {...props}
    >
      {href ? (
        <a href={href} className="block h-full">
          {content}
        </a>
      ) : (
        content
      )}
    </Card>
  )
}

Usage

import { BotIcon } from "lucide-react"
 
import { ResourceCard } from "@/components/ui/resource-card"
 
export function Example() {
  return (
    <ResourceCard
      icon={<BotIcon className="size-5" />}
      title="Support Router"
      description="Routes requests to the right workflow, tool, or human handoff."
      meta={[{ label: "Workflow agent" }, { label: "8 tools" }]}
    />
  )
}

Props

PropTypeDefault
titleReactNoderequired
iconReactNode-
descriptionReactNode-
statusReactNode-
metaResourceCardMetaItem[][]
actionsResourceCardAction[][]
hrefstring-
onOpen() => void-