Leaderboard Podium
Top-three podium cards with rank badges and avatars; first place stands taller in the middle.
import { LeaderboardPodium } from "@/components/leaderboard-podium/leaderboard-podium";
export default function Demo() { return ( <LeaderboardPodium entries={[ { rank: 1, name: "dewi", meta: "$49.2K · 412 holders" }, { rank: 2, name: "noah", meta: "$31.7K · 265 holders" }, { rank: 3, name: "sarah", meta: "$18.9K · 140 holders" }, ]} /> );}<script setup lang="ts">import { LeaderboardPodium } from "@/components/ui/leaderboard-podium";
const entries = [ { rank: 1, name: "dewi", meta: "$49.2K · 412 holders" }, { rank: 2, name: "noah", meta: "$31.7K · 265 holders" }, { rank: 3, name: "sarah", meta: "$18.9K · 140 holders" },] as const;</script>
<template> <LeaderboardPodium :entries="[...entries]" /></template><script lang="ts"> import { LeaderboardPodium, type PodiumEntry } from "$lib/components/ui/leaderboard-podium";
const entries: PodiumEntry[] = [ { rank: 1, name: "dewi", meta: "$49.2K · 412 holders" }, { rank: 2, name: "noah", meta: "$31.7K · 265 holders" }, { rank: 3, name: "sarah", meta: "$18.9K · 140 holders" }, ];</script>
<LeaderboardPodium {entries} />Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/leaderboard-podiumpnpm dlx shadcn@latest add @edmi-ui/leaderboard-podiumyarn dlx shadcn@latest add @edmi-ui/leaderboard-podiumbunx shadcn@latest add @edmi-ui/leaderboard-podiumPick 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:
card,avatar,badge,allocation-bar,index-row,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/leaderboard-podiumpnpm dlx shadcn-vue@latest add @edmi-ui/leaderboard-podiumyarn dlx shadcn-vue@latest add @edmi-ui/leaderboard-podiumbunx shadcn-vue@latest add @edmi-ui/leaderboard-podiumPick 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:
card,avatar,badge,allocation-bar,index-row,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/leaderboard-podium.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/leaderboard-podium.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/leaderboard-podium.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/leaderboard-podium.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:
card,avatar,badge,allocation-bar,index-row,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { LeaderboardPodium,} from "@/components/leaderboard-podium/leaderboard-podium";
<LeaderboardPodium entries={[ { rank: 1, name: "dewi", meta: "$49.2K · 412 holders" }, { rank: 2, name: "noah", meta: "$31.7K · 265 holders" }, { rank: 3, name: "sarah", meta: "$18.9K · 140 holders" }, ]}/><script setup lang="ts">import { LeaderboardPodium } from "@/components/ui/leaderboard-podium";</script>
<template> <LeaderboardPodium :entries="[...entries]" /></template><script lang="ts"> import { LeaderboardPodium } from "$lib/components/ui/leaderboard-podium";</script>
<LeaderboardPodium />Component source
Section titled “Component source”import { cn } from "cn";import type * as React from "react";import { AllocationBar } from "@/registry/edmi/components/allocation-bar";import { Sparkline } from "@/registry/edmi/components/index-row";import { Avatar, AvatarFallback, AvatarImage } from "@/registry/edmi/ui/avatar";import { Badge } from "@/registry/edmi/ui/badge";import { Card } from "@/registry/edmi/ui/card";import type { Elevation } from "@/registry/edmi/ui/elevation";
type PodiumAllocation = { label: string; /** Weight in percent (bars are proportional). */ value: number; /** Any CSS color; defaults cycle `--chart-1…5`. */ color?: string;};
type PodiumEntry = { rank: 1 | 2 | 3; name: string; /** Mono caption under the name, e.g. `$49.2K · 412 members`. */ meta?: string; image?: string; /** Avatar initials; defaults to the first two letters of `name`. */ initials?: string; href?: string; /** ✦ `variant="cards"`: mono ticker under the name. */ symbol?: string; /** ✦ `variant="cards"`: mono owner, top right of the card. */ owner?: string; /** ✦ `variant="cards"`: signed headline percentage, e.g. `+1.12%`. */ change?: string; /** ✦ `variant="cards"`: series for the sparkline. */ spark?: number[]; /** ✦ `variant="cards"`: weights bar with legend. */ allocation?: PodiumAllocation[]; /** ✦ `variant="cards"`: footer stat, labelled by `valueLabel`. */ value?: string; /** ✦ `variant="cards"`: footer stat, labelled by `countLabel`. */ count?: string | number;};
const isDown = (s: string) => /^[-−–]/.test(s.trim());
// `podium` (default): top three, #1 raised in the middle (warning badge), #2 left and #3 right sit lower.// `cards` ✦: three equal stat cards in rank order (leaderboard board): owner, name, headline change,// sparkline, allocation bar and value / count stats.function LeaderboardPodium({ className, entries, elevation, variant = "podium", valueLabel = "Value", countLabel = "Count", ...props}: Omit<React.ComponentProps<"ol">, "children"> & { entries: PodiumEntry[]; /** ✦ depth of the cards (forwarded to each Card). */ elevation?: Elevation; /** ✦ `podium` (default) or `cards`. */ variant?: "podium" | "cards"; /** ✦ `cards`: label of the footer `value` stat. */ valueLabel?: React.ReactNode; /** ✦ `cards`: label of the footer `count` stat. */ countLabel?: React.ReactNode;}) { const by = (r: number) => entries.find((e) => e.rank === r); if (variant === "cards") { const sorted = [by(1), by(2), by(3)].filter((e): e is PodiumEntry => !!e); return ( <ol data-slot="leaderboard-podium" data-variant="cards" className={cn("grid gap-4 lg:grid-cols-3", className)} {...props} > {sorted.map((e) => { const down = e.change ? isDown(e.change) : false; return ( <li key={e.rank}> <Card elevation={elevation} className="h-full gap-0 p-6"> <div className="flex items-center justify-between text-[13px]"> <span className="font-medium">No. {e.rank}</span> {e.owner ? ( <span className="font-mono text-xs text-muted-foreground"> {e.owner} </span> ) : null} </div> <div className="mt-3 flex items-center gap-3"> <Avatar className="size-11 rounded-xl after:rounded-xl"> {e.image ? ( <AvatarImage src={e.image} alt="" className="rounded-xl" /> ) : null} <AvatarFallback className="rounded-xl bg-muted font-mono text-sm text-foreground-2"> {(e.initials ?? e.name.slice(0, 2)).toUpperCase()} </AvatarFallback> </Avatar> <div> {e.href ? ( <a href={e.href} className="text-lg font-medium hover:underline" > {e.name} </a> ) : ( <div className="text-lg font-medium">{e.name}</div> )} {e.symbol ? ( <div className="font-mono text-xs text-muted-foreground"> {e.symbol} </div> ) : null} </div> </div> {e.change ? ( <div className="mt-4 flex items-end justify-between gap-3"> <span className={cn( "text-[30px] leading-none tracking-[-0.8px]", down ? "text-destructive-text" : "text-brand-text", )} > {e.change} </span> {e.spark ? ( <Sparkline data={e.spark} tone={down ? "down" : "up"} width={150} height={34} className="max-w-[45%]" /> ) : null} </div> ) : null} {e.allocation?.length ? ( <AllocationBar segments={e.allocation} size="sm" className="mt-4" /> ) : null} {e.value !== undefined || e.count !== undefined ? ( <div className="mt-4 flex items-center gap-4 border-t border-border-2 pt-3.5 text-[13px] text-muted-foreground"> {e.value !== undefined ? ( <span> {valueLabel}{" "} <b className="font-mono font-medium text-foreground"> {e.value} </b> </span> ) : null} {e.count !== undefined ? ( <span> {countLabel}{" "} <b className="font-mono font-medium text-foreground"> {e.count} </b> </span> ) : null} </div> ) : null} </Card> </li> ); })} </ol> ); } const ordered = [by(2), by(1), by(3)].filter((e): e is PodiumEntry => !!e); return ( <ol data-slot="leaderboard-podium" className={cn("flex items-start gap-3", className)} {...props} > {ordered.map((e) => ( <li key={e.rank} className={cn("w-[200px]", e.rank !== 1 && "mt-6")}> <Card size="sm" elevation={elevation} className="items-center gap-0 p-[18px] text-center" > <Badge shape="number" variant={e.rank === 1 ? "warning" : "secondary"} className="rounded-full" > #{e.rank} </Badge> <Avatar className="mt-3 size-11"> {e.image ? <AvatarImage src={e.image} alt="" /> : null} <AvatarFallback className="bg-linear-to-br from-info to-brand text-sm text-white"> {(e.initials ?? e.name.slice(0, 2)).toUpperCase()} </AvatarFallback> </Avatar> {e.href ? ( <a href={e.href} className="mt-2.5 font-semibold hover:underline"> {e.name} </a> ) : ( <div className="mt-2.5 font-semibold">{e.name}</div> )} {e.meta ? ( <div className="mt-0.5 font-mono text-xs text-muted-foreground"> {e.meta} </div> ) : null} </Card> </li> ))} </ol> );}
export type { PodiumAllocation, PodiumEntry };export { LeaderboardPodium };<script setup lang="ts">import type { HTMLAttributes } from 'vue'import type { PodiumEntry } from '.'import { computed } from 'vue'import { cn } from '@/registry/edmi/lib/utils'import { AllocationBar } from '@/registry/edmi/ui/allocation-bar'import { Avatar, AvatarFallback, AvatarImage } from '@/registry/edmi/ui/avatar'import { Badge } from '@/registry/edmi/ui/badge'import { Card } from '@/registry/edmi/ui/card'import type { Elevation } from '@/registry/edmi/ui/elevation'import { Sparkline } from '@/registry/edmi/ui/index-row'
const props = withDefaults(defineProps<{ /** ✦ depth of the cards, forwarded to each Card */ elevation?: Elevation /** ✦ `podium` (default) or `cards` */ variant?: 'podium' | 'cards' /** ✦ `cards`: label of the footer `value` stat */ valueLabel?: string /** ✦ `cards`: label of the footer `count` stat */ countLabel?: string entries: PodiumEntry[] class?: HTMLAttributes['class']}>(), { variant: 'podium', valueLabel: 'Value', countLabel: 'Count',})
const isDown = (s: string) => /^[-−–]/.test(s.trim())
// `podium` (default): #1 raised in the middle (warning badge), #2 left and #3 right sit lower.// `cards` ✦: three equal stat cards in rank order (leaderboard board).const ordered = computed(() => { const by = (r: number) => props.entries.find(e => e.rank === r) const order = props.variant === 'cards' ? [1, 2, 3] : [2, 1, 3] return order.map(by).filter((e): e is PodiumEntry => !!e)})</script>
<template> <ol v-if="variant === 'cards'" data-slot="leaderboard-podium" data-variant="cards" :class="cn('grid gap-4 lg:grid-cols-3', props.class)" > <li v-for="e in ordered" :key="e.rank"> <Card :elevation="elevation" class="h-full gap-0 p-6"> <div class="flex items-center justify-between text-[13px]"> <span class="font-medium">No. {{ e.rank }}</span> <span v-if="e.owner" class="font-mono text-xs text-muted-foreground">{{ e.owner }}</span> </div> <div class="mt-3 flex items-center gap-3"> <Avatar class="size-11 rounded-xl after:rounded-xl"> <AvatarImage v-if="e.image" :src="e.image" alt="" class="rounded-xl" /> <AvatarFallback class="rounded-xl bg-muted font-mono text-sm text-foreground-2"> {{ (e.initials ?? e.name.slice(0, 2)).toUpperCase() }} </AvatarFallback> </Avatar> <div> <a v-if="e.href" :href="e.href" class="text-lg font-medium hover:underline">{{ e.name }}</a> <div v-else class="text-lg font-medium"> {{ e.name }} </div> <div v-if="e.symbol" class="font-mono text-xs text-muted-foreground"> {{ e.symbol }} </div> </div> </div> <div v-if="e.change" class="mt-4 flex items-end justify-between gap-3"> <span :class="cn('text-[30px] leading-none tracking-[-0.8px]', isDown(e.change) ? 'text-destructive-text' : 'text-brand-text')"> {{ e.change }} </span> <Sparkline v-if="e.spark" :data="e.spark" :tone="isDown(e.change) ? 'down' : 'up'" :width="150" :height="34" class="max-w-[45%]" /> </div> <AllocationBar v-if="e.allocation?.length" :segments="e.allocation" size="sm" class="mt-4" /> <div v-if="e.value !== undefined || e.count !== undefined" class="mt-4 flex items-center gap-4 border-t border-border-2 pt-3.5 text-[13px] text-muted-foreground" > <span v-if="e.value !== undefined">{{ valueLabel }} <b class="font-mono font-medium text-foreground">{{ e.value }}</b></span> <span v-if="e.count !== undefined">{{ countLabel }} <b class="font-mono font-medium text-foreground">{{ e.count }}</b></span> </div> </Card> </li> </ol> <ol v-else data-slot="leaderboard-podium" :class="cn('flex items-start gap-3', props.class)"> <li v-for="e in ordered" :key="e.rank" :class="cn('w-[200px]', e.rank !== 1 && 'mt-6')"> <Card :elevation="elevation" size="sm" class="items-center gap-0 p-[18px] text-center"> <Badge shape="number" :variant="e.rank === 1 ? 'warning' : 'secondary'" class="rounded-full"> #{{ e.rank }} </Badge> <Avatar class="mt-3 size-11"> <AvatarImage v-if="e.image" :src="e.image" alt="" /> <AvatarFallback class="bg-linear-to-br from-info to-brand text-sm text-white"> {{ (e.initials ?? e.name.slice(0, 2)).toUpperCase() }} </AvatarFallback> </Avatar> <a v-if="e.href" :href="e.href" class="mt-2.5 font-semibold hover:underline">{{ e.name }}</a> <div v-else class="mt-2.5 font-semibold"> {{ e.name }} </div> <div v-if="e.meta" class="mt-0.5 font-mono text-xs text-muted-foreground"> {{ e.meta }} </div> </Card> </li> </ol></template>export type PodiumAllocation = { label: string; /** Weight in percent (bars are proportional). */ value: number; /** Any CSS color; defaults cycle `--chart-1…5`. */ color?: string;};
export type PodiumEntry = { rank: 1 | 2 | 3; name: string; /** Mono caption under the name, e.g. `$49.2K · 412 members`. */ meta?: string; image?: string; /** Avatar initials; defaults to the first two letters of `name`. */ initials?: string; href?: string; /** ✦ `variant="cards"`: mono ticker under the name. */ symbol?: string; /** ✦ `variant="cards"`: mono owner, top right of the card. */ owner?: string; /** ✦ `variant="cards"`: signed headline percentage, e.g. `+1.12%`. */ change?: string; /** ✦ `variant="cards"`: series for the sparkline. */ spark?: number[]; /** ✦ `variant="cards"`: weights bar with legend. */ allocation?: PodiumAllocation[]; /** ✦ `variant="cards"`: footer stat, labelled by `valueLabel`. */ value?: string; /** ✦ `variant="cards"`: footer stat, labelled by `countLabel`. */ count?: string | number;};
export { default as LeaderboardPodium } from "./LeaderboardPodium.vue";export { default as LeaderboardPodium, type PodiumAllocation, type PodiumEntry,} from "./leaderboard-podium.svelte";<script lang="ts" module> export type PodiumAllocation = { label: string; /** Weight in percent (bars are proportional). */ value: number; /** Any CSS color; defaults cycle `--chart-1…5`. */ color?: string; };
export type PodiumEntry = { rank: 1 | 2 | 3; name: string; /** Mono caption under the name, e.g. `$49.2K · 412 members`. */ meta?: string; image?: string; /** Avatar initials; defaults to the first two letters of `name`. */ initials?: string; href?: string; /** ✦ `variant="cards"`: mono ticker under the name. */ symbol?: string; /** ✦ `variant="cards"`: mono owner, top right of the card. */ owner?: string; /** ✦ `variant="cards"`: signed headline percentage, e.g. `+1.12%`. */ change?: string; /** ✦ `variant="cards"`: series for the sparkline. */ spark?: number[]; /** ✦ `variant="cards"`: weights bar with legend. */ allocation?: PodiumAllocation[]; /** ✦ `variant="cards"`: footer stat, labelled by `valueLabel`. */ value?: string; /** ✦ `variant="cards"`: footer stat, labelled by `countLabel`. */ count?: string | number; };</script>
<script lang="ts"> import { AllocationBar } from "$lib/registry/ui/allocation-bar/index.js"; import { Avatar, AvatarFallback, AvatarImage } from "$lib/registry/ui/avatar/index.js"; import { Badge } from "$lib/registry/ui/badge/index.js"; import { Card } from "$lib/registry/ui/card/index.js"; import { Sparkline } from "$lib/registry/ui/index-row/index.js"; import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLOlAttributes } from "svelte/elements"; import type { Elevation } from "$lib/registry/ui/elevation/index.js"; import type { Snippet } from "svelte";
let { ref = $bindable(null), class: className, entries, elevation = "auto", variant = "podium", valueLabel = "Value", countLabel = "Count", ...restProps }: WithElementRef<Omit<HTMLOlAttributes, "children">, HTMLOListElement> & { entries: PodiumEntry[]; /** ✦ depth of the cards (forwarded to each Card). */ elevation?: Elevation; /** ✦ `podium` (default) or `cards`. */ variant?: "podium" | "cards"; /** ✦ `cards`: label of the footer `value` stat. */ valueLabel?: string | Snippet; /** ✦ `cards`: label of the footer `count` stat. */ countLabel?: string | Snippet; } = $props();
const isDown = (s: string) => /^[-−–]/.test(s.trim());
// `podium` (default): #1 raised in the middle (warning badge), #2 left and #3 right sit lower. // `cards` ✦: three equal stat cards in rank order (leaderboard board). const ordered = $derived( (variant === "cards" ? ([1, 2, 3] as const) : ([2, 1, 3] as const)) .map((r) => entries.find((e) => e.rank === r)) .filter((e): e is PodiumEntry => !!e) );</script>
{#snippet text(v: string | Snippet)} {#if typeof v === "function"}{@render v()}{:else}{v}{/if}{/snippet}
{#if variant === "cards"} <ol bind:this={ref} data-slot="leaderboard-podium" data-variant="cards" class={cn("grid gap-4 lg:grid-cols-3", className)} {...restProps} > {#each ordered as e (e.rank)} {@const down = e.change ? isDown(e.change) : false} <li> <Card {elevation} class="h-full gap-0 p-6"> <div class="flex items-center justify-between text-[13px]"> <span class="font-medium">No. {e.rank}</span> {#if e.owner}<span class="font-mono text-xs text-muted-foreground">{e.owner}</span>{/if} </div> <div class="mt-3 flex items-center gap-3"> <Avatar class="size-11 rounded-xl after:rounded-xl"> {#if e.image}<AvatarImage src={e.image} alt="" class="rounded-xl" />{/if} <AvatarFallback class="rounded-xl bg-muted font-mono text-sm text-foreground-2"> {(e.initials ?? e.name.slice(0, 2)).toUpperCase()} </AvatarFallback> </Avatar> <div> {#if e.href} <a href={e.href} class="text-lg font-medium hover:underline">{e.name}</a> {:else} <div class="text-lg font-medium">{e.name}</div> {/if} {#if e.symbol}<div class="font-mono text-xs text-muted-foreground">{e.symbol}</div>{/if} </div> </div> {#if e.change} <div class="mt-4 flex items-end justify-between gap-3"> <span class={cn("text-[30px] leading-none tracking-[-0.8px]", down ? "text-destructive-text" : "text-brand-text")}> {e.change} </span> {#if e.spark} <Sparkline data={e.spark} tone={down ? "down" : "up"} width={150} height={34} class="max-w-[45%]" /> {/if} </div> {/if} {#if e.allocation?.length} <AllocationBar segments={e.allocation} size="sm" class="mt-4" /> {/if} {#if e.value !== undefined || e.count !== undefined} <div class="mt-4 flex items-center gap-4 border-t border-border-2 pt-3.5 text-[13px] text-muted-foreground"> {#if e.value !== undefined}<span>{@render text(valueLabel)} <b class="font-mono font-medium text-foreground">{e.value}</b></span>{/if} {#if e.count !== undefined}<span>{@render text(countLabel)} <b class="font-mono font-medium text-foreground">{e.count}</b></span>{/if} </div> {/if} </Card> </li> {/each} </ol>{:else} <ol bind:this={ref} data-slot="leaderboard-podium" class={cn("flex items-start gap-3", className)} {...restProps}> {#each ordered as e (e.rank)} <li class={cn("w-[200px]", e.rank !== 1 && "mt-6")}> <Card size="sm" {elevation} class="items-center gap-0 p-[18px] text-center"> <Badge shape="number" variant={e.rank === 1 ? "warning" : "secondary"} class="rounded-full">#{e.rank}</Badge> <Avatar class="mt-3 size-11"> {#if e.image}<AvatarImage src={e.image} alt="" />{/if} <AvatarFallback class="bg-linear-to-br from-info to-brand text-sm text-white"> {(e.initials ?? e.name.slice(0, 2)).toUpperCase()} </AvatarFallback> </Avatar> {#if e.href} <a href={e.href} class="mt-2.5 font-semibold hover:underline">{e.name}</a> {:else} <div class="mt-2.5 font-semibold">{e.name}</div> {/if} {#if e.meta}<div class="mt-0.5 font-mono text-xs text-muted-foreground">{e.meta}</div>{/if} </Card> </li> {/each} </ol>{/if}Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Flat by default. elevation is forwarded to every 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 { LeaderboardPodium } from "@/components/leaderboard-podium/leaderboard-podium";import type { Elevation } from "@/components/ui/elevation";
function Sample({ elevation }: { elevation: Elevation }) { return ( <LeaderboardPodium elevation={elevation} entries={[ { rank: 1, name: "dewi", meta: "$49.2K · 412 holders" }, { rank: 2, name: "noah", meta: "$31.7K · 265 holders" }, { rank: 3, name: "sarah", meta: "$18.9K · 140 holders" }, ]} /> );}
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 { LeaderboardPodium } from "@/components/ui/leaderboard-podium";
const entries = [ { rank: 1, name: "dewi", meta: "$49.2K · 412 holders" }, { rank: 2, name: "noah", meta: "$31.7K · 265 holders" }, { rank: 3, name: "sarah", meta: "$18.9K · 140 holders" },] as const;
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> <LeaderboardPodium :elevation="l.value" :entries="[...entries]" /> </div> </div></template><script lang="ts"> import { LeaderboardPodium, type PodiumEntry } from "$lib/components/ui/leaderboard-podium";
const entries: PodiumEntry[] = [ { rank: 1, name: "dewi", meta: "$49.2K · 412 holders" }, { rank: 2, name: "noah", meta: "$31.7K · 265 holders" }, { rank: 3, name: "sarah", meta: "$18.9K · 140 holders" }, ];
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><LeaderboardPodium elevation={l.value} {entries} /> </div> {/each}</div>| Part | Prop | Type | Default |
|---|---|---|---|
LeaderboardPodium |
entries |
{ rank: 1 | 2 | 3, name, meta?, image?, initials?, href? }[] |
required |
elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
|
variant ✦ |
"podium" | "cards" |
"podium" |
|
valueLabel / countLabel ✦ |
label of the cards footer stats (Svelte string | Snippet) |
"Value" / "Count" |
|
entries[] (cards) ✦ |
symbol, owner, change, spark, allocation, value, count |
cards variant fields; spark renders the Sparkline of Index Row and allocation the Allocation Bar (size="sm") |
Order is fixed visually (2, 1, 3) whatever the array order.
Cards ✦
Section titled “Cards ✦”variant="cards" is the leaderboard top three: three equal cards in rank order with owner, name and ticker, headline change with sparkline, an allocation bar and two footer stats (value, count, labelled by valueLabel / countLabel). meta is not needed there.
import { LeaderboardPodium } from "@/components/leaderboard-podium/leaderboard-podium";
const entries = [ { rank: 1, name: "Mag Four Tilt", symbol: "MAGT", owner: "GbFK…ZUWS", change: "+1.12%", spark: [1, 1.01, 1.03, 1.02, 1.04, 1.05, 1.06], allocation: [ { label: "AAPLx", value: 30 }, { label: "NVDAx", value: 40 }, { label: "TSLAx", value: 30 }, ], value: "$120.1K", count: 2, }, { rank: 2, name: "AI Frontier", symbol: "AIFR", owner: "8FcD…cL9y", change: "+0.84%", spark: [1, 1.01, 1.0, 1.02, 1.03, 1.04], allocation: [ { label: "NVDAx", value: 55 }, { label: "MSFTx", value: 45 }, ], value: "$82.4K", count: 2, }, { rank: 3, name: "Defense & Space", symbol: "DFSP", owner: "7Ge1…kXSq", change: "-2.09%", spark: [1, 0.99, 0.98, 0.985, 0.97, 0.965], allocation: [ { label: "SPACEX-pre", value: 60 }, { label: "TSLAx", value: 40 }, ], value: "$29K", count: 1, },] as const;
export default function Demo() { return ( <LeaderboardPodium variant="cards" valueLabel="AUM" countLabel="Holders" className="w-full" entries={entries.map((e) => ({ ...e, allocation: [...e.allocation], spark: [...e.spark], }))} /> );}<script setup lang="ts">import { LeaderboardPodium } from "@/components/ui/leaderboard-podium";
const entries = [ { rank: 1, name: "Mag Four Tilt", symbol: "MAGT", owner: "GbFK…ZUWS", change: "+1.12%", spark: [1, 1.01, 1.03, 1.02, 1.04, 1.05, 1.06], allocation: [ { label: "AAPLx", value: 30 }, { label: "NVDAx", value: 40 }, { label: "TSLAx", value: 30 }, ], value: "$120.1K", count: 2, }, { rank: 2, name: "AI Frontier", symbol: "AIFR", owner: "8FcD…cL9y", change: "+0.84%", spark: [1, 1.01, 1.0, 1.02, 1.03, 1.04], allocation: [ { label: "NVDAx", value: 55 }, { label: "MSFTx", value: 45 }, ], value: "$82.4K", count: 2, }, { rank: 3, name: "Defense & Space", symbol: "DFSP", owner: "7Ge1…kXSq", change: "-2.09%", spark: [1, 0.99, 0.98, 0.985, 0.97, 0.965], allocation: [ { label: "SPACEX-pre", value: 60 }, { label: "TSLAx", value: 40 }, ], value: "$29K", count: 1, },] as const;</script>
<template> <LeaderboardPodium variant="cards" value-label="AUM" count-label="Holders" class="w-full" :entries="entries.map((e) => ({ ...e, allocation: [...e.allocation], spark: [...e.spark] }))" /></template><script lang="ts"> import { LeaderboardPodium } from "$lib/components/ui/leaderboard-podium";
const entries = [ { rank: 1, name: "Mag Four Tilt", symbol: "MAGT", owner: "GbFK…ZUWS", change: "+1.12%", spark: [1, 1.01, 1.03, 1.02, 1.04, 1.05, 1.06], allocation: [ { label: "AAPLx", value: 30 }, { label: "NVDAx", value: 40 }, { label: "TSLAx", value: 30 }, ], value: "$120.1K", count: 2, }, { rank: 2, name: "AI Frontier", symbol: "AIFR", owner: "8FcD…cL9y", change: "+0.84%", spark: [1, 1.01, 1.0, 1.02, 1.03, 1.04], allocation: [ { label: "NVDAx", value: 55 }, { label: "MSFTx", value: 45 }, ], value: "$82.4K", count: 2, }, { rank: 3, name: "Defense & Space", symbol: "DFSP", owner: "7Ge1…kXSq", change: "-2.09%", spark: [1, 0.99, 0.98, 0.985, 0.97, 0.965], allocation: [ { label: "SPACEX-pre", value: 60 }, { label: "TSLAx", value: 40 }, ], value: "$29K", count: 1, }, ] as const;</script>
<LeaderboardPodium variant="cards" valueLabel="AUM" countLabel="Holders" class="w-full" entries={entries.map((e) => ({ ...e, allocation: [...e.allocation], spark: [...e.spark] }))}/>✦ Edmi additions
Section titled “✦ Edmi additions”- #1 gets the warning badge, #2/#3 the secondary badge.