Index Row
Market table row: avatar stack, name, ticker and tags, mono price, delta and sparkline; includes a matching header row.
import { IndexRow, type IndexRowData, IndexRowHeader,} from "@/components/index-row/index-row";import { Card } from "@/components/ui/card";import { Table, TableBody, TableHeader } from "@/components/ui/table";
const avatars = [{ label: "A" }, { label: "N" }, { label: "T" }];
const rows: IndexRowData[] = [ { name: "Mag Four Tilt", symbol: "MAGT", avatars, tags: ["Pre-IPO", "Clone"], owner: "GbFK…ZUWS", price: "$1.00", change: "+1.12%", value: "$120.1K", count: 2, spark: [4, 5, 4.6, 6, 6.4, 6.1, 7.2, 8, 8.4, 9.1], }, { name: "Defense & Space", symbol: "DFSP", avatars, tags: ["Pre-IPO"], owner: "7Ge1…kXSq", price: "$0.9791", change: "−2.09%", value: "$29K", count: 1, spark: [9, 8.6, 8.9, 7.8, 7.5, 7.9, 6.8, 6.4, 6.6, 5.9], },];
export default function Demo() { return ( <Card className="w-full gap-0 py-0"> <Table> <TableHeader> <IndexRowHeader labels={{ name: "Index", owner: "Creator", change: "7d", value: "AUM", count: "Holders", trend: "30d", }} /> </TableHeader> <TableBody> {rows.map((r) => ( <IndexRow key={r.symbol} index={r} /> ))} </TableBody> </Table> </Card> );}<script setup lang="ts">import { IndexRow, IndexRowHeader, type IndexRowData } from "@/components/ui/index-row";import { Card } from "@/components/ui/card";import { Table, TableBody, TableHeader } from "@/components/ui/table";
const avatars = [{ label: "A" }, { label: "N" }, { label: "T" }];
const rows: IndexRowData[] = [ { name: "Mag Four Tilt", symbol: "MAGT", avatars, tags: ["Pre-IPO", "Clone"], owner: "GbFK…ZUWS", price: "$1.00", change: "+1.12%", value: "$120.1K", count: 2, spark: [4, 5, 4.6, 6, 6.4, 6.1, 7.2, 8, 8.4, 9.1], }, { name: "Defense & Space", symbol: "DFSP", avatars, tags: ["Pre-IPO"], owner: "7Ge1…kXSq", price: "$0.9791", change: "−2.09%", value: "$29K", count: 1, spark: [9, 8.6, 8.9, 7.8, 7.5, 7.9, 6.8, 6.4, 6.6, 5.9], },];</script>
<template> <Card class="w-full gap-0 py-0"> <Table> <TableHeader> <IndexRowHeader :labels="{ name: 'Index', owner: 'Creator', change: '7d', value: 'AUM', count: 'Holders', trend: '30d' }" /> </TableHeader> <TableBody> <IndexRow v-for="r in rows" :key="r.symbol" :index="r" /> </TableBody> </Table> </Card></template><script lang="ts"> import { Card } from "$lib/components/ui/card"; import { IndexRow, IndexRowHeader, type IndexRowData } from "$lib/components/ui/index-row"; import { Table, TableBody, TableHeader } from "$lib/components/ui/table";
const avatars = [{ label: "A" }, { label: "N" }, { label: "T" }];
const rows: IndexRowData[] = [ { name: "Mag Four Tilt", symbol: "MAGT", avatars, tags: ["Pre-IPO", "Clone"], owner: "GbFK…ZUWS", price: "$1.00", change: "+1.12%", value: "$120.1K", count: 2, spark: [4, 5, 4.6, 6, 6.4, 6.1, 7.2, 8, 8.4, 9.1], }, { name: "Defense & Space", symbol: "DFSP", avatars, tags: ["Pre-IPO"], owner: "7Ge1…kXSq", price: "$0.9791", change: "−2.09%", value: "$29K", count: 1, spark: [9, 8.6, 8.9, 7.8, 7.5, 7.9, 6.8, 6.4, 6.6, 5.9], }, ];</script>
<Card class="w-full gap-0 py-0"> <Table> <TableHeader> <IndexRowHeader labels={{ name: "Index", owner: "Creator", change: "7d", value: "AUM", count: "Holders", trend: "30d" }} /> </TableHeader> <TableBody> {#each rows as r (r.symbol)} <IndexRow index={r} /> {/each} </TableBody> </Table></Card>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/index-rowpnpm dlx shadcn@latest add @edmi-ui/index-rowyarn dlx shadcn@latest add @edmi-ui/index-rowbunx shadcn@latest add @edmi-ui/index-rowPick 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:
table,avatar,badge.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/index-rowpnpm dlx shadcn-vue@latest add @edmi-ui/index-rowyarn dlx shadcn-vue@latest add @edmi-ui/index-rowbunx shadcn-vue@latest add @edmi-ui/index-rowPick 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:
table,avatar,badge.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/index-row.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/index-row.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/index-row.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/index-row.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:
table,avatar,badge.Copy the code from Component source into your project and update the import paths to match your aliases.
import { IndexRowHeader } from "@/components/index-row/index-row";import { Card } from "@/components/ui/card";import { Table, TableBody, TableHeader } from "@/components/ui/table";
<Card className="w-full gap-0 py-0"> <Table> <TableHeader> <IndexRowHeader labels={{ name: "Index", owner: "Creator", change: "7d", value: "AUM", count: "Holders", trend: "30d", }} /> </TableHeader> <TableBody> </TableBody> </Table></Card><script setup lang="ts">import { IndexRow, IndexRowHeader } from "@/components/ui/index-row";import { Card } from "@/components/ui/card";import { Table, TableBody, TableHeader } from "@/components/ui/table";</script>
<template> <Card class="w-full gap-0 py-0"> <Table> <TableHeader> <IndexRowHeader :labels="{ name: 'Index', owner: 'Creator', change: '7d', value: 'AUM', count: 'Holders', trend: '30d' }" /> </TableHeader> <TableBody> <IndexRow :key="r.symbol" :index="r" /> </TableBody> </Table> </Card></template><script lang="ts"> import { Card } from "$lib/components/ui/card"; import { IndexRow, IndexRowHeader } from "$lib/components/ui/index-row"; import { Table, TableBody, TableHeader } from "$lib/components/ui/table";</script>
<Card class="w-full gap-0 py-0"> <Table> <TableHeader> <IndexRowHeader labels={{ name: "Index", owner: "Creator", change: "7d", value: "AUM", count: "Holders", trend: "30d" }} /> </TableHeader> <TableBody> {#each rows as r (r.symbol)} <IndexRow index={r} /> {/each} </TableBody> </Table></Card>Component source
Section titled “Component source”import { cn } from "cn";import type * as React from "react";import { Avatar, AvatarFallback, AvatarGroup, AvatarImage,} from "@/registry/edmi/ui/avatar";import { Badge } from "@/registry/edmi/ui/badge";import { TableCell, TableHead, TableRow } from "@/registry/edmi/ui/table";
const isDown = (s: string) => /^[-−–]/.test(s.trim());
// Tiny trend line. `tone` defaults to the direction of the series (last vs first).function Sparkline({ className, data, tone, width = 88, height = 28, ...props}: Omit<React.ComponentProps<"svg">, "children" | "width" | "height"> & { data: number[]; tone?: "up" | "down"; width?: number; height?: number;}) { if (data.length < 2) return null; const min = Math.min(...data); const max = Math.max(...data); const span = max - min || 1; const pad = 2; const points = data .map((v, i) => { const x = pad + (i / (data.length - 1)) * (width - pad * 2); const y = pad + (1 - (v - min) / span) * (height - pad * 2); return `${x.toFixed(1)},${y.toFixed(1)}`; }) .join(" "); const down = tone ? tone === "down" : (data.at(-1) ?? 0) < (data[0] ?? 0); return ( <svg data-slot="sparkline" width={width} height={height} viewBox={`0 0 ${width} ${height}`} fill="none" aria-hidden="true" className={cn( "inline-block", down ? "text-destructive-text" : "text-success-text", className, )} {...props} > <polyline points={points} stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /> </svg> );}
type IndexRowData = { name: string; symbol: string; /** One avatar per member (e.g. a constituent): image URL or a short label. */ avatars: { label: string; image?: string }[]; tags?: string[]; /** Text of the owner column. */ owner: string; price: string; /** Signed percentage, e.g. `+1.12%`. */ change: string; /** Text of the value column (mono, right-aligned). */ value: string; /** Text of the count column (mono, right-aligned). */ count: string | number; /** Series for the trend sparkline. */ spark?: number[]; href?: string;};
type IndexRowHeaderLabels = { name?: React.ReactNode; owner?: React.ReactNode; price?: React.ReactNode; change?: React.ReactNode; value?: React.ReactNode; count?: React.ReactNode; trend?: React.ReactNode;};
const defaultLabels: Required<IndexRowHeaderLabels> = { name: "Name", owner: "Owner", price: "Price", change: "Change", value: "Value", count: "Count", trend: "Trend",};
// Header row matching the IndexRow columns. Place it in `<TableHeader>`.function IndexRowHeader({ className, rank = false, labels, ...props}: React.ComponentProps<typeof TableRow> & { /** ✦ Leading empty cell for the `rank` column of IndexRow. */ rank?: boolean; /** Column labels (each defaults to a neutral English word). */ labels?: IndexRowHeaderLabels;}) { const l = { ...defaultLabels, ...labels }; return ( <TableRow className={className} {...props}> {rank ? <TableHead className="w-10" /> : null} <TableHead>{l.name}</TableHead> <TableHead>{l.owner}</TableHead> <TableHead className="text-right">{l.price}</TableHead> <TableHead className="text-right">{l.change}</TableHead> <TableHead className="text-right">{l.value}</TableHead> <TableHead className="text-right">{l.count}</TableHead> <TableHead className="text-right">{l.trend}</TableHead> </TableRow> );}
// One market row: avatar stack, name + ticker + tags, mono numbers, delta, sparkline. Use inside `<TableBody>`.function IndexRow({ className, index, rank, delta = "text", ...props}: Omit<React.ComponentProps<typeof TableRow>, "children"> & { index: IndexRowData; /** ✦ Leading mono rank column (pair with `<IndexRowHeader rank />`). */ rank?: number | string; /** ✦ `text` (default) or `pill`: soft tinted pill for the 7d delta. */ delta?: "text" | "pill";}) { const down = isDown(index.change); return ( <TableRow data-slot="index-row" className={className} {...props}> {rank !== undefined ? ( <TableCell className="w-10 pr-0 font-mono text-[13px] text-muted-foreground"> {rank} </TableCell> ) : null} <TableCell> <div className="flex items-center gap-3"> <AvatarGroup> {index.avatars.map((t) => ( <Avatar key={t.label} className="size-7"> {t.image ? <AvatarImage src={t.image} alt="" /> : null} <AvatarFallback className="text-[10px]"> {t.label} </AvatarFallback> </Avatar> ))} </AvatarGroup> <div> <div className="flex items-center gap-2"> {index.href ? ( <a href={index.href} className="font-semibold hover:underline"> {index.name} </a> ) : ( <span className="font-semibold">{index.name}</span> )} <span className="font-mono text-[11.5px] text-muted-foreground"> {index.symbol} </span> </div> {index.tags?.length ? ( <div className="mt-1 flex items-center gap-1"> {index.tags.map((tag) => ( <Badge key={tag} variant="secondary" className="h-5 text-[11px]" > {tag} </Badge> ))} </div> ) : null} </div> </div> </TableCell> <TableCell className="font-mono text-xs text-muted-foreground"> {index.owner} </TableCell> <TableCell className="text-right font-mono text-[12.5px] font-semibold"> {index.price} </TableCell> <TableCell className={cn( "text-right font-mono text-[12.5px]", delta === "text" && (down ? "text-destructive-text" : "text-success-text"), )} > {delta === "pill" ? ( <span className={cn( "inline-block rounded-md px-2 py-[3px] text-xs", down ? "bg-destructive-soft text-destructive-text" : "bg-brand-soft text-brand-text", )} > {index.change} </span> ) : ( index.change )} </TableCell> <TableCell className="text-right font-mono text-[12.5px]"> {index.value} </TableCell> <TableCell className="text-right font-mono text-[12.5px]"> {index.count} </TableCell> <TableCell className="text-right"> {index.spark ? ( <Sparkline data={index.spark} tone={down ? "down" : "up"} /> ) : null} </TableCell> </TableRow> );}
export type { IndexRowData, IndexRowHeaderLabels };export { IndexRow, IndexRowHeader, Sparkline };<script setup lang="ts">import type { HTMLAttributes } from 'vue'import type { IndexRowData } from '.'import { computed } from 'vue'import { cn } from '@/registry/edmi/lib/utils'import { Avatar, AvatarFallback, AvatarGroup, AvatarImage } from '@/registry/edmi/ui/avatar'import { Badge } from '@/registry/edmi/ui/badge'import { TableCell, TableRow } from '@/registry/edmi/ui/table'import Sparkline from './Sparkline.vue'
const props = withDefaults(defineProps<{ index: IndexRowData /** ✦ Leading mono rank column (pair with `<IndexRowHeader rank />`). */ rank?: number | string /** ✦ `text` (default) or `pill`: soft tinted pill for the change. */ delta?: 'text' | 'pill' class?: HTMLAttributes['class']}>(), { rank: undefined, delta: 'text',})
const down = computed(() => /^[-−–]/.test(props.index.change.trim()))</script>
<template> <!-- One market row: avatar stack, name + ticker + tags, mono numbers, delta, sparkline. Use inside `<TableBody>`. --> <TableRow data-slot="index-row" :class="props.class"> <TableCell v-if="props.rank !== undefined" class="w-10 pr-0 font-mono text-[13px] text-muted-foreground"> {{ props.rank }} </TableCell> <TableCell> <div class="flex items-center gap-3"> <AvatarGroup> <Avatar v-for="t in index.avatars" :key="t.label" class="size-7"> <AvatarImage v-if="t.image" :src="t.image" alt="" /> <AvatarFallback class="text-[10px]"> {{ t.label }} </AvatarFallback> </Avatar> </AvatarGroup> <div> <div class="flex items-center gap-2"> <a v-if="index.href" :href="index.href" class="font-semibold hover:underline">{{ index.name }}</a> <span v-else class="font-semibold">{{ index.name }}</span> <span class="font-mono text-[11.5px] text-muted-foreground">{{ index.symbol }}</span> </div> <div v-if="index.tags?.length" class="mt-1 flex items-center gap-1"> <Badge v-for="tag in index.tags" :key="tag" variant="secondary" class="h-5 text-[11px]"> {{ tag }} </Badge> </div> </div> </div> </TableCell> <TableCell class="font-mono text-xs text-muted-foreground"> {{ index.owner }} </TableCell> <TableCell class="text-right font-mono text-[12.5px] font-semibold"> {{ index.price }} </TableCell> <TableCell :class="cn('text-right font-mono text-[12.5px]', props.delta === 'text' && (down ? 'text-destructive-text' : 'text-success-text'))" > <span v-if="props.delta === 'pill'" :class="cn('inline-block rounded-md px-2 py-[3px] text-xs', down ? 'bg-destructive-soft text-destructive-text' : 'bg-brand-soft text-brand-text')" >{{ index.change }}</span> <template v-else> {{ index.change }} </template> </TableCell> <TableCell class="text-right font-mono text-[12.5px]"> {{ index.value }} </TableCell> <TableCell class="text-right font-mono text-[12.5px]"> {{ index.count }} </TableCell> <TableCell class="text-right"> <Sparkline v-if="index.spark" :data="index.spark" :tone="down ? 'down' : 'up'" /> </TableCell> </TableRow></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import type { IndexRowHeaderLabels } from '.'import { computed } from 'vue'import { TableHead, TableRow } from '@/registry/edmi/ui/table'
const props = defineProps<{ /** ✦ Leading empty cell for the `rank` column of IndexRow. */ rank?: boolean /** Column labels (each defaults to a neutral English word). */ labels?: IndexRowHeaderLabels class?: HTMLAttributes['class']}>()
const l = computed(() => ({ name: 'Name', owner: 'Owner', price: 'Price', change: 'Change', value: 'Value', count: 'Count', trend: 'Trend', ...props.labels,}))</script>
<template> <!-- Header row matching the IndexRow columns. Place it in `<TableHeader>`. --> <TableRow :class="props.class"> <TableHead v-if="props.rank" class="w-10" /> <TableHead>{{ l.name }}</TableHead> <TableHead>{{ l.owner }}</TableHead> <TableHead class="text-right"> {{ l.price }} </TableHead> <TableHead class="text-right"> {{ l.change }} </TableHead> <TableHead class="text-right"> {{ l.value }} </TableHead> <TableHead class="text-right"> {{ l.count }} </TableHead> <TableHead class="text-right"> {{ l.trend }} </TableHead> </TableRow></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { computed } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = withDefaults(defineProps<{ data: number[] /** Defaults to the direction of the series (last vs first). */ tone?: 'up' | 'down' width?: number height?: number class?: HTMLAttributes['class']}>(), { width: 88, height: 28,})
const points = computed(() => { const { data, width, height } = props const min = Math.min(...data) const max = Math.max(...data) const span = max - min || 1 const pad = 2 return data .map((v, i) => { const x = pad + (i / (data.length - 1)) * (width - pad * 2) const y = pad + (1 - (v - min) / span) * (height - pad * 2) return `${x.toFixed(1)},${y.toFixed(1)}` }) .join(' ')})const down = computed(() => props.tone ? props.tone === 'down' : (props.data.at(-1) ?? 0) < (props.data[0] ?? 0),)</script>
<template> <!-- Tiny trend line. --> <svg v-if="data.length >= 2" data-slot="sparkline" :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" fill="none" aria-hidden="true" :class="cn('inline-block', down ? 'text-destructive-text' : 'text-success-text', props.class)" > <polyline :points="points" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /> </svg></template>export type IndexRowData = { name: string; symbol: string; /** One avatar per member (e.g. a constituent): image URL or a short label. */ avatars: { label: string; image?: string }[]; tags?: string[]; /** Text of the owner column. */ owner: string; price: string; /** Signed percentage, e.g. `+1.12%`. */ change: string; /** Text of the value column (mono, right-aligned). */ value: string; /** Text of the count column (mono, right-aligned). */ count: string | number; /** Series for the trend sparkline. */ spark?: number[]; href?: string;};
export type IndexRowHeaderLabels = { name?: string; owner?: string; price?: string; change?: string; value?: string; count?: string; trend?: string;};
export { default as IndexRow } from "./IndexRow.vue";export { default as IndexRowHeader } from "./IndexRowHeader.vue";export { default as Sparkline } from "./Sparkline.vue";<script lang="ts" module> import type { Snippet } from "svelte";
export type IndexRowHeaderLabels = { name?: string | Snippet; owner?: string | Snippet; price?: string | Snippet; change?: string | Snippet; value?: string | Snippet; count?: string | Snippet; trend?: string | Snippet; };</script>
<script lang="ts"> import { TableHead, TableRow } from "$lib/registry/ui/table/index.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), rank = false, labels, ...restProps }: Omit<ComponentProps<typeof TableRow>, "children"> & { /** ✦ Leading empty cell for the `rank` column of IndexRow. */ rank?: boolean; /** Column labels (each defaults to a neutral English word). */ labels?: IndexRowHeaderLabels; } = $props();
const l = $derived({ name: "Name", owner: "Owner", price: "Price", change: "Change", value: "Value", count: "Count", trend: "Trend", ...labels, });</script>
{#snippet text(v: string | Snippet)} {#if typeof v === "function"}{@render v()}{:else}{v}{/if}{/snippet}
<!-- Header row matching the IndexRow columns. Place it in `<TableHeader>`. --><TableRow bind:ref {...restProps}> {#if rank}<TableHead class="w-10" />{/if} <TableHead>{@render text(l.name)}</TableHead> <TableHead>{@render text(l.owner)}</TableHead> <TableHead class="text-right">{@render text(l.price)}</TableHead> <TableHead class="text-right">{@render text(l.change)}</TableHead> <TableHead class="text-right">{@render text(l.value)}</TableHead> <TableHead class="text-right">{@render text(l.count)}</TableHead> <TableHead class="text-right">{@render text(l.trend)}</TableHead></TableRow><script lang="ts" module> export type IndexRowData = { name: string; symbol: string; /** One avatar per member (e.g. a constituent): image URL or a short label. */ avatars: { label: string; image?: string }[]; tags?: string[]; /** Text of the owner column. */ owner: string; price: string; /** Signed percentage, e.g. `+1.12%`. */ change: string; /** Text of the value column (mono, right-aligned). */ value: string; /** Text of the count column (mono, right-aligned). */ count: string | number; /** Series for the trend sparkline. */ spark?: number[]; href?: string; };</script>
<script lang="ts"> import { Avatar, AvatarFallback, AvatarGroup, AvatarImage } from "$lib/registry/ui/avatar/index.js"; import { Badge } from "$lib/registry/ui/badge/index.js"; import { TableCell, TableRow } from "$lib/registry/ui/table/index.js"; import { cn } from "$lib/utils.js"; import type { ComponentProps } from "svelte"; import Sparkline from "./sparkline.svelte";
let { ref = $bindable(null), class: className, index, rank, delta = "text", ...restProps }: Omit<ComponentProps<typeof TableRow>, "children"> & { index: IndexRowData; /** ✦ Leading mono rank column (pair with `<IndexRowHeader rank />`). */ rank?: number | string; /** ✦ `text` (default) or `pill`: soft tinted pill for the change. */ delta?: "text" | "pill"; } = $props();
const down = $derived(/^[-−–]/.test(index.change.trim()));</script>
<!-- One market row: avatar stack, name + ticker + tags, mono numbers, delta, sparkline. Use inside `<TableBody>`. --><TableRow bind:ref data-slot="index-row" class={className} {...restProps}> {#if rank !== undefined} <TableCell class="w-10 pr-0 font-mono text-[13px] text-muted-foreground">{rank}</TableCell> {/if} <TableCell> <div class="flex items-center gap-3"> <AvatarGroup> {#each index.avatars as t (t.label)} <Avatar class="size-7"> {#if t.image}<AvatarImage src={t.image} alt="" />{/if} <AvatarFallback class="text-[10px]">{t.label}</AvatarFallback> </Avatar> {/each} </AvatarGroup> <div> <div class="flex items-center gap-2"> {#if index.href} <a href={index.href} class="font-semibold hover:underline">{index.name}</a> {:else} <span class="font-semibold">{index.name}</span> {/if} <span class="font-mono text-[11.5px] text-muted-foreground">{index.symbol}</span> </div> {#if index.tags?.length} <div class="mt-1 flex items-center gap-1"> {#each index.tags as tag (tag)} <Badge variant="secondary" class="h-5 text-[11px]">{tag}</Badge> {/each} </div> {/if} </div> </div> </TableCell> <TableCell class="font-mono text-xs text-muted-foreground">{index.owner}</TableCell> <TableCell class="text-right font-mono text-[12.5px] font-semibold">{index.price}</TableCell> <TableCell class={cn("text-right font-mono text-[12.5px]", delta === "text" && (down ? "text-destructive-text" : "text-success-text"))} > {#if delta === "pill"} <span class={cn( "inline-block rounded-md px-2 py-[3px] text-xs", down ? "bg-destructive-soft text-destructive-text" : "bg-brand-soft text-brand-text" )}>{index.change}</span > {:else} {index.change} {/if} </TableCell> <TableCell class="text-right font-mono text-[12.5px]">{index.value}</TableCell> <TableCell class="text-right font-mono text-[12.5px]">{index.count}</TableCell> <TableCell class="text-right"> {#if index.spark}<Sparkline data={index.spark} tone={down ? "down" : "up"} />{/if} </TableCell></TableRow>export { default as IndexRow, type IndexRowData } from "./index-row.svelte";export { default as IndexRowHeader, type IndexRowHeaderLabels,} from "./index-row-header.svelte";export { default as Sparkline } from "./sparkline.svelte";<script lang="ts"> import { cn } from "$lib/utils.js"; import type { SVGAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, data, tone, width = 88, height = 28, ...restProps }: Omit<SVGAttributes<SVGSVGElement>, "children" | "width" | "height"> & { ref?: SVGSVGElement | null; data: number[]; /** Defaults to the direction of the series (last vs first). */ tone?: "up" | "down"; width?: number; height?: number; } = $props();
const points = $derived.by(() => { const min = Math.min(...data); const max = Math.max(...data); const span = max - min || 1; const pad = 2; return data .map((v, i) => { const x = pad + (i / (data.length - 1)) * (width - pad * 2); const y = pad + (1 - (v - min) / span) * (height - pad * 2); return `${x.toFixed(1)},${y.toFixed(1)}`; }) .join(" "); }); const down = $derived(tone ? tone === "down" : (data.at(-1) ?? 0) < (data[0] ?? 0));</script>
<!-- Tiny trend line. -->{#if data.length >= 2} <svg bind:this={ref} data-slot="sparkline" {width} {height} viewBox="0 0 {width} {height}" fill="none" aria-hidden="true" class={cn("inline-block", down ? "text-destructive-text" : "text-success-text", className)} {...restProps} > <polyline {points} stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /> </svg>{/if}| Part | Prop | Type | Default |
|---|---|---|---|
IndexRow |
index |
{ name, symbol, avatars, tags?, owner, price, change, value, count, spark?, href? } |
required |
rank ✦ |
number | string |
none (no rank column) | |
delta ✦ |
"text" | "pill" |
"text" |
|
IndexRowHeader |
labels |
{ name?, owner?, price?, change?, value?, count?, trend? } column labels (Svelte values may be snippets) |
Name, Owner, Price, Change, Value, Count, Trend |
rank ✦ |
boolean |
false |
|
Sparkline |
data |
number[] |
required |
tone |
"up" | "down" |
last vs first | |
width / height |
number |
88 / 28 |
Place IndexRowHeader in TableHeader and IndexRow in TableBody; numeric columns are mono and right-aligned (§4.10).
Ranked, pill delta ✦
Section titled “Ranked, pill delta ✦”Leaderboard style: a leading mono rank column and the change as a soft tinted pill.
import { IndexRow, type IndexRowData, IndexRowHeader,} from "@/components/index-row/index-row";import { Card } from "@/components/ui/card";import { Table, TableBody, TableHeader } from "@/components/ui/table";
const avatars = [{ label: "A" }, { label: "N" }, { label: "T" }];
const rows: IndexRowData[] = [ { name: "Mag Four Tilt", symbol: "MAGT", avatars, tags: ["Pre-IPO", "Clone"], owner: "GbFK…ZUWS", price: "$1.00", change: "+1.12%", value: "$120.1K", count: 2, spark: [4, 5, 4.6, 6, 6.4, 6.1, 7.2, 8, 8.4, 9.1], }, { name: "Defense & Space", symbol: "DFSP", avatars, tags: ["Pre-IPO"], owner: "7Ge1…kXSq", price: "$0.9791", change: "−2.09%", value: "$29K", count: 1, spark: [9, 8.6, 8.9, 7.8, 7.5, 7.9, 6.8, 6.4, 6.6, 5.9], },];
export default function Demo() { return ( <Card className="w-full gap-0 py-0"> <Table> <TableHeader> <IndexRowHeader rank labels={{ name: "Index", owner: "Creator", change: "7d", value: "AUM", count: "Holders", trend: "30d", }} /> </TableHeader> <TableBody> {rows.map((r, i) => ( <IndexRow key={r.symbol} index={r} rank={i + 1} delta="pill" /> ))} </TableBody> </Table> </Card> );}<script setup lang="ts">import { IndexRow, IndexRowHeader, type IndexRowData } from "@/components/ui/index-row";import { Card } from "@/components/ui/card";import { Table, TableBody, TableHeader } from "@/components/ui/table";
const avatars = [{ label: "A" }, { label: "N" }, { label: "T" }];
const rows: IndexRowData[] = [ { name: "Mag Four Tilt", symbol: "MAGT", avatars, tags: ["Pre-IPO", "Clone"], owner: "GbFK…ZUWS", price: "$1.00", change: "+1.12%", value: "$120.1K", count: 2, spark: [4, 5, 4.6, 6, 6.4, 6.1, 7.2, 8, 8.4, 9.1], }, { name: "Defense & Space", symbol: "DFSP", avatars, tags: ["Pre-IPO"], owner: "7Ge1…kXSq", price: "$0.9791", change: "−2.09%", value: "$29K", count: 1, spark: [9, 8.6, 8.9, 7.8, 7.5, 7.9, 6.8, 6.4, 6.6, 5.9], },];</script>
<template> <Card class="w-full gap-0 py-0"> <Table> <TableHeader> <IndexRowHeader rank :labels="{ name: 'Index', owner: 'Creator', change: '7d', value: 'AUM', count: 'Holders', trend: '30d' }" /> </TableHeader> <TableBody> <IndexRow v-for="(r, i) in rows" :key="r.symbol" :index="r" :rank="i + 1" delta="pill" /> </TableBody> </Table> </Card></template><script lang="ts"> import { Card } from "$lib/components/ui/card"; import { IndexRow, IndexRowHeader, type IndexRowData } from "$lib/components/ui/index-row"; import { Table, TableBody, TableHeader } from "$lib/components/ui/table";
const avatars = [{ label: "A" }, { label: "N" }, { label: "T" }];
const rows: IndexRowData[] = [ { name: "Mag Four Tilt", symbol: "MAGT", avatars, tags: ["Pre-IPO", "Clone"], owner: "GbFK…ZUWS", price: "$1.00", change: "+1.12%", value: "$120.1K", count: 2, spark: [4, 5, 4.6, 6, 6.4, 6.1, 7.2, 8, 8.4, 9.1], }, { name: "Defense & Space", symbol: "DFSP", avatars, tags: ["Pre-IPO"], owner: "7Ge1…kXSq", price: "$0.9791", change: "−2.09%", value: "$29K", count: 1, spark: [9, 8.6, 8.9, 7.8, 7.5, 7.9, 6.8, 6.4, 6.6, 5.9], }, ];</script>
<Card class="w-full gap-0 py-0"> <Table> <TableHeader> <IndexRowHeader rank labels={{ name: "Index", owner: "Creator", change: "7d", value: "AUM", count: "Holders", trend: "30d" }} /> </TableHeader> <TableBody> {#each rows as r, i (r.symbol)} <IndexRow index={r} rank={i + 1} delta="pill" /> {/each} </TableBody> </Table></Card>Elevation ✦
Section titled “Elevation ✦”Flat only: a table row has no depth. Put the table in a Card (flat, or raised for a featured table) to give it a surface.
✦ Edmi additions
Section titled “✦ Edmi additions”Sparklineis exported on its own.