Agent Card
AI agent card with a deterministic 5x5 identicon, badges and a stat row.
import { AgentCard } from "@/components/agent-card/agent-card";
export default function Demo() { return ( <AgentCard className="w-[380px] max-w-full" name="XSD" tag="AI" status="Autopilot" address="dG6r…4mSr" stats={[ { label: "Indexes", value: "3" }, { label: "AUM", value: "$12.4K" }, { label: "Best 7d", value: "+4.1%" }, ]} /> );}<script setup lang="ts">import { AgentCard } from "@/components/ui/agent-card";</script>
<template> <AgentCard class="w-[380px] max-w-full" name="XSD" tag="AI" status="Autopilot" address="dG6r…4mSr" :stats="[ { label: 'Indexes', value: '3' }, { label: 'AUM', value: '$12.4K' }, { label: 'Best 7d', value: '+4.1%' }, ]" /></template><script lang="ts"> import { AgentCard } from "$lib/components/ui/agent-card";</script>
<AgentCard class="w-[380px] max-w-full" name="XSD" tag="AI" status="Autopilot" address="dG6r…4mSr" stats={[ { label: "Indexes", value: "3" }, { label: "AUM", value: "$12.4K" }, { label: "Best 7d", value: "+4.1%" }, ]}/>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/agent-cardpnpm dlx shadcn@latest add @edmi-ui/agent-cardyarn dlx shadcn@latest add @edmi-ui/agent-cardbunx shadcn@latest add @edmi-ui/agent-cardPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
badge,card,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/agent-cardpnpm dlx shadcn-vue@latest add @edmi-ui/agent-cardyarn dlx shadcn-vue@latest add @edmi-ui/agent-cardbunx shadcn-vue@latest add @edmi-ui/agent-cardPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
badge,card,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/agent-card.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/agent-card.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/agent-card.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/agent-card.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
badge,card,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { AgentCard } from "@/components/agent-card/agent-card";
<AgentCard className="w-[380px] max-w-full" name="XSD" tag="AI" status="Autopilot" address="dG6r…4mSr" stats={[ { label: "Indexes", value: "3" }, { label: "AUM", value: "$12.4K" }, { label: "Best 7d", value: "+4.1%" }, ]}/><script setup lang="ts">import { AgentCard } from "@/components/ui/agent-card";</script>
<template> <AgentCard class="w-[380px] max-w-full" name="XSD" tag="AI" status="Autopilot" address="dG6r…4mSr" :stats="[ { label: 'Indexes', value: '3' }, { label: 'AUM', value: '$12.4K' }, { label: 'Best 7d', value: '+4.1%' }, ]" /></template><script lang="ts"> import { AgentCard } from "$lib/components/ui/agent-card";</script>
<AgentCard class="w-[380px] max-w-full" name="XSD" tag="AI" status="Autopilot" address="dG6r…4mSr" stats={[ { label: "Indexes", value: "3" }, { label: "AUM", value: "$12.4K" }, { label: "Best 7d", value: "+4.1%" }, ]}/>Component source
Section titled “Component source”import { cn } from "cn";import type * as React from "react";import { Badge } from "@/registry/edmi/ui/badge";import { Card } from "@/registry/edmi/ui/card";import type { Elevation } from "@/registry/edmi/ui/elevation";
// Four tones of the board identicon: columns repeat muted-2 / muted / foreground-2 / empty.const TONES = [ "bg-muted-foreground-2", "bg-muted-foreground", "bg-foreground-2", "bg-transparent",];
function hash(seed: string) { let h = 2166136261; for (let i = 0; i < seed.length; i++) { h ^= seed.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0;}
type AgentIdenticonProps = React.ComponentProps<"div"> & { /** Deterministic seed (agent name or address). */ seed: string; /** Edge length in px. */ size?: number;};
/** 5x5 token-colored identicon, deterministic per `seed`. */function AgentIdenticon({ seed, size = 44, className, style, ...props}: AgentIdenticonProps) { let h = hash(seed); const cells = Array.from({ length: 25 }, (_, i) => { if (i % 5 === 0) h = hash(`${seed}:${i}:${h}`); return TONES[(h >>> ((i % 5) * 2)) & 3]; }); return ( <div data-slot="agent-identicon" aria-hidden="true" className={cn( "grid shrink-0 grid-cols-5 overflow-hidden rounded-xl border border-border bg-muted", className, )} style={{ width: size, height: size, ...style }} {...props} > {cells.map((tone, i) => ( // biome-ignore lint/suspicious/noArrayIndexKey: static grid <span key={i} className={tone} /> ))} </div> );}
type AgentCardStat = { label: string; value: React.ReactNode };
type AgentCardProps = React.ComponentProps<typeof Card> & { name: string; /** Mono sub line, usually a shortened address. */ address?: string; /** Secondary badge next to the name (e.g. "AI"). */ tag?: string; /** Brand badge with a status dot (e.g. "Live"); omit to hide it. */ status?: React.ReactNode; stats?: AgentCardStat[]; /** Identicon seed; defaults to `name`. */ seed?: string; /** ✦ depth of the card (forwarded to Card). */ elevation?: Elevation; // ✦ `children` render in a footer under the stats (a note, an action row).};
function AgentCard({ className, name, address, tag, status, stats, seed, elevation, children, ...props}: AgentCardProps) { return ( <Card data-slot="agent-card" elevation={elevation} className={cn("gap-0 p-5", className)} {...props} > <div className="flex items-center gap-3.5"> <AgentIdenticon seed={seed ?? name} /> <div className="min-w-0"> <div className="flex flex-wrap items-center gap-2"> <span className="text-base font-semibold">{name}</span> {tag ? <Badge variant="secondary">{tag}</Badge> : null} {status ? ( <Badge variant="brand"> <span className="size-1.5 rounded-full bg-current" /> {status} </Badge> ) : null} </div> {address ? ( <div className="mt-1 font-mono text-xs text-muted-foreground"> {address} </div> ) : null} </div> </div> {stats?.length ? ( <div className="mt-4 flex justify-between border-t border-border pt-3.5"> {stats.map((s) => ( <div key={s.label}> <div className="text-xs text-muted-foreground">{s.label}</div> <div className="mt-1 font-mono text-[19px]">{s.value}</div> </div> ))} </div> ) : null} {children ? ( <div data-slot="agent-card-footer" className="mt-4 border-t border-border pt-3.5 text-[13px] text-muted-foreground" > {children} </div> ) : null} </Card> );}
export type { AgentCardStat };export { AgentCard, AgentIdenticon };<script setup lang="ts">import type { HTMLAttributes } from 'vue'import { Badge } from '@/registry/edmi/ui/badge'import { Card } from '@/registry/edmi/ui/card'import type { Elevation } from '@/registry/edmi/ui/elevation'import { cn } from '@/registry/edmi/lib/utils'import AgentIdenticon from './AgentIdenticon.vue'import type { AgentCardStat } from './types'
const props = defineProps<{ /** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2 */ elevation?: Elevation class?: HTMLAttributes['class'] name: string /** Mono sub line, usually a shortened address. */ address?: string /** Secondary badge next to the name (e.g. "AI"). */ tag?: string /** Brand badge with a status dot (e.g. "Live"; or use the `status` slot); omit to hide it. */ status?: string stats?: AgentCardStat[] /** Identicon seed; defaults to `name`. */ seed?: string}>()</script>
<template> <Card :elevation="elevation" data-slot="agent-card" :class="cn('gap-0 p-5', props.class)"> <div class="flex items-center gap-3.5"> <AgentIdenticon :seed="seed ?? name" /> <div class="min-w-0"> <div class="flex flex-wrap items-center gap-2"> <span class="text-base font-semibold">{{ name }}</span> <Badge v-if="tag" variant="secondary">{{ tag }}</Badge> <Badge v-if="status || $slots.status" variant="brand"> <span class="size-1.5 rounded-full bg-current" /> <slot name="status">{{ status }}</slot> </Badge> </div> <div v-if="address" class="mt-1 font-mono text-xs text-muted-foreground"> {{ address }} </div> </div> </div> <div v-if="stats?.length" class="mt-4 flex justify-between border-t border-border pt-3.5"> <div v-for="s in stats" :key="s.label"> <div class="text-xs text-muted-foreground">{{ s.label }}</div> <div class="mt-1 font-mono text-[19px]"> <slot name="stat-value" :stat="s">{{ s.value }}</slot> </div> </div> </div> <!-- ✦ default slot: footer under the stats (a note, an action row) --> <div v-if="$slots.default" data-slot="agent-card-footer" class="mt-4 border-t border-border pt-3.5 text-[13px] text-muted-foreground"> <slot /> </div> </Card></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { computed } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
// Four tones of the board identicon: columns repeat muted-2 / muted / foreground-2 / empty.const TONES = ['bg-muted-foreground-2', 'bg-muted-foreground', 'bg-foreground-2', 'bg-transparent']
function hash(seed: string) { let h = 2166136261 for (let i = 0; i < seed.length; i++) { h ^= seed.charCodeAt(i) h = Math.imul(h, 16777619) } return h >>> 0}
const props = withDefaults(defineProps<{ class?: HTMLAttributes['class'] /** Deterministic seed (agent name or address). */ seed: string /** Edge length in px. */ size?: number}>(), { size: 44 })
const cells = computed(() => { let h = hash(props.seed) return Array.from({ length: 25 }, (_, i) => { if (i % 5 === 0) h = hash(`${props.seed}:${i}:${h}`) return TONES[(h >>> ((i % 5) * 2)) & 3] })})</script>
<template> <!-- 5x5 token-colored identicon, deterministic per `seed`. --> <div data-slot="agent-identicon" aria-hidden="true" :class="cn('grid shrink-0 grid-cols-5 overflow-hidden rounded-xl border border-border bg-muted', props.class)" :style="{ width: `${size}px`, height: `${size}px` }" > <span v-for="(tone, i) in cells" :key="i" :class="tone" /> </div></template>export { default as AgentCard } from "./AgentCard.vue";export { default as AgentIdenticon } from "./AgentIdenticon.vue";export type { AgentCardStat } from "./types";export type AgentCardStat = { label: string; value: string | number };<script lang="ts" module> import type { Snippet } from "svelte";
export type AgentCardStat = { label: string; value: string | number | Snippet };</script>
<script lang="ts"> import { cn } from "$lib/utils.js"; import { Badge } from "$lib/registry/ui/badge/index.js"; import { Card } from "$lib/registry/ui/card/index.js"; import AgentIdenticon from "./agent-identicon.svelte"; import type { Elevation } from "$lib/registry/ui/elevation/index.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, name, address, tag, status, stats, seed, elevation = "auto", children, ...restProps }: Omit<ComponentProps<typeof Card>, "children"> & { /** ✦ Footer under the stats (a note, an action row). */ children?: Snippet; name: string; /** Mono sub line, usually a shortened address. */ address?: string; /** Secondary badge next to the name (e.g. "AI"). */ tag?: string; /** Brand badge with a status dot (e.g. "Live"); omit to hide it. */ status?: string | Snippet; stats?: AgentCardStat[]; /** Identicon seed; defaults to `name`. */ seed?: string; /** ✦ depth of the card (forwarded to Card). */ elevation?: Elevation; } = $props();</script>
<Card bind:ref data-slot="agent-card" {elevation} class={cn("gap-0 p-5", className)} {...restProps}> <div class="flex items-center gap-3.5"> <AgentIdenticon seed={seed ?? name} /> <div class="min-w-0"> <div class="flex flex-wrap items-center gap-2"> <span class="text-base font-semibold">{name}</span> {#if tag}<Badge variant="secondary">{tag}</Badge>{/if} {#if status} <Badge variant="brand"> <span class="size-1.5 rounded-full bg-current"></span> {#if typeof status === "function"}{@render status()}{:else}{status}{/if} </Badge> {/if} </div> {#if address} <div class="mt-1 font-mono text-xs text-muted-foreground">{address}</div> {/if} </div> </div> {#if stats?.length} <div class="mt-4 flex justify-between border-t border-border pt-3.5"> {#each stats as s (s.label)} <div> <div class="text-xs text-muted-foreground">{s.label}</div> <div class="mt-1 font-mono text-[19px]"> {#if typeof s.value === "function"}{@render s.value()}{:else}{s.value}{/if} </div> </div> {/each} </div> {/if} {#if children} <div data-slot="agent-card-footer" class="mt-4 border-t border-border pt-3.5 text-[13px] text-muted-foreground"> {@render children()} </div> {/if}</Card><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
// Four tones of the board identicon: columns repeat muted-2 / muted / foreground-2 / empty. const TONES = ["bg-muted-foreground-2", "bg-muted-foreground", "bg-foreground-2", "bg-transparent"];
function hash(seed: string) { let h = 2166136261; for (let i = 0; i < seed.length; i++) { h ^= seed.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; }
let { class: className, seed, size = 44, style, ...restProps }: HTMLAttributes<HTMLDivElement> & { /** Deterministic seed (agent name or address). */ seed: string; /** Edge length in px. */ size?: number; } = $props();
const cells = $derived.by(() => { let h = hash(seed); return Array.from({ length: 25 }, (_, i) => { if (i % 5 === 0) h = hash(`${seed}:${i}:${h}`); return TONES[(h >>> ((i % 5) * 2)) & 3]; }); });</script>
<!-- 5x5 token-colored identicon, deterministic per `seed`. --><div data-slot="agent-identicon" aria-hidden="true" class={cn("grid shrink-0 grid-cols-5 overflow-hidden rounded-xl border border-border bg-muted", className)} style="width: {size}px; height: {size}px;{style ? ` ${style}` : ''}" {...restProps}> {#each cells as tone, i (i)} <span class={tone}></span> {/each}</div>import Root, { type AgentCardStat } from "./agent-card.svelte";import Identicon from "./agent-identicon.svelte";
export { type AgentCardStat, Identicon, Identicon as AgentIdenticon, Root, // Root as AgentCard,};Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Flat by default. elevation is forwarded to the card: sunken (-1) a soft inset well, raised (+1) the bevel, floating (+2) the bevel plus one soft drop. In a layered ElevationProvider the card resolves to its role default (raised).
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
import { AgentCard } from "@/components/agent-card/agent-card";import type { Elevation } from "@/components/ui/elevation";
function Sample({ elevation }: { elevation: Elevation }) { return ( <AgentCard elevation={elevation} className="w-[380px] max-w-full" name="XSD" tag="AI" status="Autopilot" address="dG6r…4mSr" stats={[ { label: "Indexes", value: "3" }, { label: "AUM", value: "$12.4K" }, { label: "Best 7d", value: "+4.1%" }, ]} /> );}
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-6"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <Sample elevation={value} /> </div> ))} </div> );}<script setup lang="ts">import { AgentCard } from "@/components/ui/agent-card";
const levels = [ { value: "flat", label: "Flat (0)" } as const, { value: "raised", label: "Raised (+1)" } as const, { value: "floating", label: "Floating (+2)" } as const,];</script>
<template> <div class="flex flex-col gap-6"> <div v-for="l in levels" :key="l.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ l.label }}</p> <AgentCard :elevation="l.value" class="w-[380px] max-w-full" name="XSD" tag="AI" status="Autopilot" address="dG6r…4mSr" :stats="[ { label: 'Indexes', value: '3' }, { label: 'AUM', value: '$12.4K' }, { label: 'Best 7d', value: '+4.1%' }, ]" /> </div> </div></template><script lang="ts"> import { AgentCard } from "$lib/components/ui/agent-card";
const levels = [ { value: "flat", label: "Flat (0)" } as const, { value: "raised", label: "Raised (+1)" } as const, { value: "floating", label: "Floating (+2)" } as const, ];</script>
<div class="flex flex-col gap-6"> {#each levels as l (l.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{l.label}</p><AgentCard elevation={l.value} class="w-[380px] max-w-full" name="XSD" tag="AI" status="Autopilot" address="dG6r…4mSr" stats={[ { label: "Indexes", value: "3" }, { label: "AUM", value: "$12.4K" }, { label: "Best 7d", value: "+4.1%" }, ]}/> </div> {/each}</div>| Part | Prop | Type | Default |
|---|---|---|---|
AgentCard |
name |
string |
required |
address |
string (mono sub line) |
none | |
tag |
string (secondary badge) |
none | |
status |
brand badge with a dot, e.g. Live (React ReactNode, Vue status slot, Svelte string | Snippet) |
none | |
stats |
{ label, value }[] (value: React ReactNode, Vue stat-value scoped slot, Svelte string | number | Snippet) |
none | |
seed |
string identicon seed |
name |
|
elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
|
children ✦ |
React children, Vue default slot, Svelte children snippet: footer under the stats |
none | |
AgentIdenticon |
seed, size |
token-coloured 5x5 grid, deterministic per seed | size 44 |
AgentIdenticon is intentionally not merged with the AI pack’s Agent Avatar: both are seeded 5x5 identicons, but the identicon here is a full grid painted in neutral token tones (--muted-foreground-2, --muted-foreground, --foreground-2) on a muted tile for list cards, while Agent Avatar is a mirrored 3x5 mark tinted with a chart color. Each keeps its own look.
Footer ✦
Section titled “Footer ✦”Children render in a footer under the stats: a note, an action row.
import { AgentCard } from "@/components/agent-card/agent-card";
export default function Demo() { return ( <AgentCard className="w-[380px] max-w-full" name="XSD" tag="AI" status="Autopilot" address="dG6r…4mSr" stats={[ { label: "Indexes", value: "3" }, { label: "AUM", value: "$12.4K" }, { label: "Best 7d", value: "+4.1%" }, ]} > Rebalanced MAG4 weights 2h ago. </AgentCard> );}<script setup lang="ts">import { AgentCard } from "@/components/ui/agent-card";</script>
<template> <AgentCard class="w-[380px] max-w-full" name="XSD" tag="AI" status="Autopilot" address="dG6r…4mSr" :stats="[ { label: 'Indexes', value: '3' }, { label: 'AUM', value: '$12.4K' }, { label: 'Best 7d', value: '+4.1%' }, ]" > Rebalanced MAG4 weights 2h ago. </AgentCard></template><script lang="ts"> import { AgentCard } from "$lib/components/ui/agent-card";</script>
<AgentCard class="w-[380px] max-w-full" name="XSD" tag="AI" status="Autopilot" address="dG6r…4mSr" stats={[ { label: "Indexes", value: "3" }, { label: "AUM", value: "$12.4K" }, { label: "Best 7d", value: "+4.1%" }, ]}> Rebalanced MAG4 weights 2h ago.</AgentCard>✦ Edmi additions
Section titled “✦ Edmi additions”- Whole block is an Edmi addition (DESIGN §5 Patterns, board 12). Composed from: card, badge.
- Install:
shadcn add @edmi-ui/agent-card.