Queue
Queued messages and todos in collapsible sections with status dots.
- Summarize Q3 filingsFor NVDAx, MSFTx
- Draft a feed post about MAG4
- Compare fees with SPYx
- Quote MAG4 legsquotes.csv
- Check keeper limits
import { Queue, QueueItem, QueueItemAction, QueueItemActions, QueueItemAttachment, QueueItemContent, QueueItemDescription, QueueItemFile, QueueItemIndicator, QueueList, QueueSection, QueueSectionContent, QueueSectionLabel, QueueSectionTrigger,} from "@/components/ai/queue";import { useState } from "react";import { IconPlaceholder } from "@/edmi/icon-placeholder";
type Todo = { id: string; title: string; description?: string };
const initialQueued: Todo[] = [ { id: "1", title: "Summarize Q3 filings", description: "For NVDAx, MSFTx" }, { id: "2", title: "Draft a feed post about MAG4" }, { id: "3", title: "Compare fees with SPYx" },];
const initialDone: Todo[] = [ { id: "d1", title: "Quote MAG4 legs" }, { id: "d2", title: "Check keeper limits" },];
export default function Demo() { const [queued, setQueued] = useState(initialQueued); const [done, setDone] = useState(initialDone); const complete = (todo: Todo) => { setQueued((q) => q.filter((t) => t.id !== todo.id)); setDone((d) => [todo, ...d]); }; const uncomplete = (todo: Todo) => { setDone((d) => d.filter((t) => t.id !== todo.id)); setQueued((q) => [...q, todo]); }; const edit = (todo: Todo) => { const title = window.prompt("Edit task", todo.title)?.trim(); if (title) setQueued((q) => q.map((t) => (t.id === todo.id ? { ...t, title } : t))); }; return ( <Queue className="w-full max-w-md"> <QueueSection> <QueueSectionTrigger> <QueueSectionLabel label="Queued" count={queued.length} /> </QueueSectionTrigger> <QueueSectionContent> <QueueList> {queued.map((todo) => ( <QueueItem key={todo.id}> <div className="flex items-start gap-2"> <QueueItemIndicator role="button" tabIndex={0} aria-label="Mark completed" className="cursor-pointer" onClick={() => complete(todo)} onKeyDown={(e) => e.key === "Enter" && complete(todo)} /> <QueueItemContent>{todo.title}</QueueItemContent> <QueueItemActions> <QueueItemAction aria-label="Edit" onClick={() => edit(todo)} > <IconPlaceholder lucide="PencilIcon" tabler="IconPencil" hugeicons="EditIcon" phosphor="PencilIcon" remixicon="RiPencilLine" /> </QueueItemAction> <QueueItemAction aria-label="Remove" onClick={() => setQueued((q) => q.filter((t) => t.id !== todo.id)) } > <IconPlaceholder lucide="Trash2Icon" tabler="IconTrash" hugeicons="Delete02Icon" phosphor="TrashIcon" remixicon="RiDeleteBinLine" /> </QueueItemAction> </QueueItemActions> </div> {todo.description && ( <QueueItemDescription> {todo.description} </QueueItemDescription> )} </QueueItem> ))} </QueueList> </QueueSectionContent> </QueueSection> <QueueSection defaultOpen={false}> <QueueSectionTrigger> <QueueSectionLabel label="Completed" count={done.length} /> </QueueSectionTrigger> <QueueSectionContent> <QueueList> {done.map((todo) => ( <QueueItem key={todo.id}> <div className="flex items-start gap-2"> <QueueItemIndicator completed role="button" tabIndex={0} aria-label="Mark not completed" className="cursor-pointer" onClick={() => uncomplete(todo)} onKeyDown={(e) => e.key === "Enter" && uncomplete(todo)} /> <QueueItemContent completed>{todo.title}</QueueItemContent> </div> {todo.id === "d1" && ( <QueueItemAttachment> <QueueItemFile>quotes.csv</QueueItemFile> </QueueItemAttachment> )} </QueueItem> ))} </QueueList> </QueueSectionContent> </QueueSection> </Queue> );}<script setup lang="ts">import { Queue, QueueItem, QueueItemAction, QueueItemActions, QueueItemAttachment, QueueItemContent, QueueItemDescription, QueueItemFile, QueueItemIndicator, QueueList, QueueSection, QueueSectionContent, QueueSectionLabel, QueueSectionTrigger,} from "@/components/ai/queue";import { PencilIcon, Trash2Icon } from "@lucide/vue";import { ref } from "vue";
type Todo = { id: string; title: string; description?: string };
const queued = ref<Todo[]>([ { id: "1", title: "Summarize Q3 filings", description: "For NVDAx, MSFTx" }, { id: "2", title: "Draft a feed post about MAG4" }, { id: "3", title: "Compare fees with SPYx" },]);const done = ref<Todo[]>([ { id: "d1", title: "Quote MAG4 legs" }, { id: "d2", title: "Check keeper limits" },]);
function complete(todo: Todo) { queued.value = queued.value.filter((t) => t.id !== todo.id); done.value = [todo, ...done.value];}function uncomplete(todo: Todo) { done.value = done.value.filter((t) => t.id !== todo.id); queued.value = [...queued.value, todo];}function remove(todo: Todo) { queued.value = queued.value.filter((t) => t.id !== todo.id);}function edit(todo: Todo) { const title = window.prompt("Edit task", todo.title)?.trim(); if (title) queued.value = queued.value.map((t) => (t.id === todo.id ? { ...t, title } : t));}</script>
<template> <Queue class="w-full max-w-md"> <QueueSection> <QueueSectionTrigger> <QueueSectionLabel label="Queued" :count="queued.length" /> </QueueSectionTrigger> <QueueSectionContent> <QueueList> <QueueItem v-for="todo in queued" :key="todo.id"> <div class="flex items-start gap-2"> <QueueItemIndicator role="button" tabindex="0" aria-label="Mark completed" class="cursor-pointer" @click="complete(todo)" @keydown.enter="complete(todo)" /> <QueueItemContent>{{ todo.title }}</QueueItemContent> <QueueItemActions> <QueueItemAction aria-label="Edit" @click="edit(todo)"><PencilIcon /></QueueItemAction> <QueueItemAction aria-label="Remove" @click="remove(todo)"><Trash2Icon /></QueueItemAction> </QueueItemActions> </div> <QueueItemDescription v-if="todo.description">{{ todo.description }}</QueueItemDescription> </QueueItem> </QueueList> </QueueSectionContent> </QueueSection> <QueueSection :default-open="false"> <QueueSectionTrigger> <QueueSectionLabel label="Completed" :count="done.length" /> </QueueSectionTrigger> <QueueSectionContent> <QueueList> <QueueItem v-for="todo in done" :key="todo.id"> <div class="flex items-start gap-2"> <QueueItemIndicator completed role="button" tabindex="0" aria-label="Mark not completed" class="cursor-pointer" @click="uncomplete(todo)" @keydown.enter="uncomplete(todo)" /> <QueueItemContent completed>{{ todo.title }}</QueueItemContent> </div> <QueueItemAttachment v-if="todo.id === 'd1'"> <QueueItemFile>quotes.csv</QueueItemFile> </QueueItemAttachment> </QueueItem> </QueueList> </QueueSectionContent> </QueueSection> </Queue></template><script lang="ts"> import { Queue, QueueItem, QueueItemAction, QueueItemActions, QueueItemAttachment, QueueItemContent, QueueItemDescription, QueueItemFile, QueueItemIndicator, QueueList, QueueSection, QueueSectionContent, QueueSectionLabel, QueueSectionTrigger, } from "$lib/components/ai/queue"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte";
type Todo = { id: string; title: string; description?: string };
let queued = $state<Todo[]>([ { id: "1", title: "Summarize Q3 filings", description: "For NVDAx, MSFTx" }, { id: "2", title: "Draft a feed post about MAG4" }, { id: "3", title: "Compare fees with SPYx" }, ]); let done = $state<Todo[]>([ { id: "d1", title: "Quote MAG4 legs" }, { id: "d2", title: "Check keeper limits" }, ]);
function complete(todo: Todo) { queued = queued.filter((t) => t.id !== todo.id); done = [todo, ...done]; } function uncomplete(todo: Todo) { done = done.filter((t) => t.id !== todo.id); queued = [...queued, todo]; } function remove(todo: Todo) { queued = queued.filter((t) => t.id !== todo.id); } function edit(todo: Todo) { const title = window.prompt("Edit task", todo.title)?.trim(); if (title) queued = queued.map((t) => (t.id === todo.id ? { ...t, title } : t)); }</script>
<Queue class="w-full max-w-md"> <QueueSection> <QueueSectionTrigger> <QueueSectionLabel label="Queued" count={queued.length} /> </QueueSectionTrigger> <QueueSectionContent> <QueueList> {#each queued as todo (todo.id)} <QueueItem> <div class="flex items-start gap-2"> <QueueItemIndicator role="button" tabindex={0} aria-label="Mark completed" class="cursor-pointer" onclick={() => complete(todo)} onkeydown={(e) => e.key === "Enter" && complete(todo)} /> <QueueItemContent>{todo.title}</QueueItemContent> <QueueItemActions> <QueueItemAction aria-label="Edit" onclick={() => edit(todo)}> <IconPlaceholder lucide="PencilIcon" tabler="IconPencil" hugeicons="EditIcon" phosphor="PencilIcon" remixicon="RiPencilLine" /> </QueueItemAction> <QueueItemAction aria-label="Remove" onclick={() => remove(todo)}> <IconPlaceholder lucide="Trash2Icon" tabler="IconTrash" hugeicons="Delete02Icon" phosphor="TrashIcon" remixicon="RiDeleteBinLine" /> </QueueItemAction> </QueueItemActions> </div> {#if todo.description} <QueueItemDescription>{todo.description}</QueueItemDescription> {/if} </QueueItem> {/each} </QueueList> </QueueSectionContent> </QueueSection> <QueueSection open={false}> <QueueSectionTrigger> <QueueSectionLabel label="Completed" count={done.length} /> </QueueSectionTrigger> <QueueSectionContent> <QueueList> {#each done as todo (todo.id)} <QueueItem> <div class="flex items-start gap-2"> <QueueItemIndicator completed role="button" tabindex={0} aria-label="Mark not completed" class="cursor-pointer" onclick={() => uncomplete(todo)} onkeydown={(e) => e.key === "Enter" && uncomplete(todo)} /> <QueueItemContent completed>{todo.title}</QueueItemContent> </div> {#if todo.id === "d1"} <QueueItemAttachment> <QueueItemFile>quotes.csv</QueueItemFile> </QueueItemAttachment> {/if} </QueueItem> {/each} </QueueList> </QueueSectionContent> </QueueSection></Queue>Built on badge, button, collapsible and scroll-area.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-queuepnpm dlx shadcn@latest add @edmi-ui/ai-queueyarn dlx shadcn@latest add @edmi-ui/ai-queuebunx shadcn@latest add @edmi-ui/ai-queuePick 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,button,collapsible,scroll-area.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-queuepnpm dlx shadcn-vue@latest add @edmi-ui/ai-queueyarn dlx shadcn-vue@latest add @edmi-ui/ai-queuebunx shadcn-vue@latest add @edmi-ui/ai-queuePick 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:
badge,button,collapsible,scroll-area.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-queue.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-queue.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-queue.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-queue.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,button,collapsible,scroll-area.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Queue, QueueList, QueueSection, QueueSectionContent, QueueSectionLabel, QueueSectionTrigger,} from "@/components/ai/queue";
<Queue className="w-full max-w-md"> <QueueSection> <QueueSectionTrigger> <QueueSectionLabel label="Queued" /> </QueueSectionTrigger> <QueueSectionContent> <QueueList> </QueueList> </QueueSectionContent> </QueueSection> <QueueSection defaultOpen={false}> <QueueSectionTrigger> <QueueSectionLabel label="Completed" /> </QueueSectionTrigger> <QueueSectionContent> <QueueList> </QueueList> </QueueSectionContent> </QueueSection></Queue><script setup lang="ts">import { Queue, QueueSection, QueueSectionLabel, QueueSectionTrigger,} from "@/components/ai/queue";</script>
<template> <Queue class="w-full max-w-md"> <QueueSection> <QueueSectionTrigger> <QueueSectionLabel label="Queued" /> </QueueSectionTrigger> <!-- … --> </QueueSection> <!-- … --> </Queue></template><script lang="ts"> import { Queue, QueueSection, QueueSectionLabel, QueueSectionTrigger, } from "$lib/components/ai/queue";</script>
<Queue class="w-full max-w-md"> <QueueSection> <QueueSectionTrigger> <QueueSectionLabel label="Queued" /> </QueueSectionTrigger> <!-- … --> </QueueSection> <!-- … --></Queue>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type { ComponentProps, ReactNode } from "react";import { PaperclipIcon, CaretDownIcon } from "@phosphor-icons/react";import { Badge } from "@/registry/edmi/ui/badge";import { Button } from "@/registry/edmi/ui/button";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/registry/edmi/ui/collapsible";import { ScrollArea } from "@/registry/edmi/ui/scroll-area";
export interface QueueMessagePart { type: string; text?: string; url?: string; filename?: string; mediaType?: string;}
export interface QueueMessage { id: string; parts: QueueMessagePart[];}
export interface QueueTodo { id: string; title: string; description?: string; status?: "pending" | "completed";}
export type QueueItemProps = ComponentProps<"li">;
export const QueueItem = ({ className, ...props }: QueueItemProps) => ( <li data-slot="ai-queue-item" className={cn( "group/queue-item flex flex-col gap-0.5 rounded-[7px] px-2 py-[7px] text-[13.5px] transition-colors hover:bg-accent", className, )} {...props} />);
export type QueueItemIndicatorProps = ComponentProps<"span"> & { completed?: boolean;};
export const QueueItemIndicator = ({ completed = false, className, ...props}: QueueItemIndicatorProps) => ( <span data-slot="ai-queue-item-indicator" className={cn( "mt-0.5 inline-block size-3.5 shrink-0 rounded-full border-[1.5px]", completed ? "border-[color-mix(in_srgb,var(--success)_40%,var(--popover))] bg-success-soft" : "border-muted-foreground-2", className, )} {...props} />);
// QueueItemAvatar: leading slot (avatar, icon) of an item rowexport type QueueItemAvatarProps = ComponentProps<"span">;
export const QueueItemAvatar = ({ className, ...props}: QueueItemAvatarProps) => ( <span data-slot="ai-queue-item-avatar" className={cn("flex shrink-0 items-center", className)} {...props} />);
// QueueItemStatus: trailing slot (age, spinner, status dot) of an item rowexport type QueueItemStatusProps = ComponentProps<"span">;
export const QueueItemStatus = ({ className, ...props}: QueueItemStatusProps) => ( <span data-slot="ai-queue-item-status" className={cn( "flex shrink-0 items-center gap-2 text-xs text-muted-foreground", className, )} {...props} />);
export type QueueItemContentProps = ComponentProps<"span"> & { completed?: boolean;};
export const QueueItemContent = ({ completed = false, className, ...props}: QueueItemContentProps) => ( <span data-slot="ai-queue-item-content" className={cn( "line-clamp-1 grow break-words", completed ? "text-muted-foreground-2 line-through" : "text-foreground", className, )} {...props} />);
export type QueueItemDescriptionProps = ComponentProps<"div"> & { completed?: boolean;};
export const QueueItemDescription = ({ completed = false, className, ...props}: QueueItemDescriptionProps) => ( <div data-slot="ai-queue-item-description" className={cn( "ml-[22px] text-xs", completed ? "text-muted-foreground-2 line-through" : "text-muted-foreground", className, )} {...props} />);
export type QueueItemActionsProps = ComponentProps<"div">;
export const QueueItemActions = ({ className, ...props}: QueueItemActionsProps) => ( <div data-slot="ai-queue-item-actions" className={cn("flex gap-0.5", className)} {...props} />);
export type QueueItemActionProps = Omit< ComponentProps<typeof Button>, "variant" | "size">;
export const QueueItemAction = ({ className, ...props}: QueueItemActionProps) => ( <Button data-slot="ai-queue-item-action" className={cn( "text-muted-foreground opacity-0 transition-opacity group-focus-within/queue-item:opacity-100 group-hover/queue-item:opacity-100 hover:text-foreground focus-visible:opacity-100", className, )} size="icon-xs" type="button" variant="ghost" {...props} />);
export type QueueItemAttachmentProps = ComponentProps<"div">;
export const QueueItemAttachment = ({ className, ...props}: QueueItemAttachmentProps) => ( <div data-slot="ai-queue-item-attachment" className={cn("mt-1 ml-[22px] flex flex-wrap gap-2", className)} {...props} />);
export type QueueItemImageProps = ComponentProps<"img">;
export const QueueItemImage = ({ className, alt = "", ...props}: QueueItemImageProps) => ( <img alt={alt} className={cn( "size-8 rounded-md border border-border object-cover", className, )} height={32} width={32} {...props} />);
export type QueueItemFileProps = ComponentProps<"span">;
export const QueueItemFile = ({ children, className, ...props}: QueueItemFileProps) => ( <span data-slot="ai-queue-item-file" className={cn( "flex items-center gap-1 rounded-md border border-border bg-secondary px-2 py-1 text-xs", className, )} {...props} > <PaperclipIcon className="size-3" /> <span className="max-w-[100px] truncate">{children}</span> </span>);
export type QueueListProps = ComponentProps<typeof ScrollArea>;
export const QueueList = ({ children, className, ...props}: QueueListProps) => ( <ScrollArea data-slot="ai-queue-list" className={cn("mt-0.5", className)} {...props} > <div className="max-h-48"> <ul>{children}</ul> </div> </ScrollArea>);
// QueueSection: collapsible section container; a section that follows another gets the divider.export type QueueSectionProps = ComponentProps<typeof Collapsible>;
export const QueueSection = ({ className, defaultOpen = true, ...props}: QueueSectionProps) => ( <Collapsible data-slot="ai-queue-section" className={cn( "not-first:mt-1.5 not-first:border-t not-first:border-border-2 not-first:pt-1.5", className, )} defaultOpen={defaultOpen} {...props} />);
// QueueSectionTrigger: section headerexport type QueueSectionTriggerProps = ComponentProps< typeof CollapsibleTrigger>;
export const QueueSectionTrigger = ({ children, className, ...props}: QueueSectionTriggerProps) => ( <CollapsibleTrigger data-slot="ai-queue-section-trigger" className={cn( "group/queue-trigger flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] font-semibold text-foreground focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring", className, )} {...props} > {children} </CollapsibleTrigger>);
// QueueSectionLabel: chevron, optional icon, label and a count badgeexport type QueueSectionLabelProps = ComponentProps<"span"> & { count?: number; label: string; icon?: ReactNode; /** ✦ Show the collapse chevron (default true). */ chevron?: boolean;};
export const QueueSectionLabel = ({ count, label, icon, chevron = true, className, ...props}: QueueSectionLabelProps) => ( <span data-slot="ai-queue-section-label" className={cn("flex items-center gap-2", className)} {...props} > {chevron && ( <CaretDownIcon className="size-4 text-muted-foreground transition-transform group-data-[panel-open]/queue-trigger:rotate-180" /> )} {icon} <span>{label}</span> {count !== undefined && ( <Badge className="h-5" shape="number" variant="secondary"> {count} </Badge> )} </span>);
// QueueSectionContent: collapsible content areaexport type QueueSectionContentProps = ComponentProps< typeof CollapsibleContent>;
export const QueueSectionContent = ({ className, ...props}: QueueSectionContentProps) => ( <CollapsibleContent data-slot="ai-queue-section-content" className={cn( "h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0", className, )} {...props} />);
export type QueueProps = ComponentProps<"div"> & { /** ✦ `card` (default) or `flat` (no border, fill or padding). */ variant?: "card" | "flat";};
export const Queue = ({ className, variant = "card", ...props}: QueueProps) => ( <div data-slot="ai-queue" data-variant={variant} className={cn( "not-prose flex flex-col", variant === "card" && "rounded-xl border border-border bg-card p-2", className, )} {...props} />);<script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<{ /** ✦ `card` (default) or `flat` (no border, fill or padding). */ variant?: "card" | "flat" class?: HTMLAttributes["class"]}>(), { variant: "card",})</script>
<template> <div data-slot="ai-queue" :data-variant="props.variant" :class="cn('not-prose flex flex-col', props.variant === 'card' && 'rounded-xl border border-border bg-card p-2', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <li data-slot="ai-queue-item" :class="cn( 'group/queue-item flex flex-col gap-0.5 rounded-[7px] px-2 py-[7px] text-[13.5px] transition-colors hover:bg-accent', props.class, )" > <slot /> </li></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Button } from "@/registry/edmi/ui/button"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <Button data-slot="ai-queue-item-action" type="button" variant="ghost" size="icon-xs" :class="cn( 'text-muted-foreground opacity-0 transition-opacity group-focus-within/queue-item:opacity-100 group-hover/queue-item:opacity-100 hover:text-foreground focus-visible:opacity-100', props.class, )" > <slot /> </Button></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div data-slot="ai-queue-item-actions" :class="cn('flex gap-0.5', props.class)"> <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div data-slot="ai-queue-item-attachment" :class="cn('mt-1 ml-[22px] flex flex-wrap gap-2', props.class)"> <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <span data-slot="ai-queue-item-avatar" :class="cn('flex shrink-0 items-center', props.class)"> <slot /> </span></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<{ completed?: boolean class?: HTMLAttributes["class"]}>(), { completed: false,})</script>
<template> <span data-slot="ai-queue-item-content" :class="cn( 'line-clamp-1 grow break-words', props.completed ? 'text-muted-foreground-2 line-through' : 'text-foreground', props.class, )" > <slot /> </span></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<{ completed?: boolean class?: HTMLAttributes["class"]}>(), { completed: false,})</script>
<template> <div data-slot="ai-queue-item-description" :class="cn( 'ml-[22px] text-xs', props.completed ? 'text-muted-foreground-2 line-through' : 'text-muted-foreground', props.class, )" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { PaperclipIcon } from "@lucide/vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <span data-slot="ai-queue-item-file" :class="cn('flex items-center gap-1 rounded-md border border-border bg-secondary px-2 py-1 text-xs', props.class)" > <PaperclipIcon class="size-3" /> <span class="max-w-[100px] truncate"><slot /></span> </span></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<{ src?: string alt?: string class?: HTMLAttributes["class"]}>(), { alt: "",})</script>
<template> <img data-slot="ai-queue-item-image" :src="src" :alt="alt" width="32" height="32" :class="cn('size-8 rounded-md border border-border object-cover', props.class)" ></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<{ completed?: boolean class?: HTMLAttributes["class"]}>(), { completed: false,})</script>
<template> <span data-slot="ai-queue-item-indicator" :class="cn( 'mt-0.5 inline-block size-3.5 shrink-0 rounded-full border-[1.5px]', props.completed ? 'border-[color-mix(in_srgb,var(--success)_40%,var(--popover))] bg-success-soft' : 'border-muted-foreground-2', props.class, )" /></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <span data-slot="ai-queue-item-status" :class="cn('flex shrink-0 items-center gap-2 text-xs text-muted-foreground', props.class)"> <slot /> </span></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { ScrollArea } from "@/registry/edmi/ui/scroll-area"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <ScrollArea data-slot="ai-queue-list" :class="cn('mt-0.5', props.class)"> <div class="max-h-48"> <ul> <slot /> </ul> </div> </ScrollArea></template><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 QueueSectionProps extends /* @vue-ignore */ CollapsibleRootProps { class?: HTMLAttributes["class"]}
const props = defineProps<QueueSectionProps>()</script>
<template> <!-- Collapsible props (open, update:open) fall through as attrs; defaultOpen is true unless overridden. A section that follows another gets the divider. --> <Collapsible :default-open="true" data-slot="ai-queue-section" :class="cn('not-first:mt-1.5 not-first:border-t not-first:border-border-2 not-first:pt-1.5', 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-queue-section-content" :class="cn( 'overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down', props.class, )" > <slot /> </CollapsibleContent></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { ChevronDownIcon } from "@lucide/vue"import { cn } from "@/registry/edmi/lib/utils"import { Badge } from "@/registry/edmi/ui/badge"
const props = withDefaults(defineProps<{ label: string count?: number /** ✦ Show the collapse chevron (default true). */ chevron?: boolean class?: HTMLAttributes["class"]}>(), { chevron: true,})</script>
<template> <span data-slot="ai-queue-section-label" :class="cn('flex items-center gap-2', props.class)"> <ChevronDownIcon v-if="props.chevron" class="size-4 text-muted-foreground transition-transform group-data-[state=open]/queue-trigger:rotate-180" /> <slot name="icon" /> <span>{{ label }}</span> <Badge v-if="count !== undefined" shape="number" variant="secondary" class="h-5">{{ count }}</Badge> </span></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { CollapsibleTrigger } from "@/registry/edmi/ui/collapsible"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <CollapsibleTrigger data-slot="ai-queue-section-trigger" :class="cn( 'group/queue-trigger flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] font-semibold text-foreground focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring', props.class, )" > <slot /> </CollapsibleTrigger></template>export interface QueueMessagePart { type: string; text?: string; url?: string; filename?: string; mediaType?: string;}
export interface QueueMessage { id: string; parts: QueueMessagePart[];}
export interface QueueTodo { id: string; title: string; description?: string; status?: "pending" | "completed";}export { default as Queue } from "./Queue.vue";export { default as QueueItem } from "./QueueItem.vue";export { default as QueueItemAction } from "./QueueItemAction.vue";export { default as QueueItemActions } from "./QueueItemActions.vue";export { default as QueueItemAttachment } from "./QueueItemAttachment.vue";export { default as QueueItemAvatar } from "./QueueItemAvatar.vue";export { default as QueueItemContent } from "./QueueItemContent.vue";export { default as QueueItemDescription } from "./QueueItemDescription.vue";export { default as QueueItemFile } from "./QueueItemFile.vue";export { default as QueueItemImage } from "./QueueItemImage.vue";export { default as QueueItemIndicator } from "./QueueItemIndicator.vue";export { default as QueueItemStatus } from "./QueueItemStatus.vue";export { default as QueueList } from "./QueueList.vue";export { default as QueueSection } from "./QueueSection.vue";export { default as QueueSectionContent } from "./QueueSectionContent.vue";export { default as QueueSectionLabel } from "./QueueSectionLabel.vue";export { default as QueueSectionTrigger } from "./QueueSectionTrigger.vue";export type { QueueMessage, QueueMessagePart, QueueTodo } from "./types";import Root from "./queue.svelte";import Item from "./queue-item.svelte";import ItemAction from "./queue-item-action.svelte";import ItemActions from "./queue-item-actions.svelte";import ItemAttachment from "./queue-item-attachment.svelte";import ItemAvatar from "./queue-item-avatar.svelte";import ItemContent from "./queue-item-content.svelte";import ItemDescription from "./queue-item-description.svelte";import ItemFile from "./queue-item-file.svelte";import ItemImage from "./queue-item-image.svelte";import ItemIndicator from "./queue-item-indicator.svelte";import ItemStatus from "./queue-item-status.svelte";import List from "./queue-list.svelte";import Section from "./queue-section.svelte";import SectionContent from "./queue-section-content.svelte";import SectionLabel from "./queue-section-label.svelte";import SectionTrigger from "./queue-section-trigger.svelte";
export type { QueueMessage, QueueMessagePart, QueueTodo } from "./types.js";export { Item, Item as QueueItem, ItemAction, ItemAction as QueueItemAction, ItemActions, ItemActions as QueueItemActions, ItemAttachment, ItemAttachment as QueueItemAttachment, ItemAvatar, ItemAvatar as QueueItemAvatar, ItemContent, ItemContent as QueueItemContent, ItemDescription, ItemDescription as QueueItemDescription, ItemFile, ItemFile as QueueItemFile, ItemImage, ItemImage as QueueItemImage, ItemIndicator, ItemIndicator as QueueItemIndicator, ItemStatus, ItemStatus as QueueItemStatus, List, List as QueueList, Root, // Root as Queue, Section, Section as QueueSection, SectionContent, SectionContent as QueueSectionContent, SectionLabel, SectionLabel as QueueSectionLabel, SectionTrigger, SectionTrigger as QueueSectionTrigger,};<script lang="ts"> import { cn } from "$lib/utils.js"; import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js";
let { class: className, children, ...restProps }: Omit<ButtonProps, "variant" | "size"> = $props();</script>
<Button data-slot="ai-queue-item-action" type="button" variant="ghost" size="icon-xs" class={cn( "text-muted-foreground opacity-0 transition-opacity group-focus-within/queue-item:opacity-100 group-hover/queue-item:opacity-100 hover:text-foreground focus-visible:opacity-100", className )} {...restProps}> {@render children?.()}</Button><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();</script>
<div data-slot="ai-queue-item-actions" class={cn("flex gap-0.5", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();</script>
<div data-slot="ai-queue-item-attachment" class={cn("mt-1 ml-[22px] flex flex-wrap gap-2", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> 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-queue-item-avatar" class={cn("flex shrink-0 items-center", className)} {...restProps}> {@render children?.()}</span><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, completed = false, children, ...restProps }: HTMLAttributes<HTMLSpanElement> & { completed?: boolean } = $props();</script>
<span data-slot="ai-queue-item-content" class={cn( "line-clamp-1 grow break-words", completed ? "text-muted-foreground-2 line-through" : "text-foreground", className )} {...restProps}> {@render children?.()}</span><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, completed = false, children, ...restProps }: HTMLAttributes<HTMLDivElement> & { completed?: boolean } = $props();</script>
<div data-slot="ai-queue-item-description" class={cn( "ml-[22px] text-xs", completed ? "text-muted-foreground-2 line-through" : "text-muted-foreground", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import PaperclipIcon from "phosphor-svelte/lib/Paperclip"; 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-queue-item-file" class={cn( "flex items-center gap-1 rounded-md border border-border bg-secondary px-2 py-1 text-xs", className )} {...restProps}> <PaperclipIcon class="size-3" /> <span class="max-w-[100px] truncate">{@render children?.()}</span></span><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLImgAttributes } from "svelte/elements";
let { class: className, alt = "", ...restProps }: HTMLImgAttributes = $props();</script>
<img data-slot="ai-queue-item-image" {alt} width="32" height="32" class={cn("size-8 rounded-md border border-border object-cover", className)} {...restProps}/><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, completed = false, ...restProps }: HTMLAttributes<HTMLSpanElement> & { completed?: boolean } = $props();</script>
<span data-slot="ai-queue-item-indicator" class={cn( "mt-0.5 inline-block size-3.5 shrink-0 rounded-full border-[1.5px]", completed ? "border-[color-mix(in_srgb,var(--success)_40%,var(--popover))] bg-success-soft" : "border-muted-foreground-2", className )} {...restProps}></span><script lang="ts"> 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-queue-item-status" class={cn("flex shrink-0 items-center gap-2 text-xs text-muted-foreground", className)} {...restProps}> {@render children?.()}</span><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLLiAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLLiAttributes = $props();</script>
<li data-slot="ai-queue-item" class={cn( "group/queue-item flex flex-col gap-0.5 rounded-[7px] px-2 py-[7px] text-[13.5px] transition-colors hover:bg-accent", className )} {...restProps}> {@render children?.()}</li><script lang="ts"> import { cn } from "$lib/utils.js"; import { ScrollArea } from "$lib/registry/ui/scroll-area/index.js"; import type { ComponentProps } from "svelte";
let { class: className, children, ...restProps }: ComponentProps<typeof ScrollArea> = $props();</script>
<ScrollArea data-slot="ai-queue-list" class={cn("mt-0.5", className)} {...restProps}> <div class="max-h-48"> <ul> {@render children?.()} </ul> </div></ScrollArea><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-queue-section-content" class={cn( "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", className )} {...restProps}> {@render children?.()}</CollapsibleContent><script lang="ts"> import CaretDownIcon from "phosphor-svelte/lib/CaretDown"; import { cn } from "$lib/utils.js"; import { Badge } from "$lib/registry/ui/badge/index.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, label, count, icon, chevron = true, ...restProps }: Omit<HTMLAttributes<HTMLSpanElement>, "children"> & { label: string; count?: number; icon?: Snippet; chevron?: boolean; } = $props();</script>
<span data-slot="ai-queue-section-label" class={cn("flex items-center gap-2", className)} {...restProps}> {#if chevron} <CaretDownIcon class="size-4 text-muted-foreground transition-transform group-data-[state=open]/queue-trigger:rotate-180" /> {/if} {@render icon?.()} <span>{label}</span> {#if count !== undefined} <Badge shape="number" variant="secondary" class="h-5">{count}</Badge> {/if}</span><script lang="ts"> import { cn } from "$lib/utils.js"; import { CollapsibleTrigger } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte";
let { class: className, children, ...restProps }: ComponentProps<typeof CollapsibleTrigger> = $props();</script>
<CollapsibleTrigger data-slot="ai-queue-section-trigger" class={cn( "group/queue-trigger flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] font-semibold text-foreground focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring", className )} {...restProps}> {@render children?.()}</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>
<!-- A section that follows another gets the divider. --><Collapsible bind:open data-slot="ai-queue-section" class={cn("not-first:mt-1.5 not-first:border-t not-first:border-border-2 not-first:pt-1.5", className)} {...restProps}> {@render children?.()}</Collapsible><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, variant = "card", children, ...restProps }: HTMLAttributes<HTMLDivElement> & { variant?: "card" | "flat" } = $props();</script>
<div data-slot="ai-queue" data-variant={variant} class={cn("not-prose flex flex-col", variant === "card" && "rounded-xl border border-border bg-card p-2", className)} {...restProps}> {@render children?.()}</div>export interface QueueMessagePart { type: string; text?: string; url?: string; filename?: string; mediaType?: string;}
export interface QueueMessage { id: string; parts: QueueMessagePart[];}
export interface QueueTodo { id: string; title: string; description?: string; status?: "pending" | "completed";}| Part | Prop | Type | Default |
|---|---|---|---|
| QueueSection | open, defaultOpen |
collapsible state | defaultOpen = true |
| Queue | variant ✦ |
"card" | "flat" |
"card" |
| QueueSectionLabel | label, count, icon, chevron ✦ |
label, number badge, optional icon, collapse chevron | chevron = true |
| QueueItemAvatar, QueueItemStatus ✦ | leading and trailing slots of an item row | ||
| QueueItemIndicator, QueueItemContent, QueueItemDescription | completed |
boolean |
false |
| QueueItemAction | Button props | ghost icon-xs, revealed on hover or focus |
|
| QueueItemImage, QueueItemFile | attachment thumbnails |
Flat ✦
Section titled “Flat ✦”variant="flat" drops the card border, fill and padding (the AI home board). Hide the chevron with chevron={false} on QueueSectionLabel, and use QueueItemAvatar / QueueItemStatus for the leading avatar and trailing age or status.
- Rebalance MAG4 weights 2m
- Summarize Q3 filings 14m
- Compare fees with SPYx 1h
import { AgentAvatar } from "@/components/ai/agent-avatar";import { Queue, QueueItem, QueueItemAvatar, QueueItemContent, QueueItemStatus, QueueList, QueueSection, QueueSectionContent, QueueSectionLabel, QueueSectionTrigger,} from "@/components/ai/queue";import { Spinner } from "@/components/ui/spinner";
const tasks = [ { id: "1", agent: "keeper", title: "Rebalance MAG4 weights", age: "2m", running: true, }, { id: "2", agent: "scout", title: "Summarize Q3 filings", age: "14m", running: false, }, { id: "3", agent: "quote", title: "Compare fees with SPYx", age: "1h", running: false, },];
export default function Demo() { return ( <Queue variant="flat" className="w-full max-w-md"> <QueueSection> <QueueSectionTrigger className="font-normal text-muted-foreground"> <QueueSectionLabel label="Tasks" chevron={false} /> </QueueSectionTrigger> <QueueSectionContent> <QueueList> {tasks.map((task) => ( <QueueItem key={task.id}> <div className="flex items-center gap-2"> <QueueItemAvatar> <AgentAvatar seed={task.agent} size={22} tile={false} /> </QueueItemAvatar> <QueueItemContent>{task.title}</QueueItemContent> <QueueItemStatus> <span className="font-mono">{task.age}</span> {task.running ? ( <Spinner className="size-3.5" /> ) : ( <span className="size-1.5 rounded-full bg-success" /> )} </QueueItemStatus> </div> </QueueItem> ))} </QueueList> </QueueSectionContent> </QueueSection> </Queue> );}<script setup lang="ts">import { AgentAvatar } from "@/components/ai/agent-avatar";import { Queue, QueueItem, QueueItemAvatar, QueueItemContent, QueueItemStatus, QueueList, QueueSection, QueueSectionContent, QueueSectionLabel, QueueSectionTrigger,} from "@/components/ai/queue";import { Spinner } from "@/components/ui/spinner";
const tasks = [ { id: "1", agent: "keeper", title: "Rebalance MAG4 weights", age: "2m", running: true }, { id: "2", agent: "scout", title: "Summarize Q3 filings", age: "14m", running: false }, { id: "3", agent: "quote", title: "Compare fees with SPYx", age: "1h", running: false },];</script>
<template> <Queue variant="flat" class="w-full max-w-md"> <QueueSection> <QueueSectionTrigger class="font-normal text-muted-foreground"> <QueueSectionLabel label="Tasks" :chevron="false" /> </QueueSectionTrigger> <QueueSectionContent> <QueueList> <QueueItem v-for="task in tasks" :key="task.id"> <div class="flex items-center gap-2"> <QueueItemAvatar> <AgentAvatar :seed="task.agent" :size="22" :tile="false" /> </QueueItemAvatar> <QueueItemContent>{{ task.title }}</QueueItemContent> <QueueItemStatus> <span class="font-mono">{{ task.age }}</span> <Spinner v-if="task.running" class="size-3.5" /> <span v-else class="size-1.5 rounded-full bg-success" /> </QueueItemStatus> </div> </QueueItem> </QueueList> </QueueSectionContent> </QueueSection> </Queue></template><script lang="ts"> import { AgentAvatar } from "$lib/components/ai/agent-avatar"; import { Queue, QueueItem, QueueItemAvatar, QueueItemContent, QueueItemStatus, QueueList, QueueSection, QueueSectionContent, QueueSectionLabel, QueueSectionTrigger, } from "$lib/components/ai/queue"; import { Spinner } from "$lib/components/ui/spinner";
const tasks = [ { id: "1", agent: "keeper", title: "Rebalance MAG4 weights", age: "2m", running: true }, { id: "2", agent: "scout", title: "Summarize Q3 filings", age: "14m", running: false }, { id: "3", agent: "quote", title: "Compare fees with SPYx", age: "1h", running: false }, ];</script>
<Queue variant="flat" class="w-full max-w-md"> <QueueSection> <QueueSectionTrigger class="font-normal text-muted-foreground"> <QueueSectionLabel label="Tasks" chevron={false} /> </QueueSectionTrigger> <QueueSectionContent> <QueueList> {#each tasks as task (task.id)} <QueueItem> <div class="flex items-center gap-2"> <QueueItemAvatar> <AgentAvatar seed={task.agent} size={22} tile={false} /> </QueueItemAvatar> <QueueItemContent>{task.title}</QueueItemContent> <QueueItemStatus> <span class="font-mono">{task.age}</span> {#if task.running} <Spinner class="size-3.5" /> {:else} <span class="size-1.5 rounded-full bg-success"></span> {/if} </QueueItemStatus> </div> </QueueItem> {/each} </QueueList> </QueueSectionContent> </QueueSection></Queue>✦ Edmi additions
Section titled “✦ Edmi additions”- The count is a number badge next to the label, and a section that follows another gets a divider.
- Item actions appear on hover and keyboard focus.
- The types
QueueMessageandQueueTodoare exported for AI SDK data.