Skip to content
edmi-ui

Task List

Agent tasks grouped by status badge (ready to review, running, completed).

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Ready to review
ICP analysisResearch agent
Running
Q1 analyst reportVault agent
Completed
Q4 drift reviewKeeper agent
Rebalance playbook v2Research agent
Ready to review
ICP analysisResearch agent
Running
Q1 analyst reportVault agent
Completed
Q4 drift reviewKeeper agent
Rebalance playbook v2Research agent
Ready to review
ICP analysis Research agent
Running
Q1 analyst report Vault agent
Completed
Q4 drift review Keeper agent
Rebalance playbook v2 Research agent
Terminal window
npx shadcn@latest add @edmi-ui/task-list
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/task-list
Terminal window
yarn dlx shadcn@latest add @edmi-ui/task-list
Terminal window
bunx shadcn@latest add @edmi-ui/task-list

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/task-list
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/task-list
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/task-list
Terminal window
bunx shadcn-vue@latest add @edmi-ui/task-list

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/task-list.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/task-list.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/task-list.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/task-list.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

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" />
registry/blocks/task-list/task-list.tsx
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 };
registry/ui/task-list/TaskList.vue
<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>
registry/ui/task-list/index.ts
export { default as TaskList } from "./TaskList.vue";
export type { Task, TaskStatus } from "./types";
registry/ui/task-list/types.ts
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;
};
src/lib/registry/ui/task-list/index.ts
import Root, { type Task, type TaskStatus } from "./task-list.svelte";
export {
Root,
//
Root as TaskList,
type Task,
type TaskStatus,
};
src/lib/registry/ui/task-list/task-list.svelte
<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>

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).

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

Ready to review
ICP analysisResearch agent
Running
Q1 analyst reportVault agent
Completed
Q4 drift reviewKeeper agent
Rebalance playbook v2Research agent

Raised (+1)

Ready to review
ICP analysisResearch agent
Running
Q1 analyst reportVault agent
Completed
Q4 drift reviewKeeper agent
Rebalance playbook v2Research agent

Floating (+2)

Ready to review
ICP analysisResearch agent
Running
Q1 analyst reportVault agent
Completed
Q4 drift reviewKeeper agent
Rebalance playbook v2Research agent

Flat (0)

Ready to review
ICP analysisResearch agent
Running
Q1 analyst reportVault agent
Completed
Q4 drift reviewKeeper agent
Rebalance playbook v2Research agent

Raised (+1)

Ready to review
ICP analysisResearch agent
Running
Q1 analyst reportVault agent
Completed
Q4 drift reviewKeeper agent
Rebalance playbook v2Research agent

Floating (+2)

Ready to review
ICP analysisResearch agent
Running
Q1 analyst reportVault agent
Completed
Q4 drift reviewKeeper agent
Rebalance playbook v2Research agent

Flat (0)

Ready to review
ICP analysis Research agent
Running
Q1 analyst report Vault agent
Completed
Q4 drift review Keeper agent
Rebalance playbook v2 Research agent

Raised (+1)

Ready to review
ICP analysis Research agent
Running
Q1 analyst report Vault agent
Completed
Q4 drift review Keeper agent
Rebalance playbook v2 Research agent

Floating (+2)

Ready to review
ICP analysis Research agent
Running
Q1 analyst report Vault agent
Completed
Q4 drift review Keeper agent
Rebalance playbook v2 Research agent
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"
  • Whole block is an Edmi addition (DESIGN §5 Patterns, board 12). Composed from: card, badge, separator.
  • Install: shadcn add @edmi-ui/task-list.