Chat Header
Conversation header: title, agent or model, share and more actions.
import { ChatHeader, ChatHeaderActions, ChatHeaderMenu, ChatHeaderProject, ChatHeaderShare, ChatHeaderTitle,} from "@/components/ai/chat-header";import { Button } from "@/components/ui/button";import { DropdownMenuItem } from "@/components/ui/dropdown-menu";import { IconPlaceholder } from "@/edmi/icon-placeholder";
export default function Demo() { return ( <ChatHeader className="max-w-3xl"> <ChatHeaderTitle> <span className="truncate">Keeper starter plan</span> <ChatHeaderProject status="connected" /> <ChatHeaderMenu> <DropdownMenuItem>Rename</DropdownMenuItem> <DropdownMenuItem>Move to project</DropdownMenuItem> <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem> </ChatHeaderMenu> </ChatHeaderTitle> <ChatHeaderActions> <Button aria-label="Web access" size="icon-sm" type="button" variant="ghost" > <IconPlaceholder lucide="GlobeIcon" tabler="IconWorld" hugeicons="Globe02Icon" phosphor="GlobeIcon" remixicon="RiGlobalLine" className="size-4" /> </Button> <Button aria-label="1 attached file" size="sm" type="button" variant="ghost" > <IconPlaceholder lucide="FileTextIcon" tabler="IconFileDescription" hugeicons="File01Icon" phosphor="FileTextIcon" remixicon="RiFileTextLine" className="size-3.5" /> 1 </Button> <ChatHeaderShare /> </ChatHeaderActions> </ChatHeader> );}<script setup lang="ts">import { FileTextIcon, GlobeIcon } from "@lucide/vue";import { ChatHeader, ChatHeaderActions, ChatHeaderMenu, ChatHeaderProject, ChatHeaderShare, ChatHeaderTitle,} from "@/components/ai/chat-header";import { Button } from "@/components/ui/button";import { DropdownMenuItem } from "@/components/ui/dropdown-menu";</script>
<template> <ChatHeader class="max-w-3xl"> <ChatHeaderTitle> <span class="truncate">Keeper starter plan</span> <ChatHeaderProject status="connected" /> <ChatHeaderMenu> <DropdownMenuItem>Rename</DropdownMenuItem> <DropdownMenuItem>Move to project</DropdownMenuItem> <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem> </ChatHeaderMenu> </ChatHeaderTitle> <ChatHeaderActions> <Button aria-label="Web access" size="icon-sm" type="button" variant="ghost"> <GlobeIcon class="size-4" /> </Button> <Button aria-label="1 attached file" size="sm" type="button" variant="ghost"> <FileTextIcon class="size-3.5" /> 1 </Button> <ChatHeaderShare /> </ChatHeaderActions> </ChatHeader></template><script lang="ts"> import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte"; import { ChatHeader, ChatHeaderActions, ChatHeaderMenu, ChatHeaderProject, ChatHeaderShare, ChatHeaderTitle, } from "$lib/components/ai/chat-header"; import { Button } from "$lib/components/ui/button"; import { DropdownMenuItem } from "$lib/components/ui/dropdown-menu";</script>
<ChatHeader class="max-w-3xl"> <ChatHeaderTitle> <span class="truncate">Keeper starter plan</span> <ChatHeaderProject status="connected" /> <ChatHeaderMenu> <DropdownMenuItem>Rename</DropdownMenuItem> <DropdownMenuItem>Move to project</DropdownMenuItem> <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem> </ChatHeaderMenu> </ChatHeaderTitle> <ChatHeaderActions> <Button aria-label="Web access" size="icon-sm" type="button" variant="ghost"> <IconPlaceholder lucide="GlobeIcon" tabler="IconWorld" hugeicons="Globe02Icon" phosphor="GlobeIcon" remixicon="RiGlobalLine" class="size-4" /> </Button> <Button aria-label="1 attached file" size="sm" type="button" variant="ghost"> <IconPlaceholder lucide="FileTextIcon" tabler="IconFileDescription" hugeicons="File01Icon" phosphor="FileTextIcon" remixicon="RiFileTextLine" class="size-3.5" /> 1 </Button> <ChatHeaderShare /> </ChatHeaderActions></ChatHeader>Edmi ✦ addition. Built on button and dropdown-menu.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-chat-headerpnpm dlx shadcn@latest add @edmi-ui/ai-chat-headeryarn dlx shadcn@latest add @edmi-ui/ai-chat-headerbunx shadcn@latest add @edmi-ui/ai-chat-headerPick 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:
button,dropdown-menu.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-chat-headerpnpm dlx shadcn-vue@latest add @edmi-ui/ai-chat-headeryarn dlx shadcn-vue@latest add @edmi-ui/ai-chat-headerbunx shadcn-vue@latest add @edmi-ui/ai-chat-headerPick 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/vueTerminal window pnpm add @lucide/vueTerminal window yarn add @lucide/vueTerminal window bun add @lucide/vueAdd the Edmi components this one builds on:
button,dropdown-menu.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-chat-header.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-chat-header.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-chat-header.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-chat-header.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:
button,dropdown-menu.Copy the code from Component source into your project and update the import paths to match your aliases.
import { ChatHeader, ChatHeaderMenu, ChatHeaderProject, ChatHeaderTitle,} from "@/components/ai/chat-header";import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
<ChatHeader className="max-w-3xl"> <ChatHeaderTitle> <span className="truncate">Keeper starter plan</span> <ChatHeaderProject status="connected" /> <ChatHeaderMenu> <DropdownMenuItem>Rename</DropdownMenuItem> <DropdownMenuItem>Move to project</DropdownMenuItem> <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem> </ChatHeaderMenu> </ChatHeaderTitle> {/* … */}</ChatHeader><script setup lang="ts">import { ChatHeader, ChatHeaderActions, ChatHeaderMenu, ChatHeaderProject, ChatHeaderShare, ChatHeaderTitle,} from "@/components/ai/chat-header";import { Button } from "@/components/ui/button";import { DropdownMenuItem } from "@/components/ui/dropdown-menu";</script>
<template> <ChatHeader class="max-w-3xl"> <ChatHeaderTitle> <span class="truncate">Keeper starter plan</span> <ChatHeaderProject status="connected" /> <ChatHeaderMenu> <DropdownMenuItem>Rename</DropdownMenuItem> <DropdownMenuItem>Move to project</DropdownMenuItem> <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem> </ChatHeaderMenu> </ChatHeaderTitle> <ChatHeaderActions> <Button aria-label="Web access" size="icon-sm" type="button" variant="ghost"> <GlobeIcon class="size-4" /> </Button> <Button aria-label="1 attached file" size="sm" type="button" variant="ghost"> <FileTextIcon class="size-3.5" /> 1 </Button> <ChatHeaderShare /> </ChatHeaderActions> </ChatHeader></template><script lang="ts"> import { ChatHeader, ChatHeaderActions, ChatHeaderMenu, ChatHeaderProject, ChatHeaderShare, ChatHeaderTitle, } from "$lib/components/ai/chat-header"; import { Button } from "$lib/components/ui/button"; import { DropdownMenuItem } from "$lib/components/ui/dropdown-menu";</script>
<ChatHeader class="max-w-3xl"> <ChatHeaderTitle> <span class="truncate">Keeper starter plan</span> <ChatHeaderProject status="connected" /> <ChatHeaderMenu> <DropdownMenuItem>Rename</DropdownMenuItem> <DropdownMenuItem>Move to project</DropdownMenuItem> <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem> </ChatHeaderMenu> </ChatHeaderTitle> <ChatHeaderActions> <Button aria-label="Web access" size="icon-sm" type="button" variant="ghost"> <GlobeIcon class="size-4" /> </Button> <Button aria-label="1 attached file" size="sm" type="button" variant="ghost"> <FileTextIcon class="size-3.5" /> 1 </Button> <ChatHeaderShare /> </ChatHeaderActions></ChatHeader>Component source
Section titled “Component source”import { cn } from "cn";import type { ComponentProps, ReactNode } from "react";import { HardDrivesIcon, CaretDownIcon } from "@phosphor-icons/react";import { Button } from "@/registry/edmi/ui/button";import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger,} from "@/registry/edmi/ui/dropdown-menu";
export type ChatHeaderProps = ComponentProps<"header">;
/** Thread bar: title, project indicator and actions. */export const ChatHeader = ({ className, ...props }: ChatHeaderProps) => ( <header data-slot="ai-chat-header" className={cn( "flex h-[52px] w-full items-center justify-between gap-3 rounded-[calc(var(--radius)*1.2)] border border-border bg-background pr-2.5 pl-4", className, )} {...props} />);
export type ChatHeaderTitleProps = ComponentProps<"div">;
/** Title with its indicators; put `ChatHeaderProject` and `ChatHeaderMenu` inside. */export const ChatHeaderTitle = ({ className, ...props}: ChatHeaderTitleProps) => ( <div data-slot="ai-chat-header-title" className={cn("flex min-w-0 items-center gap-2.5 text-[14.5px]", className)} {...props} />);
export type ChatHeaderProjectProps = ComponentProps<"span"> & { /** `connected` shows the green dot. */ status?: "connected" | "idle"; icon?: ReactNode;};
/** Project icon with a status dot (dot = connected). */export const ChatHeaderProject = ({ status = "connected", icon, className, ...props}: ChatHeaderProjectProps) => ( <span data-slot="ai-chat-header-project" data-status={status} className={cn("relative inline-flex text-muted-foreground", className)} {...props} > {icon ?? ( <HardDrivesIcon className="size-4" /> )} {status === "connected" && ( <span className="absolute -top-px -right-0.5 size-1.5 rounded-full bg-success" /> )} </span>);
export type ChatHeaderMenuProps = ComponentProps<typeof DropdownMenuContent>;
/** Chevron after the title; children are the `DropdownMenuItem`s. */export const ChatHeaderMenu = ({ children, ...props }: ChatHeaderMenuProps) => ( <DropdownMenu> <DropdownMenuTrigger render={ <Button aria-label="Conversation menu" size="icon-xs" type="button" variant="ghost" /> } > <CaretDownIcon className="size-4" /> </DropdownMenuTrigger> <DropdownMenuContent {...props}>{children}</DropdownMenuContent> </DropdownMenu>);
export type ChatHeaderActionsProps = ComponentProps<"div">;
export const ChatHeaderActions = ({ className, ...props}: ChatHeaderActionsProps) => ( <div data-slot="ai-chat-header-actions" className={cn("flex shrink-0 items-center gap-1", className)} {...props} />);
export type ChatHeaderShareProps = ComponentProps<typeof Button>;
export const ChatHeaderShare = ({ children = "Share", ...props}: ChatHeaderShareProps) => ( <Button size="sm" type="button" variant="secondary" {...props}> {children} </Button>);<script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <!-- Thread bar: title, project indicator and actions. --> <header data-slot="ai-chat-header" :class="cn('flex h-[52px] w-full items-center justify-between gap-3 rounded-[calc(var(--radius)*1.2)] border border-border bg-background pr-2.5 pl-4', props.class)" > <slot /> </header></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-chat-header-actions" :class="cn('flex shrink-0 items-center gap-1', props.class)"> <slot /> </div></template><script setup lang="ts">import { ChevronDown } from "@lucide/vue"import { Button } from "@/registry/edmi/ui/button"import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger,} from "@/registry/edmi/ui/dropdown-menu"</script>
<template> <!-- Chevron after the title; the default slot holds the `DropdownMenuItem`s. --> <DropdownMenu> <DropdownMenuTrigger as-child> <Button aria-label="Conversation menu" size="icon-xs" type="button" variant="ghost"> <ChevronDown class="size-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="start"> <slot /> </DropdownMenuContent> </DropdownMenu></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { ServerIcon } from "@lucide/vue"import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"] /** `connected` shows the green dot. */ status?: "connected" | "idle"}>(), { status: "connected",})</script>
<template> <!-- Project icon with a status dot (dot = connected). The icon slot replaces the default. --> <span data-slot="ai-chat-header-project" :data-status="status" :class="cn('relative inline-flex text-muted-foreground', props.class)" > <slot> <ServerIcon class="size-4" /> </slot> <span v-if="status === 'connected'" class="absolute -top-px -right-0.5 size-1.5 rounded-full bg-success" /> </span></template><script setup lang="ts">import { Button } from "@/registry/edmi/ui/button"</script>
<template> <Button size="sm" type="button" variant="secondary"> <slot>Share</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> <!-- Title with its indicators; put `ChatHeaderProject` and `ChatHeaderMenu` inside. --> <div data-slot="ai-chat-header-title" :class="cn('flex min-w-0 items-center gap-2.5 text-[14.5px]', props.class)"> <slot /> </div></template>export { default as ChatHeader } from "./ChatHeader.vue";export { default as ChatHeaderActions } from "./ChatHeaderActions.vue";export { default as ChatHeaderMenu } from "./ChatHeaderMenu.vue";export { default as ChatHeaderProject } from "./ChatHeaderProject.vue";export { default as ChatHeaderShare } from "./ChatHeaderShare.vue";export { default as ChatHeaderTitle } from "./ChatHeaderTitle.vue";<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> = $props();</script>
<div bind:this={ref} data-slot="ai-chat-header-actions" class={cn("flex shrink-0 items-center gap-1", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import CaretDownIcon from "phosphor-svelte/lib/CaretDown"; import { Button } from "$lib/registry/ui/button/index.js"; import * as DropdownMenu from "$lib/registry/ui/dropdown-menu/index.js"; import type { ComponentProps } from "svelte";
let { children, ...restProps }: ComponentProps<typeof DropdownMenu.Content> = $props();</script>
<!-- Chevron after the title; children are the `DropdownMenu.Item`s. --><DropdownMenu.Root> <DropdownMenu.Trigger> {#snippet child({ props })} <Button {...props} aria-label="Conversation menu" size="icon-xs" type="button" variant="ghost"> <CaretDownIcon class="size-4" /> </Button> {/snippet} </DropdownMenu.Trigger> <DropdownMenu.Content {...restProps}> {@render children?.()} </DropdownMenu.Content></DropdownMenu.Root><script lang="ts"> import HardDrivesIcon from "phosphor-svelte/lib/HardDrives"; import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), status = "connected", class: className, children, ...restProps }: WithElementRef<Omit<HTMLAttributes<HTMLSpanElement>, "children">, HTMLSpanElement> & { /** `connected` shows the green dot. */ status?: "connected" | "idle"; /** Replaces the default project icon. */ children?: Snippet; } = $props();</script>
<!-- Project icon with a status dot (dot = connected). --><span bind:this={ref} data-slot="ai-chat-header-project" data-status={status} class={cn("relative inline-flex text-muted-foreground", className)} {...restProps}> {#if children} {@render children()} {:else} <HardDrivesIcon class="size-4" /> {/if} {#if status === "connected"} <span class="absolute -top-px -right-0.5 size-1.5 rounded-full bg-success"></span> {/if}</span><script lang="ts"> import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js";
let { ref = $bindable(null), children, ...restProps }: Omit<ButtonProps, "href"> = $props();</script>
<Button bind:ref size="sm" type="button" variant="secondary" {...restProps}> {#if children} {@render children()} {:else} Share {/if}</Button><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> = $props();</script>
<!-- Title with its indicators; put `ChatHeaderProject` and `ChatHeaderMenu` inside. --><div bind:this={ref} data-slot="ai-chat-header-title" class={cn("flex min-w-0 items-center gap-2.5 text-[14.5px]", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();</script>
<!-- Thread bar: title, project indicator and actions. --><header bind:this={ref} data-slot="ai-chat-header" class={cn( "flex h-[52px] w-full items-center justify-between gap-3 rounded-[calc(var(--radius)*1.2)] border border-border bg-background pr-2.5 pl-4", className )} {...restProps}> {@render children?.()}</header>import ChatHeader from "./chat-header.svelte";import ChatHeaderActions from "./chat-header-actions.svelte";import ChatHeaderMenu from "./chat-header-menu.svelte";import ChatHeaderProject from "./chat-header-project.svelte";import ChatHeaderShare from "./chat-header-share.svelte";import ChatHeaderTitle from "./chat-header-title.svelte";
export { ChatHeader, ChatHeaderActions, ChatHeaderMenu, ChatHeaderProject, ChatHeaderShare, ChatHeaderTitle,};| Part | Prop | Type | Default |
|---|---|---|---|
| ChatHeader | header props |
||
| ChatHeaderTitle | title row | ||
| ChatHeaderProject | status, icon |
connected | idle, ReactNode (Vue: default slot, Svelte: children snippet) |
connected |
| ChatHeaderMenu | DropdownMenuContent props | chevron with the DropdownMenuItems as children |
|
| ChatHeaderActions | right side | ||
| ChatHeaderShare | Button props | Share |
Elevation ✦
Section titled “Elevation ✦”Flat only. The header is a bar with a border; no elevation prop.
✦ Edmi additions
Section titled “✦ Edmi additions”- Title with a project indicator (dot = connected), a menu chevron, and actions on the right.