Skip to content
edmi-ui

Queue

Queued messages and todos in collapsible sections with status dots.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
  • Summarize Q3 filings
    For NVDAx, MSFTx
  • Draft a feed post about MAG4
  • Compare fees with SPYx
  • Summarize Q3 filings
    For NVDAx, MSFTx
  • Draft a feed post about MAG4
  • Compare fees with SPYx

Built on badge, button, collapsible and scroll-area.

Terminal window
npx shadcn@latest add @edmi-ui/ai-queue
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/ai-queue
Terminal window
yarn dlx shadcn@latest add @edmi-ui/ai-queue
Terminal window
bunx shadcn@latest add @edmi-ui/ai-queue

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/ai-queue
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/ai-queue
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/ai-queue
Terminal window
bunx shadcn-vue@latest add @edmi-ui/ai-queue

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/ai-queue.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-queue.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-queue.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-queue.json

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

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>
registry/components/ai/queue.tsx
"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 row
export 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 row
export 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 header
export 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 badge
export 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 area
export 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}
/>
);
registry/components/ai/queue/Queue.vue
<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>
registry/components/ai/queue/QueueItem.vue
<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>
registry/components/ai/queue/QueueItemAction.vue
<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>
registry/components/ai/queue/QueueItemActions.vue
<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>
registry/components/ai/queue/QueueItemAttachment.vue
<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>
registry/components/ai/queue/QueueItemAvatar.vue
<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>
registry/components/ai/queue/QueueItemContent.vue
<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>
registry/components/ai/queue/QueueItemDescription.vue
<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>
registry/components/ai/queue/QueueItemFile.vue
<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>
registry/components/ai/queue/QueueItemImage.vue
<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>
registry/components/ai/queue/QueueItemIndicator.vue
<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>
registry/components/ai/queue/QueueItemStatus.vue
<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>
registry/components/ai/queue/QueueList.vue
<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>
registry/components/ai/queue/QueueSection.vue
<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>
registry/components/ai/queue/QueueSectionContent.vue
<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>
registry/components/ai/queue/QueueSectionLabel.vue
<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>
registry/components/ai/queue/QueueSectionTrigger.vue
<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>
registry/components/ai/queue/types.ts
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";
}
registry/components/ai/queue/index.ts
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";
src/lib/registry/ai/queue/index.ts
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,
};
src/lib/registry/ai/queue/queue-item-action.svelte
<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>
src/lib/registry/ai/queue/queue-item-actions.svelte
<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>
src/lib/registry/ai/queue/queue-item-attachment.svelte
<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>
src/lib/registry/ai/queue/queue-item-avatar.svelte
<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>
src/lib/registry/ai/queue/queue-item-content.svelte
<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>
src/lib/registry/ai/queue/queue-item-description.svelte
<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>
src/lib/registry/ai/queue/queue-item-file.svelte
<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>
src/lib/registry/ai/queue/queue-item-image.svelte
<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}
/>
src/lib/registry/ai/queue/queue-item-indicator.svelte
<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>
src/lib/registry/ai/queue/queue-item-status.svelte
<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>
src/lib/registry/ai/queue/queue-item.svelte
<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>
src/lib/registry/ai/queue/queue-list.svelte
<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>
src/lib/registry/ai/queue/queue-section-content.svelte
<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>
src/lib/registry/ai/queue/queue-section-label.svelte
<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>
src/lib/registry/ai/queue/queue-section-trigger.svelte
<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>
src/lib/registry/ai/queue/queue-section.svelte
<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>
src/lib/registry/ai/queue/queue.svelte
<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>
src/lib/registry/ai/queue/types.ts
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

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.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
  • Rebalance MAG4 weights2m
  • Summarize Q3 filings14m
  • Compare fees with SPYx1h
  • Rebalance MAG4 weights 2m
  • Summarize Q3 filings 14m
  • Compare fees with SPYx 1h
  • 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 QueueMessage and QueueTodo are exported for AI SDK data.