Task
Collapsible task row with file chips.
import { Task, TaskContent, TaskItem, TaskItemFile, TaskTrigger,} from "@/components/ai/task";
export default function Demo() { return ( <Task className="w-full max-w-lg"> <TaskTrigger title="Update the weights file · 2 of 5" /> <TaskContent> <TaskItem status="completed"> Read <TaskItemFile>weights.ts</TaskItemFile> </TaskItem> <TaskItem status="completed">Validate total = 100%</TaskItem> <TaskItem status="in-progress"> Write <TaskItemFile>weights.ts</TaskItemFile> </TaskItem> <TaskItem status="pending">Run tests</TaskItem> <TaskItem status="error">Deploy preview failed</TaskItem> </TaskContent> </Task> );}<script setup lang="ts">import { Task, TaskContent, TaskItem, TaskItemFile, TaskTrigger } from "@/components/ai/task";</script>
<template> <Task class="w-full max-w-lg"> <TaskTrigger title="Update the weights file · 2 of 5" /> <TaskContent> <TaskItem status="completed">Read <TaskItemFile>weights.ts</TaskItemFile></TaskItem> <TaskItem status="completed">Validate total = 100%</TaskItem> <TaskItem status="in-progress">Write <TaskItemFile>weights.ts</TaskItemFile></TaskItem> <TaskItem status="pending">Run tests</TaskItem> <TaskItem status="error">Deploy preview failed</TaskItem> </TaskContent> </Task></template><script lang="ts"> import { Task, TaskContent, TaskItem, TaskItemFile, TaskTrigger, } from "$lib/components/ai/task";</script>
<Task class="w-full max-w-lg"> <TaskTrigger title="Update the weights file · 2 of 5" /> <TaskContent> <TaskItem status="completed">Read <TaskItemFile>weights.ts</TaskItemFile></TaskItem> <TaskItem status="completed">Validate total = 100%</TaskItem> <TaskItem status="in-progress">Write <TaskItemFile>weights.ts</TaskItemFile></TaskItem> <TaskItem status="pending">Run tests</TaskItem> <TaskItem status="error">Deploy preview failed</TaskItem> </TaskContent></Task>Built on collapsible.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-taskpnpm dlx shadcn@latest add @edmi-ui/ai-taskyarn dlx shadcn@latest add @edmi-ui/ai-taskbunx shadcn@latest add @edmi-ui/ai-taskPick 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:
collapsible.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/ai-taskpnpm dlx shadcn-vue@latest add @edmi-ui/ai-taskyarn dlx shadcn-vue@latest add @edmi-ui/ai-taskbunx shadcn-vue@latest add @edmi-ui/ai-taskPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @lucide/vue reka-uiTerminal window pnpm add @lucide/vue reka-uiTerminal window yarn add @lucide/vue reka-uiTerminal window bun add @lucide/vue reka-uiAdd the Edmi components this one builds on:
collapsible.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/ai-task.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-task.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-task.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-task.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:
collapsible.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Task, TaskContent, TaskItem, TaskItemFile, TaskTrigger,} from "@/components/ai/task";
<Task className="w-full max-w-lg"> <TaskTrigger title="Update the weights file · 2 of 5" /> <TaskContent> <TaskItem status="completed"> Read <TaskItemFile>weights.ts</TaskItemFile> </TaskItem> <TaskItem status="completed">Validate total = 100%</TaskItem> <TaskItem status="in-progress"> Write <TaskItemFile>weights.ts</TaskItemFile> </TaskItem> <TaskItem status="pending">Run tests</TaskItem> <TaskItem status="error">Deploy preview failed</TaskItem> </TaskContent></Task><script setup lang="ts">import { Task, TaskContent, TaskItem, TaskItemFile, TaskTrigger,} from "@/components/ai/task";</script>
<template> <Task class="w-full max-w-lg"> <TaskTrigger title="Update the weights file · 2 of 5" /> <TaskContent> <TaskItem status="completed">Read <TaskItemFile>weights.ts</TaskItemFile></TaskItem> <TaskItem status="completed">Validate total = 100%</TaskItem> <TaskItem status="in-progress">Write <TaskItemFile>weights.ts</TaskItemFile></TaskItem> <TaskItem status="pending">Run tests</TaskItem> <TaskItem status="error">Deploy preview failed</TaskItem> </TaskContent> </Task></template><script lang="ts"> import { Task, TaskContent, TaskItem, TaskItemFile, TaskTrigger, } from "$lib/components/ai/task";</script>
<Task class="w-full max-w-lg"> <TaskTrigger title="Update the weights file · 2 of 5" /> <TaskContent> <TaskItem status="completed">Read <TaskItemFile>weights.ts</TaskItemFile></TaskItem> <TaskItem status="completed">Validate total = 100%</TaskItem> <TaskItem status="in-progress">Write <TaskItemFile>weights.ts</TaskItemFile></TaskItem> <TaskItem status="pending">Run tests</TaskItem> <TaskItem status="error">Deploy preview failed</TaskItem> </TaskContent></Task>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type { ComponentProps } from "react";import { Children } from "react";import { FileIcon, CircleIcon, SpinnerIcon, CheckCircleIcon, WarningCircleIcon, CaretDownIcon } from "@phosphor-icons/react";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/registry/edmi/ui/collapsible";
export type TaskItemFileProps = ComponentProps<"div">;
export const TaskItemFile = ({ children, className, ...props}: TaskItemFileProps) => ( <div data-slot="ai-task-item-file" className={cn( "inline-flex h-5 items-center gap-1 rounded-md border border-border bg-secondary px-1.5 font-mono text-[11px] text-secondary-foreground [&_svg]:size-3 [&_svg]:shrink-0", className, )} {...props} > <FileIcon /> {children} </div>);
export type TaskItemStatus = "pending" | "in-progress" | "completed" | "error";
export type TaskItemProps = ComponentProps<"div"> & { /** ✦ Leading status icon: pending, in-progress (spinner), completed (struck through), error. */ status?: TaskItemStatus;};
const statusIcon = { pending: ( <CircleIcon className="text-muted-foreground-2" /> ), "in-progress": ( <SpinnerIcon className="animate-spin text-info-text" /> ), completed: ( <CheckCircleIcon className="text-success-text" /> ), error: ( <WarningCircleIcon className="text-destructive-text" /> ),} as const;
export const TaskItem = ({ children, className, status, ...props}: TaskItemProps) => status ? ( <div data-slot="ai-task-item" data-status={status} className={cn( "flex flex-wrap items-center gap-2 text-[13.5px] [&>svg]:size-4 [&>svg]:shrink-0", status === "completed" && "text-muted-foreground", status === "pending" && "text-muted-foreground", status === "error" && "text-destructive-text", className, )} {...props} > {statusIcon[status]} {/* line-through only the text, not the file chips (decorations propagate into flex items) */} {status === "completed" ? Children.map(children, (child) => typeof child === "string" ? ( <span className="line-through">{child}</span> ) : ( child ), ) : children} </div> ) : ( <div data-slot="ai-task-item" className={cn("text-[13.5px] text-muted-foreground", className)} {...props} > {children} </div> );
export type TaskProps = ComponentProps<typeof Collapsible>;
export const Task = ({ defaultOpen = true, className, ...props}: TaskProps) => ( <Collapsible data-slot="ai-task" className={cn("not-prose", className)} defaultOpen={defaultOpen} {...props} />);
export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & { title: string;};
export const TaskTrigger = ({ children, className, title, ...props}: TaskTriggerProps) => ( <CollapsibleTrigger data-slot="ai-task-trigger" className={cn( "group/task-trigger flex w-full items-center gap-2 text-[13.5px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", className, )} {...props} > {children ?? ( <> <CheckCircleIcon className="size-4" /> <span>{title}</span> <CaretDownIcon className="ml-auto size-4 transition-transform group-data-[panel-open]/task-trigger:rotate-180" /> </> )} </CollapsibleTrigger>);
export type TaskContentProps = ComponentProps<typeof CollapsibleContent>;
export const TaskContent = ({ children, className, ...props}: TaskContentProps) => ( <CollapsibleContent data-slot="ai-task-content" className={cn( "h-(--collapsible-panel-height) overflow-hidden outline-none transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0", className, )} {...props} > <div className="space-y-2 pt-2.5 pl-[23px]">{children}</div> </CollapsibleContent>);<script setup lang="ts">import type { CollapsibleRootProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Collapsible } from "@/registry/edmi/ui/collapsible"
interface TaskProps extends /* @vue-ignore */ CollapsibleRootProps { class?: HTMLAttributes["class"]}
const props = defineProps<TaskProps>()</script>
<template> <!-- Collapsible props (open, update:open) fall through as attrs; defaultOpen is true unless overridden. --> <Collapsible :default-open="true" data-slot="ai-task" :class="cn('not-prose', props.class)"> <slot /> </Collapsible></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { CollapsibleContent } from "@/registry/edmi/ui/collapsible"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <CollapsibleContent data-slot="ai-task-content" :class="cn( 'overflow-hidden outline-none data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down', props.class, )" > <div class="space-y-2 pt-2.5 pl-[23px]"> <slot /> </div> </CollapsibleContent></template><script setup lang="ts">import type { HTMLAttributes, VNode } from "vue"import { AlertCircleIcon, CircleCheckIcon, CircleIcon, Loader2Icon } from "@lucide/vue"import { Text, h, useSlots } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] /** ✦ Leading status icon: pending, in-progress (spinner), completed (struck through), error. */ status?: "pending" | "in-progress" | "completed" | "error"}>()
const slots = useSlots()
// line-through only the text, not the file chips (decorations propagate into flex items)function Content() { const nodes: VNode[] = slots.default?.() ?? [] if (props.status !== "completed") return nodes return nodes.map(node => node.type === Text && typeof node.children === "string" ? h("span", { class: "line-through" }, node.children) : node, )}</script>
<template> <div v-if="status" data-slot="ai-task-item" :data-status="status" :class="cn( 'flex flex-wrap items-center gap-2 text-[13.5px] [&>svg]:size-4 [&>svg]:shrink-0', status === 'completed' && 'text-muted-foreground', status === 'pending' && 'text-muted-foreground', status === 'error' && 'text-destructive-text', props.class, )" > <CircleIcon v-if="status === 'pending'" class="text-muted-foreground-2" /> <Loader2Icon v-else-if="status === 'in-progress'" class="animate-spin text-info-text" /> <CircleCheckIcon v-else-if="status === 'completed'" class="text-success-text" /> <AlertCircleIcon v-else class="text-destructive-text" /> <Content /> </div> <div v-else data-slot="ai-task-item" :class="cn('text-[13.5px] text-muted-foreground', props.class)"> <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { FileIcon } from "@lucide/vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div data-slot="ai-task-item-file" :class="cn( 'inline-flex h-5 items-center gap-1 rounded-md border border-border bg-secondary px-1.5 font-mono text-[11px] text-secondary-foreground [&_svg]:size-3 [&_svg]:shrink-0', props.class, )" > <FileIcon /> <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { ChevronDownIcon, CircleCheckIcon } from "@lucide/vue"import { cn } from "@/registry/edmi/lib/utils"import { CollapsibleTrigger } from "@/registry/edmi/ui/collapsible"
const props = defineProps<{ title: string class?: HTMLAttributes["class"]}>()</script>
<template> <CollapsibleTrigger data-slot="ai-task-trigger" :class="cn( 'group/task-trigger flex w-full items-center gap-2 text-[13.5px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring', props.class, )" > <slot> <CircleCheckIcon class="size-4" /> <span>{{ title }}</span> <ChevronDownIcon class="ml-auto size-4 transition-transform group-data-[state=open]/task-trigger:rotate-180" /> </slot> </CollapsibleTrigger></template>export { default as Task } from "./Task.vue";export { default as TaskContent } from "./TaskContent.vue";export { default as TaskItem } from "./TaskItem.vue";export { default as TaskItemFile } from "./TaskItemFile.vue";export { default as TaskTrigger } from "./TaskTrigger.vue";import Root from "./task.svelte";import Content from "./task-content.svelte";import Item from "./task-item.svelte";import ItemFile from "./task-item-file.svelte";import Trigger from "./task-trigger.svelte";
export { Content, Content as TaskContent, Item, Item as TaskItem, ItemFile, ItemFile as TaskItemFile, Root, // Root as Task, Trigger, Trigger as TaskTrigger,};<script lang="ts"> import { cn } from "$lib/utils.js"; import { CollapsibleContent } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte";
let { class: className, children, ...restProps }: ComponentProps<typeof CollapsibleContent> = $props();</script>
<CollapsibleContent data-slot="ai-task-content" class={cn( "overflow-hidden outline-none data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", className )} {...restProps}> <div class="space-y-2 pt-2.5 pl-[23px]"> {@render children?.()} </div></CollapsibleContent><script lang="ts"> import FileIcon from "phosphor-svelte/lib/File"; import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLSpanElement> = $props();</script>
<span data-slot="ai-task-item-file" class={cn( "inline-flex h-5 items-center gap-1 rounded-md border border-border bg-secondary px-1.5 align-middle font-mono text-[11px] text-secondary-foreground [&_svg]:size-3 [&_svg]:shrink-0", className )} {...restProps}> <FileIcon /> {@render children?.()}</span><script lang="ts"> import CircleIcon from "phosphor-svelte/lib/Circle"; import SpinnerIcon from "phosphor-svelte/lib/Spinner"; import CheckCircleIcon from "phosphor-svelte/lib/CheckCircle"; import WarningCircleIcon from "phosphor-svelte/lib/WarningCircle"; import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, status, children, ...restProps }: HTMLAttributes<HTMLDivElement> & { /** ✦ Leading status icon: pending, in-progress (spinner), completed (struck through), error. */ status?: "pending" | "in-progress" | "completed" | "error"; } = $props();</script>
{#if status} <div data-slot="ai-task-item" data-status={status} class={cn( "flex items-start gap-2 text-[13.5px] [&>svg]:mt-0.5 [&>svg]:size-4 [&>svg]:shrink-0", (status === "completed" || status === "pending") && "text-muted-foreground", status === "error" && "text-destructive-text", className )} {...restProps} > {#if status === "pending"} <CircleIcon class="text-muted-foreground-2" /> {:else if status === "in-progress"} <SpinnerIcon class="animate-spin text-info-text" /> {:else if status === "completed"} <CheckCircleIcon class="text-success-text" /> {:else} <WarningCircleIcon class="text-destructive-text" /> {/if} <!-- Inline flow: the line-through reaches the text but not the inline-flex file chips. --> <span class={cn("min-w-0", status === "completed" && "line-through")}> {@render children?.()} </span> </div>{:else} <div data-slot="ai-task-item" class={cn("text-[13.5px] text-muted-foreground", className)} {...restProps}> {@render children?.()} </div>{/if}<script lang="ts"> import CheckCircleIcon from "phosphor-svelte/lib/CheckCircle"; import CaretDownIcon from "phosphor-svelte/lib/CaretDown"; import { cn } from "$lib/utils.js"; import { CollapsibleTrigger } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte";
let { class: className, title, children, ...restProps }: Omit<ComponentProps<typeof CollapsibleTrigger>, "title"> & { title: string } = $props();</script>
<CollapsibleTrigger data-slot="ai-task-trigger" class={cn( "group/task-trigger flex w-full items-center gap-2 text-[13.5px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", className )} {...restProps}> {#if children} {@render children()} {:else} <CheckCircleIcon class="size-4" /> <span>{title}</span> <CaretDownIcon class="ml-auto size-4 transition-transform group-data-[state=open]/task-trigger:rotate-180" /> {/if}</CollapsibleTrigger><script lang="ts"> import { cn } from "$lib/utils.js"; import { Collapsible } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte";
let { class: className, open = $bindable(true), children, ...restProps }: ComponentProps<typeof Collapsible> = $props();</script>
<Collapsible bind:open data-slot="ai-task" class={cn("not-prose", className)} {...restProps}> {@render children?.()}</Collapsible>| Part | Prop | Type | Default |
|---|---|---|---|
| Task | open, defaultOpen, onOpenChange |
collapsible state | defaultOpen = true |
| TaskTrigger | title |
string |
|
| TaskItem | status |
pending | in-progress | completed | error |
none |
| TaskItemFile | mono file chip |
✦ Edmi additions
Section titled “✦ Edmi additions”TaskItemstatus✦ adds a leading status icon: spinner while in progress, struck-through text when completed, destructive text on error.TaskItemFilecarries a file icon; the chip is never struck through.