Task List
Agent tasks grouped by status badge (ready to review, running, completed).
import { TaskList } from "@/components/task-list/task-list";
export default function Demo() { return ( <TaskList className="w-[300px] max-w-full" tasks={[ { title: "ICP analysis", agent: "Research agent", status: "review" }, { title: "Q1 analyst report", agent: "Vault agent", status: "running" }, { title: "Q4 drift review", agent: "Keeper agent", status: "completed", }, { title: "Rebalance playbook v2", agent: "Research agent", status: "completed", }, ]} /> );}<script setup lang="ts">import { TaskList, type Task } from "@/components/ui/task-list";
const tasks: Task[] = [ { title: "ICP analysis", agent: "Research agent", status: "review" }, { title: "Q1 analyst report", agent: "Vault agent", status: "running" }, { title: "Q4 drift review", agent: "Keeper agent", status: "completed" }, { title: "Rebalance playbook v2", agent: "Research agent", status: "completed" },];</script>
<template> <TaskList class="w-[300px] max-w-full" :tasks="tasks" /></template><script lang="ts"> import { TaskList, type Task } from "$lib/components/ui/task-list";
const tasks: Task[] = [ { title: "ICP analysis", agent: "Research agent", status: "review" }, { title: "Q1 analyst report", agent: "Vault agent", status: "running" }, { title: "Q4 drift review", agent: "Keeper agent", status: "completed" }, { title: "Rebalance playbook v2", agent: "Research agent", status: "completed" }, ];</script>
<TaskList class="w-[300px] max-w-full" {tasks} />Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/task-listpnpm dlx shadcn@latest add @edmi-ui/task-listyarn dlx shadcn@latest add @edmi-ui/task-listbunx shadcn@latest add @edmi-ui/task-listPick 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,separator,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/task-listpnpm dlx shadcn-vue@latest add @edmi-ui/task-listyarn dlx shadcn-vue@latest add @edmi-ui/task-listbunx shadcn-vue@latest add @edmi-ui/task-listPick 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,separator,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/task-list.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/task-list.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/task-list.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/task-list.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,separator,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { TaskList } from "@/components/task-list/task-list";
<TaskList className="w-[300px] max-w-full" tasks={[ { title: "ICP analysis", agent: "Research agent", status: "review" }, { title: "Q1 analyst report", agent: "Vault agent", status: "running" }, { title: "Q4 drift review", agent: "Keeper agent", status: "completed", }, { title: "Rebalance playbook v2", agent: "Research agent", status: "completed", }, ]}/><script setup lang="ts">import { TaskList } from "@/components/ui/task-list";</script>
<template> <TaskList class="w-[300px] max-w-full" /></template><script lang="ts"> import { TaskList } from "$lib/components/ui/task-list";</script>
<TaskList class="w-[300px] max-w-full" />Component source
Section titled “Component source”import { cn } from "cn";import * 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";import { Separator } from "@/registry/edmi/ui/separator";
type TaskStatus = "review" | "running" | "completed";
type Task = { id?: string; title: React.ReactNode; /** Owner label, rendered uppercase mono (e.g. "Research agent"). */ agent?: React.ReactNode; status: TaskStatus;};
const STATUS: Record< TaskStatus, { label: string; variant: "warning" | "info" | "success" }> = { review: { label: "Ready to review", variant: "warning" }, running: { label: "Running", variant: "info" }, completed: { label: "Completed", variant: "success" },};
type TaskListProps = React.ComponentProps<typeof Card> & { tasks: Task[]; /** Override the status badge labels. */ labels?: Partial<Record<TaskStatus, string>>; /** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation;};
/** Tasks grouped by status (first-appearance order), one badge per group. */function TaskList({ className, tasks, labels, elevation, ...props}: TaskListProps) { const groups: { status: TaskStatus; items: Task[] }[] = []; for (const t of tasks) { const g = groups.find((x) => x.status === t.status); if (g) g.items.push(t); else groups.push({ status: t.status, items: [t] }); } return ( <Card data-slot="task-list" elevation={elevation} className={cn("gap-0 px-[18px] py-4", className)} {...props} > {groups.map((g, gi) => ( <React.Fragment key={g.status}> {gi > 0 ? <Separator className="my-3" /> : null} <div data-slot="task-group" data-status={g.status}> <Badge variant={STATUS[g.status].variant} className="font-mono text-[10.5px]" > {labels?.[g.status] ?? STATUS[g.status].label} </Badge> {g.items.map((t, i) => ( <div key={t.id ?? i} data-slot="task-item" className="mt-2 flex items-center justify-between gap-2.5 text-[13px]" > <span>{t.title}</span> {t.agent ? ( <span className="text-[10px] whitespace-nowrap text-muted-foreground uppercase"> {t.agent} </span> ) : null} </div> ))} </div> </React.Fragment> ))} </Card> );}
export type { Task, TaskStatus };export { TaskList };<script setup lang="ts">import type { HTMLAttributes } from 'vue'import { computed } 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 { Separator } from '@/registry/edmi/ui/separator'import { cn } from '@/registry/edmi/lib/utils'import type { Task, TaskStatus } from './types'
const STATUS: Record<TaskStatus, { label: string; variant: 'warning' | 'info' | 'success' }> = { review: { label: 'Ready to review', variant: 'warning' }, running: { label: 'Running', variant: 'info' }, completed: { label: 'Completed', variant: 'success' },}
const props = defineProps<{ /** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2 */ elevation?: Elevation class?: HTMLAttributes['class'] tasks: Task[] /** Override the status badge labels. */ labels?: Partial<Record<TaskStatus, string>>}>()// ✦ Scoped slots `title` and `agent` ({ task }) = rich content for a task row (default: the text).
// Tasks grouped by status (first-appearance order), one badge per group.const groups = computed(() => { const out: { status: TaskStatus; items: Task[] }[] = [] for (const t of props.tasks) { const g = out.find((x) => x.status === t.status) if (g) g.items.push(t) else out.push({ status: t.status, items: [t] }) } return out})</script>
<template> <Card :elevation="elevation" data-slot="task-list" :class="cn('gap-0 px-[18px] py-4', props.class)"> <template v-for="(g, gi) in groups" :key="g.status"> <Separator v-if="gi > 0" class="my-3" /> <div data-slot="task-group" :data-status="g.status"> <Badge :variant="STATUS[g.status].variant" class="font-mono text-[10.5px]"> {{ labels?.[g.status] ?? STATUS[g.status].label }} </Badge> <div v-for="(t, i) in g.items" :key="t.id ?? i" data-slot="task-item" class="mt-2 flex items-center justify-between gap-2.5 text-[13px]" > <span><slot name="title" :task="t">{{ t.title }}</slot></span> <span v-if="t.agent || $slots.agent" class="text-[10px] whitespace-nowrap text-muted-foreground uppercase"> <slot name="agent" :task="t">{{ t.agent }}</slot> </span> </div> </div> </template> </Card></template>export { default as TaskList } from "./TaskList.vue";export type { Task, TaskStatus } from "./types";export type TaskStatus = "review" | "running" | "completed";
export type Task = { id?: string; title: string; /** Owner label, rendered uppercase mono (e.g. "Research agent"). */ agent?: string; status: TaskStatus;};import Root, { type Task, type TaskStatus } from "./task-list.svelte";
export { Root, // Root as TaskList, type Task, type TaskStatus,};<script lang="ts" module> import type { Snippet } from "svelte";
export type TaskStatus = "review" | "running" | "completed";
export type Task = { id?: string; title: string | Snippet; /** Owner label, rendered uppercase mono (e.g. "Research agent"). */ agent?: string | Snippet; status: TaskStatus; };</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 type { Elevation } from "$lib/registry/ui/elevation/index.js"; import { Separator } from "$lib/registry/ui/separator/index.js"; import type { ComponentProps } from "svelte";
const STATUS: Record<TaskStatus, { label: string; variant: "warning" | "info" | "success" }> = { review: { label: "Ready to review", variant: "warning" }, running: { label: "Running", variant: "info" }, completed: { label: "Completed", variant: "success" }, };
let { ref = $bindable(null), class: className, tasks, labels, elevation = "auto", ...restProps }: Omit<ComponentProps<typeof Card>, "children"> & { tasks: Task[]; /** Override the status badge labels. */ labels?: Partial<Record<TaskStatus, string>>; /** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation; } = $props();
// Tasks grouped by status (first-appearance order), one badge per group. const groups = $derived.by(() => { const out: { status: TaskStatus; items: Task[] }[] = []; for (const t of tasks) { const g = out.find((x) => x.status === t.status); if (g) g.items.push(t); else out.push({ status: t.status, items: [t] }); } return out; });</script>
<Card bind:ref {elevation} data-slot="task-list" class={cn("gap-0 px-[18px] py-4", className)} {...restProps}> {#each groups as g, gi (g.status)} {#if gi > 0}<Separator class="my-3" />{/if} <div data-slot="task-group" data-status={g.status}> <Badge variant={STATUS[g.status].variant} class="font-mono text-[10.5px]"> {labels?.[g.status] ?? STATUS[g.status].label} </Badge> {#each g.items as t, i (t.id ?? i)} <div data-slot="task-item" class="mt-2 flex items-center justify-between gap-2.5 text-[13px]"> <span>{#if typeof t.title === "function"}{@render t.title()}{:else}{t.title}{/if}</span> {#if t.agent} <span class="text-[10px] whitespace-nowrap text-muted-foreground uppercase">{#if typeof t.agent === "function"}{@render t.agent()}{:else}{t.agent}{/if}</span> {/if} </div> {/each} </div> {/each}</Card>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 { TaskList } from "@/components/task-list/task-list";import type { Elevation } from "@/components/ui/elevation";
function Sample({ elevation }: { elevation: Elevation }) { return ( <TaskList elevation={elevation} className="w-[300px] max-w-full" tasks={[ { title: "ICP analysis", agent: "Research agent", status: "review" }, { title: "Q1 analyst report", agent: "Vault agent", status: "running" }, { title: "Q4 drift review", agent: "Keeper agent", status: "completed", }, { title: "Rebalance playbook v2", agent: "Research agent", status: "completed", }, ]} /> );}
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 { TaskList, type Task } from "@/components/ui/task-list";
const tasks: Task[] = [ { title: "ICP analysis", agent: "Research agent", status: "review" }, { title: "Q1 analyst report", agent: "Vault agent", status: "running" }, { title: "Q4 drift review", agent: "Keeper agent", status: "completed" }, { title: "Rebalance playbook v2", agent: "Research agent", status: "completed" },];
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> <TaskList :elevation="l.value" class="w-[300px] max-w-full" :tasks="tasks" /> </div> </div></template><script lang="ts"> import { TaskList, type Task } from "$lib/components/ui/task-list";
const tasks: Task[] = [ { title: "ICP analysis", agent: "Research agent", status: "review" }, { title: "Q1 analyst report", agent: "Vault agent", status: "running" }, { title: "Q4 drift review", agent: "Keeper agent", status: "completed" }, { title: "Rebalance playbook v2", agent: "Research agent", status: "completed" }, ];
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><TaskList elevation={l.value} class="w-[300px] max-w-full" {tasks} /> </div> {/each}</div>| Part | Prop | Type | Default |
|---|---|---|---|
TaskList |
tasks |
{ id?, title, agent?, status }[]; status is review (warning), running (info) or completed (success), grouped in first-appearance order |
required |
Task.title, Task.agent |
React ReactNode; Vue string with scoped slots title / agent ({ task }); Svelte string | Snippet |
none | |
labels |
Partial<Record<status, string>>; overrides badge text |
none | |
elevation ✦ |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
✦ Edmi additions
Section titled “✦ Edmi additions”- Whole block is an Edmi addition (DESIGN §5 Patterns, board 12). Composed from: card, badge, separator.
- Install:
shadcn add @edmi-ui/task-list.