Session Panel
Chat side panel: Progress, Outputs with preview and file list, and what was used in this session.
import { SessionFile, SessionOutputPreview, SessionOutputTitle, SessionPanel, SessionPanelDivider, SessionProgress, SessionSection, SessionSource,} from "@/components/ai/session-panel";import { Button } from "@/components/ui/button";import { useState } from "react";import { IconPlaceholder } from "@/edmi/icon-placeholder";
export default function Demo() { const [open, setOpen] = useState(true);
if (!open) { return ( <Button onClick={() => setOpen(true)} size="sm" variant="outline"> Show session </Button> ); }
return ( <SessionPanel> <SessionProgress onClose={() => setOpen(false)} value={60}> Reading the keeper config, then drafting the report. </SessionProgress> <SessionPanelDivider /> <SessionSection title="Outputs"> <SessionOutputPreview className="bg-[#14213d] px-5 py-[18px]"> <div className="font-mono text-[7px] tracking-[1px] text-[#e39a3c]"> RESEARCH · OCT 2026 </div> <div className="mt-9 font-serif text-[17px] leading-[1.2] font-bold text-white"> MAG4 rebalance, three trades </div> </SessionOutputPreview> <SessionOutputTitle meta="Artifact"> Rebalance report </SessionOutputTitle> <div className="mt-1"> <SessionFile name="Rebalance report" format="PDF" /> <SessionFile name="Rebalance report" format="MD" kind="code" /> </div> </SessionSection> <SessionPanelDivider /> <SessionSection title="Used in this session"> <SessionSource icon={ <IconPlaceholder lucide="GlobeIcon" tabler="IconWorld" hugeicons="Globe02Icon" phosphor="GlobeIcon" remixicon="RiGlobalLine" /> } label="Web search" favicons={[ { label: "Jupiter", color: "#e5484d" }, { label: "Coinglass", color: "#3b6fd6" }, { label: "Orca", color: "#111111" }, ]} more={4} /> <SessionSource icon={ <IconPlaceholder lucide="ClockIcon" tabler="IconClock" hugeicons="Clock01Icon" phosphor="ClockIcon" remixicon="RiTimeLine" /> } label="Memory" > Read · Keeper, MAG4, fees </SessionSource> </SessionSection> </SessionPanel> );}<script setup lang="ts">import { ClockIcon, GlobeIcon } from "@lucide/vue";import { SessionFile, SessionOutputPreview, SessionOutputTitle, SessionPanel, SessionPanelDivider, SessionProgress, SessionSection, SessionSource,} from "@/components/ai/session-panel";import { Button } from "@/components/ui/button";import { ref } from "vue";
const open = ref(true);
const favicons = [ { label: "Jupiter", color: "#e5484d" }, { label: "Coinglass", color: "#3b6fd6" }, { label: "Orca", color: "#111111" },];</script>
<template> <Button v-if="!open" size="sm" variant="outline" @click="open = true">Show session</Button> <SessionPanel v-else> <SessionProgress :value="60" @close="open = false"> Reading the keeper config, then drafting the report. </SessionProgress> <SessionPanelDivider /> <SessionSection title="Outputs"> <SessionOutputPreview class="bg-[#14213d] px-5 py-[18px]"> <div class="font-mono text-[7px] tracking-[1px] text-[#e39a3c]"> RESEARCH · OCT 2026 </div> <div class="mt-9 font-serif text-[17px] leading-[1.2] font-bold text-white"> MAG4 rebalance, three trades </div> </SessionOutputPreview> <SessionOutputTitle meta="Artifact">Rebalance report</SessionOutputTitle> <div class="mt-1"> <SessionFile name="Rebalance report" format="PDF" /> <SessionFile name="Rebalance report" format="MD" kind="code" /> </div> </SessionSection> <SessionPanelDivider /> <SessionSection title="Used in this session"> <SessionSource label="Web search" :favicons="favicons" :more="4"> <template #icon> <GlobeIcon /> </template> </SessionSource> <SessionSource label="Memory"> <template #icon> <ClockIcon /> </template> Read · Keeper, MAG4, fees </SessionSource> </SessionSection> </SessionPanel></template><script lang="ts"> import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte"; import { SessionFile, SessionOutputPreview, SessionOutputTitle, SessionPanel, SessionPanelDivider, SessionProgress, SessionSection, SessionSource, } from "$lib/components/ai/session-panel"; import { Button } from "$lib/components/ui/button";
let open = $state(true);
const favicons = [ { label: "Jupiter", color: "#e5484d" }, { label: "Coinglass", color: "#3b6fd6" }, { label: "Orca", color: "#111111" }, ];</script>
{#if !open} <Button size="sm" variant="outline" onclick={() => (open = true)}>Show session</Button>{:else}<SessionPanel> <SessionProgress value={60} onClose={() => (open = false)}> Reading the keeper config, then drafting the report. </SessionProgress> <SessionPanelDivider /> <SessionSection title="Outputs"> <SessionOutputPreview class="bg-[#14213d] px-5 py-[18px]"> <div class="font-mono text-[7px] tracking-[1px] text-[#e39a3c]">RESEARCH · OCT 2026</div> <div class="mt-9 font-serif text-[17px] leading-[1.2] font-bold text-white"> MAG4 rebalance, three trades </div> </SessionOutputPreview> <SessionOutputTitle meta="Artifact">Rebalance report</SessionOutputTitle> <div class="mt-1"> <SessionFile name="Rebalance report" format="PDF" /> <SessionFile name="Rebalance report" format="MD" kind="code" /> </div> </SessionSection> <SessionPanelDivider /> <SessionSection title="Used in this session"> <SessionSource label="Web search" {favicons} more={4}> {#snippet icon()} <IconPlaceholder lucide="GlobeIcon" tabler="IconWorld" hugeicons="Globe02Icon" phosphor="GlobeIcon" remixicon="RiGlobalLine" /> {/snippet} </SessionSource> <SessionSource label="Memory"> {#snippet icon()} <IconPlaceholder lucide="ClockIcon" tabler="IconClock" hugeicons="Clock01Icon" phosphor="ClockIcon" remixicon="RiTimeLine" /> {/snippet} Read · Keeper, MAG4, fees </SessionSource> </SessionSection></SessionPanel>{/if}Edmi ✦ addition. Built on card, collapsible, progress, separator, button and ai-artifact-card (file icons).
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-session-panelpnpm dlx shadcn@latest add @edmi-ui/ai-session-panelyarn dlx shadcn@latest add @edmi-ui/ai-session-panelbunx shadcn@latest add @edmi-ui/ai-session-panelPick 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,card,collapsible,progress,separator,ai-artifact-card,elevation.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-session-panelpnpm dlx shadcn-vue@latest add @edmi-ui/ai-session-panelyarn dlx shadcn-vue@latest add @edmi-ui/ai-session-panelbunx shadcn-vue@latest add @edmi-ui/ai-session-panelPick 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,card,collapsible,progress,separator,ai-artifact-card,elevation.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-session-panel.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-session-panel.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-session-panel.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-session-panel.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,card,collapsible,progress,separator,ai-artifact-card,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { SessionFile, SessionOutputPreview, SessionOutputTitle, SessionPanel, SessionPanelDivider, SessionProgress, SessionSection,} from "@/components/ai/session-panel";
<SessionPanel> <SessionProgress value={60}> Reading the keeper config, then drafting the report. </SessionProgress> <SessionPanelDivider /> <SessionSection title="Outputs"> <SessionOutputPreview className="bg-[#14213d] px-5 py-[18px]"> <div className="font-mono text-[7px] tracking-[1px] text-[#e39a3c]"> RESEARCH · OCT 2026 </div> <div className="mt-9 font-serif text-[17px] leading-[1.2] font-bold text-white"> MAG4 rebalance, three trades </div> </SessionOutputPreview> <SessionOutputTitle meta="Artifact"> Rebalance report </SessionOutputTitle> <div className="mt-1"> <SessionFile name="Rebalance report" format="PDF" /> <SessionFile name="Rebalance report" format="MD" kind="code" /> </div> </SessionSection> <SessionPanelDivider /> {/* … */}</SessionPanel><script setup lang="ts">import { Button } from "@/components/ui/button";</script>
<template> <Button size="sm" variant="outline">Show session</Button> <!-- … --></template><script lang="ts"> import { Button } from "$lib/components/ui/button";</script>
{#if !open} <Button size="sm" variant="outline">Show session</Button>{:else}<!-- … -->Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type { ComponentProps, ReactNode } from "react";import { CaretRightIcon, XIcon } from "@phosphor-icons/react";import { type ArtifactKind, ArtifactKindIcon,} from "@/registry/edmi/components/ai/artifact-card";import { Button } from "@/registry/edmi/ui/button";import { Card } from "@/registry/edmi/ui/card";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/registry/edmi/ui/collapsible";import type { Elevation } from "@/registry/edmi/ui/elevation";import { Progress } from "@/registry/edmi/ui/progress";import { Separator } from "@/registry/edmi/ui/separator";
export type SessionPanelProps = Omit< ComponentProps<typeof Card>, "elevation"> & { /** ✦ depth: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation;};
/** Chat side panel: Progress, Outputs and what was used in this session. */export const SessionPanel = ({ className, elevation, ...props}: SessionPanelProps) => ( <Card data-slot="ai-session-panel" elevation={elevation} className={cn("w-full max-w-[380px] gap-0 px-4 py-3.5", className)} {...props} />);
export type SessionPanelDividerProps = ComponentProps<typeof Separator>;
export const SessionPanelDivider = ({ className, ...props}: SessionPanelDividerProps) => ( <Separator className={cn("my-3", className)} {...props} />);
export type SessionProgressProps = ComponentProps<typeof Collapsible> & { title?: ReactNode; /** 0 to 100: shows a progress bar above the content. */ value?: number; onClose?: () => void;};
/** Collapsible "Progress" section with the panel close button. */export const SessionProgress = ({ title = "Progress", value, onClose, className, children, ...props}: SessionProgressProps) => ( <Collapsible data-slot="ai-session-progress" className={cn("w-full", className)} {...props} > <div className="flex items-center justify-between"> <CollapsibleTrigger className="group/trigger flex items-center gap-1 text-sm outline-none focus-visible:underline"> {title} <CaretRightIcon className="size-3.5 text-muted-foreground transition-transform group-data-[panel-open]/trigger:rotate-90" /> </CollapsibleTrigger> {onClose && ( <Button aria-label="Close" onClick={onClose} size="icon-xs" type="button" variant="ghost" > <XIcon className="size-3.5" /> </Button> )} </div> <CollapsibleContent className="pt-3 text-[13px] text-muted-foreground"> {value !== undefined && <Progress value={value} className="mb-3" />} {children} </CollapsibleContent> </Collapsible>);
export type SessionSectionProps = Omit<ComponentProps<"section">, "title"> & { title: ReactNode;};
export const SessionSection = ({ title, className, children, ...props}: SessionSectionProps) => ( <section data-slot="ai-session-section" className={cn("flex flex-col", className)} {...props} > <h3 className="mb-2.5 text-sm font-normal">{title}</h3> {children} </section>);
export type SessionOutputPreviewProps = ComponentProps<"div">;
/** Large preview of the main output (render the document, slide or image inside). */export const SessionOutputPreview = ({ className, ...props}: SessionOutputPreviewProps) => ( <div data-slot="ai-session-output-preview" className={cn( "h-[150px] overflow-hidden rounded-[var(--radius)] border border-border bg-muted", className, )} {...props} />);
export type SessionOutputTitleProps = ComponentProps<"div"> & { /** Small line under the title (`Artifact`). */ meta?: ReactNode;};
export const SessionOutputTitle = ({ meta, className, children, ...props}: SessionOutputTitleProps) => ( <div data-slot="ai-session-output-title" className={cn("mt-2", className)} {...props} > <div className="text-sm">{children}</div> {meta && ( <div className="font-mono text-[12.5px] text-muted-foreground"> {meta} </div> )} </div>);
export type SessionFileProps = Omit< ComponentProps<"button">, "children" | "name"> & { name: ReactNode; /** Format badge on the right (`PDF`, `MD`). */ format?: ReactNode; kind?: ArtifactKind;};
export const SessionFile = ({ name, format, kind = "document", className, ...props}: SessionFileProps) => ( <button data-slot="ai-session-file" type="button" className={cn( "flex w-full items-center gap-2.5 rounded-md py-[7px] text-left text-sm outline-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring", className, )} {...props} > <span className="text-muted-foreground"> <ArtifactKindIcon kind={kind} className="size-4" /> </span> <span className="min-w-0 flex-1 truncate">{name}</span> {format && ( <span className="font-mono text-[11.5px] text-muted-foreground"> {format} </span> )} </button>);
export type SessionSourceFavicon = { label: string; /** Favicon image; initials on `color` when omitted. */ src?: string; /** Brand color of the initials tile (data, not themed). */ color?: string;};
export type SessionSourceProps = Omit<ComponentProps<"div">, "children"> & { icon: ReactNode; label: ReactNode; favicons?: SessionSourceFavicon[]; /** Count after the favicons (`+4`). */ more?: number; /** Detail line after the label (memory: `Read · Career, Tech`). */ children?: ReactNode;};
/** One row of "Used in this session": web search, memory, tools. */export const SessionSource = ({ icon, label, favicons, more, className, children, ...props}: SessionSourceProps) => ( <div data-slot="ai-session-source" className={cn("flex items-center gap-2.5 py-[7px] text-sm", className)} {...props} > <span className="text-muted-foreground [&_svg]:size-4">{icon}</span> <span className={cn(!children && "flex-1")}>{label}</span> {children && ( <span className="min-w-0 flex-1 truncate text-[12.5px] text-muted-foreground"> {children} </span> )} {favicons && favicons.length > 0 && ( <span className="flex items-center"> {favicons.map((f) => f.src ? ( <img key={f.label} alt={f.label} src={f.src} className="-ml-[3px] size-4 rounded-[4px] border border-card object-cover" /> ) : ( <span key={f.label} className="-ml-[3px] inline-flex size-4 items-center justify-center rounded-[4px] border border-card bg-muted-foreground text-[8px] font-bold text-white" style={f.color ? { background: f.color } : undefined} > {f.label.slice(0, 2).toUpperCase()} </span> ), )} </span> )} {more ? ( <span className="text-[12.5px] text-muted-foreground">+{more}</span> ) : null} </div>);<script setup lang="ts">import type { HTMLAttributes } from "vue"import type { ArtifactKind } from "@/registry/edmi/components/ai/artifact-card"import { ArtifactKindIcon } from "@/registry/edmi/components/ai/artifact-card"import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"] /** File name; the `name` slot overrides it. */ name?: string /** Format badge on the right (`PDF`, `MD`); the `format` slot overrides it. */ format?: string kind?: ArtifactKind}>(), { kind: "document",})</script>
<template> <button data-slot="ai-session-file" type="button" :class="cn('flex w-full items-center gap-2.5 rounded-md py-[7px] text-left text-sm outline-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring', props.class)" > <span class="text-muted-foreground"> <ArtifactKindIcon :kind="kind" class="size-4" /> </span> <span class="min-w-0 flex-1 truncate"><slot name="name">{{ name }}</slot></span> <span v-if="format || $slots.format" class="font-mono text-[11.5px] text-muted-foreground"><slot name="format">{{ format }}</slot></span> </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> <!-- Large preview of the main output (render the document, slide or image inside). --> <div data-slot="ai-session-output-preview" :class="cn('h-[150px] overflow-hidden rounded-[var(--radius)] border border-border bg-muted', 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"] /** Small line under the title (`Artifact`); the `meta` slot overrides it. */ meta?: string}>()</script>
<template> <div data-slot="ai-session-output-title" :class="cn('mt-2', props.class)"> <div class="text-sm"> <slot /> </div> <div v-if="meta || $slots.meta" class="font-mono text-[12.5px] text-muted-foreground"> <slot name="meta"> {{ meta }} </slot> </div> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Card } from "@/registry/edmi/ui/card"import type { Elevation } from "@/registry/edmi/ui/elevation"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"] /** ✦ depth: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation}>(), { elevation: undefined,})</script>
<template> <Card data-slot="ai-session-panel" :elevation="elevation" :class="cn('w-full max-w-[380px] gap-0 px-4 py-3.5', props.class)"> <slot /> </Card></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Separator } from "@/registry/edmi/ui/separator"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <Separator :class="cn('my-3', props.class)" /></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { ChevronRight, X } from "@lucide/vue"import { getCurrentInstance } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Button } from "@/registry/edmi/ui/button"import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/registry/edmi/ui/collapsible"import { Progress } from "@/registry/edmi/ui/progress"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"] /** Heading text; the `title` slot overrides it. */ title?: string /** 0 to 100: shows a progress bar above the content. */ value?: number defaultOpen?: boolean}>(), { title: "Progress", defaultOpen: false,})
const emit = defineEmits<{ (e: "close"): void}>()
// The close button shows when a `close` listener is attached (same idea as `stop` on the submit button).const hasClose = !!getCurrentInstance()?.vnode.props?.onClose</script>
<template> <!-- Collapsible "Progress" section with the panel close button. --> <Collapsible data-slot="ai-session-progress" :default-open="defaultOpen" :class="cn('w-full', props.class)"> <div class="flex items-center justify-between"> <CollapsibleTrigger class="group/trigger flex items-center gap-1 text-sm outline-none focus-visible:underline"> <slot name="title"> {{ title }} </slot> <ChevronRight class="size-3.5 text-muted-foreground transition-transform group-data-[state=open]/trigger:rotate-90" /> </CollapsibleTrigger> <Button v-if="hasClose" aria-label="Close" size="icon-xs" type="button" variant="ghost" @click="emit('close')"> <X class="size-3.5" /> </Button> </div> <CollapsibleContent class="pt-3 text-[13px] text-muted-foreground"> <Progress v-if="value !== undefined" :model-value="value" class="mb-3" /> <slot /> </CollapsibleContent> </Collapsible></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] /** Heading text; the `title` slot overrides it. */ title?: string}>()</script>
<template> <section data-slot="ai-session-section" :class="cn('flex flex-col', props.class)"> <h3 class="mb-2.5 text-sm font-normal"> <slot name="title"> {{ title }} </slot> </h3> <slot /> </section></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import type { SessionSourceFavicon } from "./types"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] /** Row label; the `label` slot overrides it. */ label?: string favicons?: SessionSourceFavicon[] /** Count after the favicons (`+4`). */ more?: number}>()</script>
<template> <!-- One row of "Used in this session": web search, memory, tools. Slots: `icon`, default = detail line. --> <div data-slot="ai-session-source" :class="cn('flex items-center gap-2.5 py-[7px] text-sm', props.class)"> <span class="text-muted-foreground [&_svg]:size-4"> <slot name="icon" /> </span> <span :class="cn(!$slots.default && 'flex-1')"><slot name="label">{{ label }}</slot></span> <span v-if="$slots.default" class="min-w-0 flex-1 truncate text-[12.5px] text-muted-foreground"> <slot /> </span> <span v-if="favicons && favicons.length > 0" class="flex items-center"> <template v-for="f in favicons" :key="f.label"> <img v-if="f.src" :alt="f.label" :src="f.src" class="-ml-[3px] size-4 rounded-[4px] border border-card object-cover" > <span v-else class="-ml-[3px] inline-flex size-4 items-center justify-center rounded-[4px] border border-card bg-muted-foreground text-[8px] font-bold text-white" :style="f.color ? { background: f.color } : undefined" > {{ f.label.slice(0, 2).toUpperCase() }} </span> </template> </span> <span v-if="more" class="text-[12.5px] text-muted-foreground">+{{ more }}</span> </div></template>import type { ArtifactKind } from "@/registry/edmi/components/ai/artifact-card";
export type SessionSourceFavicon = { label: string; /** Favicon image; initials on `color` when omitted. */ src?: string; /** Brand color of the initials tile (data, not themed). */ color?: string;};
export type { ArtifactKind };export { default as SessionFile } from "./SessionFile.vue";export { default as SessionOutputPreview } from "./SessionOutputPreview.vue";export { default as SessionOutputTitle } from "./SessionOutputTitle.vue";export { default as SessionPanel } from "./SessionPanel.vue";export { default as SessionPanelDivider } from "./SessionPanelDivider.vue";export { default as SessionProgress } from "./SessionProgress.vue";export { default as SessionSection } from "./SessionSection.vue";export { default as SessionSource } from "./SessionSource.vue";export * from "./types";import SessionFile from "./session-file.svelte";import SessionOutputPreview from "./session-output-preview.svelte";import SessionOutputTitle from "./session-output-title.svelte";import SessionPanel from "./session-panel.svelte";import SessionPanelDivider from "./session-panel-divider.svelte";import SessionProgress from "./session-progress.svelte";import SessionSection from "./session-section.svelte";import SessionSource from "./session-source.svelte";
export * from "./types.js";export { SessionFile, SessionOutputPreview, SessionOutputTitle, SessionPanel, SessionPanelDivider, SessionProgress, SessionSection, SessionSource,};<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLButtonAttributes } from "svelte/elements"; import { ArtifactKindIcon, type ArtifactKind } from "../artifact-card/index.js";
let { ref = $bindable(null), name, format, kind = "document", class: className, ...restProps }: WithElementRef<Omit<HTMLButtonAttributes, "children" | "name">, HTMLButtonElement> & { name: string | Snippet; /** Format badge on the right (`PDF`, `MD`). */ format?: string | Snippet; kind?: ArtifactKind; } = $props();</script>
<button bind:this={ref} data-slot="ai-session-file" type="button" class={cn( "flex w-full items-center gap-2.5 rounded-md py-[7px] text-left text-sm outline-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring", className )} {...restProps}> <span class="text-muted-foreground"> <ArtifactKindIcon {kind} class="size-4" /> </span> <span class="min-w-0 flex-1 truncate">{#if typeof name === "string"}{name}{:else}{@render name()}{/if}</span> {#if typeof format === "string"} <span class="font-mono text-[11.5px] text-muted-foreground">{format}</span> {:else if format} <span class="font-mono text-[11.5px] text-muted-foreground">{@render format()}</span> {/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>
<!-- Large preview of the main output (render the document, slide or image inside). --><div bind:this={ref} data-slot="ai-session-output-preview" class={cn( "h-[150px] overflow-hidden rounded-[var(--radius)] border border-border bg-muted", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), meta, class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & { /** Small line under the title (`Artifact`). */ meta?: string | Snippet; } = $props();</script>
<div bind:this={ref} data-slot="ai-session-output-title" class={cn("mt-2", className)} {...restProps}> <div class="text-sm">{@render children?.()}</div> {#if meta} <div class="font-mono text-[12.5px] text-muted-foreground">{#if typeof meta === "string"}{meta}{:else}{@render meta()}{/if}</div> {/if}</div><script lang="ts"> import { Separator } from "$lib/registry/ui/separator/index.js"; import { cn } from "$lib/utils.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, ...restProps }: ComponentProps<typeof Separator> = $props();</script>
<Separator bind:ref class={cn("my-3", className)} {...restProps} /><script lang="ts"> import { Card } from "$lib/registry/ui/card/index.js"; import type { Elevation } from "$lib/registry/ui/elevation/index.js"; import { cn } from "$lib/utils.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, children, elevation = "auto", ...restProps }: Omit<ComponentProps<typeof Card>, "elevation"> & { /** ✦ depth: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation; } = $props();</script>
<Card bind:ref data-slot="ai-session-panel" {elevation} class={cn("w-full max-w-[380px] gap-0 px-4 py-3.5", className)} {...restProps}> {@render children?.()}</Card><script lang="ts"> import CaretRightIcon from "phosphor-svelte/lib/CaretRight"; import XIcon from "phosphor-svelte/lib/X"; import { Button } from "$lib/registry/ui/button/index.js"; import * as Collapsible from "$lib/registry/ui/collapsible/index.js"; import { Progress } from "$lib/registry/ui/progress/index.js"; import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte";
let { ref = $bindable(null), title = "Progress", value, onClose, defaultOpen = false, open = $bindable(defaultOpen), class: className, children, }: { title?: string | Snippet; ref?: HTMLDivElement | null; /** 0 to 100: shows a progress bar above the content. */ value?: number; /** Shows the close button. */ onClose?: () => void; defaultOpen?: boolean; open?: boolean; class?: string; children?: Snippet; } = $props();</script>
<!-- Collapsible "Progress" section with the panel close button. --><Collapsible.Root bind:ref data-slot="ai-session-progress" bind:open class={cn("w-full", className)}> <div class="flex items-center justify-between"> <Collapsible.Trigger class="group/trigger flex items-center gap-1 text-sm outline-none focus-visible:underline" > {#if typeof title === "string"}{title}{:else}{@render title()}{/if} <CaretRightIcon class="size-3.5 text-muted-foreground transition-transform group-data-[state=open]/trigger:rotate-90" /> </Collapsible.Trigger> {#if onClose} <Button aria-label="Close" onclick={onClose} size="icon-xs" type="button" variant="ghost"> <XIcon class="size-3.5" /> </Button> {/if} </div> <Collapsible.Content class="pt-3 text-[13px] text-muted-foreground"> {#if value !== undefined} <Progress {value} class="mb-3" /> {/if} {@render children?.()} </Collapsible.Content></Collapsible.Root><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), title, class: className, children, ...restProps }: WithElementRef<Omit<HTMLAttributes<HTMLElement>, "title">> & { title: string | Snippet } = $props();</script>
<section bind:this={ref} data-slot="ai-session-section" class={cn("flex flex-col", className)} {...restProps}> <h3 class="mb-2.5 text-sm font-normal">{#if typeof title === "string"}{title}{:else}{@render title()}{/if}</h3> {@render children?.()}</section><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements"; import type { SessionSourceFavicon } from "./types.js";
let { ref = $bindable(null), icon, label, favicons, more, class: className, children, ...restProps }: WithElementRef<Omit<HTMLAttributes<HTMLDivElement>, "children">, HTMLDivElement> & { icon: Snippet; label: string | Snippet; favicons?: SessionSourceFavicon[]; /** Count after the favicons (`+4`). */ more?: number; /** Detail line after the label (memory: `Read · Career, Tech`). */ children?: Snippet; } = $props();</script>
<!-- One row of "Used in this session": web search, memory, tools. --><div bind:this={ref} data-slot="ai-session-source" class={cn("flex items-center gap-2.5 py-[7px] text-sm", className)} {...restProps}> <span class="text-muted-foreground [&_svg]:size-4">{@render icon()}</span> <span class={cn(!children && "flex-1")}>{#if typeof label === "string"}{label}{:else}{@render label()}{/if}</span> {#if children} <span class="min-w-0 flex-1 truncate text-[12.5px] text-muted-foreground"> {@render children()} </span> {/if} {#if favicons && favicons.length > 0} <span class="flex items-center"> {#each favicons as f (f.label)} {#if f.src} <img alt={f.label} src={f.src} class="-ml-[3px] size-4 rounded-[4px] border border-card object-cover" /> {:else} <span class="-ml-[3px] inline-flex size-4 items-center justify-center rounded-[4px] border border-card bg-muted-foreground text-[8px] font-bold text-white" style={f.color ? `background: ${f.color}` : undefined} > {f.label.slice(0, 2).toUpperCase()} </span> {/if} {/each} </span> {/if} {#if more} <span class="text-[12.5px] text-muted-foreground">+{more}</span> {/if}</div>export type SessionSourceFavicon = { label: string; /** Favicon image; initials on `color` when omitted. */ src?: string; /** Brand color of the initials tile (data, not themed). */ color?: string;};Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Flat by default and a surface in layered mode. sunken (-1) is a soft inset, raised (+1) a bevel and floating (+2) a bevel with a soft drop.
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
import { SessionFile, SessionOutputTitle, SessionPanel, SessionPanelDivider, SessionProgress, SessionSection,} from "@/components/ai/session-panel";
const levels = [ { value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="grid w-full max-w-3xl gap-5 sm:grid-cols-2"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <SessionPanel elevation={value}> <SessionProgress value={60}>Drafting the report.</SessionProgress> <SessionPanelDivider /> <SessionSection title="Outputs"> <SessionOutputTitle meta="Artifact"> Rebalance report </SessionOutputTitle> <SessionFile name="Rebalance report" format="PDF" /> </SessionSection> </SessionPanel> </div> ))} </div> );}<script setup lang="ts">import { SessionFile, SessionOutputTitle, SessionPanel, SessionPanelDivider, SessionProgress, SessionSection,} from "@/components/ai/session-panel";
const levels = [ { value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },];</script>
<template> <div class="grid w-full max-w-3xl gap-5 sm:grid-cols-2"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <SessionPanel :elevation="level.value"> <SessionProgress :value="60">Drafting the report.</SessionProgress> <SessionPanelDivider /> <SessionSection title="Outputs"> <SessionOutputTitle meta="Artifact">Rebalance report</SessionOutputTitle> <SessionFile name="Rebalance report" format="PDF" /> </SessionSection> </SessionPanel> </div> </div></template><script lang="ts"> import { SessionFile, SessionOutputTitle, SessionPanel, SessionPanelDivider, SessionProgress, SessionSection, } from "$lib/components/ai/session-panel";
const levels = [ { value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }, ] as const;</script>
<div class="grid w-full max-w-3xl gap-5 sm:grid-cols-2"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <SessionPanel elevation={level.value}> <SessionProgress value={60}>Drafting the report.</SessionProgress> <SessionPanelDivider /> <SessionSection title="Outputs"> <SessionOutputTitle meta="Artifact">Rebalance report</SessionOutputTitle> <SessionFile name="Rebalance report" format="PDF" /> </SessionSection> </SessionPanel> </div> {/each}</div>| Part | Prop | Type | Default |
|---|---|---|---|
| SessionPanel | elevation, Card props |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
| SessionProgress | title, value, onClose, Collapsible props |
ReactNode, 0-100, () => void |
Progress |
| SessionSection | title |
ReactNode |
|
| SessionOutputPreview | box for the large preview | ||
| SessionOutputTitle | meta |
ReactNode |
|
| SessionFile | name, format, kind |
ReactNode, ReactNode, ArtifactKind |
document |
| SessionSource | icon, label, favicons, more, children |
web search, memory, tools row |
In Vue ReactNode props are slots of the same name (the string prop stays as a fallback); in Svelte they accept string | Snippet.
✦ Edmi additions
Section titled “✦ Edmi additions”SessionSourcefavicons stack with a count after them (+4); the detail line (Read · Career, Tech) goes inchildren.