Components
InlineAttachment
BetaBeta — the props contract is close to final, but small breaking changes can still land.Inline file and media previews for chat turns.
Composition
Attachment with actions
- <InlineAttachment>
- <InlineAttachmentMedia />
- <InlineAttachmentContent>
- <InlineAttachmentTitle />
- <InlineAttachmentDescription />
- <InlineAttachmentActions>
- <InlineAttachmentAction />
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
This agent installs from the inline-attachment registry. Install the bundle with the command above, or inspect the source below.
npx shadcn@latest add https://control-ui.dev/r/inline-attachment.jsonUsage
import { InlineAttachment, InlineAttachmentContent, InlineAttachmentMedia, InlineAttachmentTitle,} from "@/components/control-ui/inline-attachment";export function Example({ url }: { url?: string }) { return ( <InlineAttachment name="app-icon.png" state={url ? "ready" : "pending"} aspect={1}> <InlineAttachmentMedia src={url} /> <InlineAttachmentContent> <InlineAttachmentTitle /> </InlineAttachmentContent> </InlineAttachment> );}Dependencies
Effects
src/registry/sources/control-ui/effects.cssSupportRaw code
This agent’s owned source and private support files
"use client";import type { ComponentProps, CSSProperties, ReactNode } from "react";import { createContext, useContext } from "react";import type { InlineAttachmentKnobStyle } from "@/components/control-ui/knob-contracts/inline-attachment-knobs";import { cn } from "@/components/control-ui/lib/cn";export type InlineAttachmentState = "ready" | "pending" | "error";type InlineAttachmentContextValue = { name: string; state: InlineAttachmentState };type InlineAttachmentStyle = CSSProperties & InlineAttachmentKnobStyle;type InlineAttachmentStyleProps<Props, Style> = Omit<Props, "style"> & { style?: CSSProperties & Style };const InlineAttachmentContext = createContext<InlineAttachmentContextValue | null>(null);function useInlineAttachmentContext() { const context = useContext(InlineAttachmentContext); if (!context) throw new Error("InlineAttachment compound components must be rendered inside <InlineAttachment>."); return context;}function defaultLabel(name: string, state: InlineAttachmentState) { if (state === "pending") return `Generating ${name}`; if (state === "error") return `${name} is unavailable`; return `Open attachment: ${name}`;}export type InlineAttachmentProps = InlineAttachmentStyleProps<ComponentProps<"button">, InlineAttachmentKnobStyle> & { name: string; state?: InlineAttachmentState; /** Width-to-height ratio of the reserved box — generated media knows its ratio before its pixels, so arrival never reflows the turn. */ aspect?: number;};export function InlineAttachment({ name, state = "ready", aspect = 1.26, disabled, className, children, style, "aria-label": ariaLabel, ...props}: InlineAttachmentProps) { const attachmentStyle = { aspectRatio: aspect, ...style } satisfies InlineAttachmentStyle; return ( <InlineAttachmentContext.Provider value={{ name, state }}> <button type="button" aria-label={ariaLabel ?? defaultLabel(name, state)} aria-busy={state === "pending" || undefined} disabled={disabled || state === "pending"} data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="root" data-surface="panel" data-state={state} className={cn("group relative w-full max-w-72 cursor-pointer overflow-hidden disabled:cursor-default", className)} style={attachmentStyle} {...props} > {children} </button> </InlineAttachmentContext.Provider> );}export type InlineAttachmentMediaProps = ComponentProps<"div"> & { src?: string; alt?: string;} & { style?: CSSProperties & InlineAttachmentKnobStyle };function GeneratingPlaceholder() { return ( <div aria-hidden="true" data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="placeholder" className={cn("size-full", "halftone")} /> );}function DocumentPlaceholder() { return ( <div data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="document" className="size-full"> <div data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="document-sheet" className="h-full"> <div data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="document-heading" className="" /> <div className="grid"> <div data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="document-line" data-width="long" className="" /> <div data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="document-line" data-width="medium" className="" /> <div data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="document-line" data-width="longest" className="" /> <div data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="document-line" data-width="short" className="" /> </div> <div data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="document-stamp" className="" /> </div> </div> );}function mediaContent({ state, src, alt, children }: { state: InlineAttachmentState; src?: string; alt: string; children?: ReactNode }) { if (state === "pending") return <GeneratingPlaceholder />; if (src) { return ( <img src={src} alt={alt} loading="lazy" decoding="async" data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="media-image" className="size-full object-cover" /> ); } return children ?? <DocumentPlaceholder />;}export function InlineAttachmentMedia({ src, alt, className, children, ...props }: InlineAttachmentMediaProps) { const { name, state } = useInlineAttachmentContext(); return ( <div data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="image" className={cn("size-full", className)} {...props} > {mediaContent({ state, src, alt: alt ?? name, children })} </div> );}export type InlineAttachmentContentProps = InlineAttachmentStyleProps<ComponentProps<"div">, InlineAttachmentKnobStyle>;export function InlineAttachmentContent({ className, ...props }: InlineAttachmentContentProps) { return ( <div data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="content" className={cn("absolute", className)} {...props} /> );}export type InlineAttachmentTitleProps = ComponentProps<"div"> & { style?: CSSProperties & InlineAttachmentKnobStyle };export function InlineAttachmentTitle({ children, className, ...props }: InlineAttachmentTitleProps) { const { name } = useInlineAttachmentContext(); return ( <div data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="title" className={cn("truncate", className)} {...props} > {children ?? name} </div> );}export type InlineAttachmentDescriptionProps = InlineAttachmentStyleProps<ComponentProps<"div">, InlineAttachmentKnobStyle>;export function InlineAttachmentDescription({ className, ...props }: InlineAttachmentDescriptionProps) { return ( <div data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="description" className={cn("truncate", className)} {...props} /> );}export type InlineAttachmentActionsProps = ComponentProps<"div"> & { style?: CSSProperties & InlineAttachmentKnobStyle };export function InlineAttachmentActions({ className, ...props }: InlineAttachmentActionsProps) { return ( <div data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="actions" className={cn("flex items-center", className)} {...props} /> );}export type InlineAttachmentActionProps = InlineAttachmentStyleProps<ComponentProps<"span">, InlineAttachmentKnobStyle>;export function InlineAttachmentAction({ className, ...props }: InlineAttachmentActionProps) { return ( <span data-control-ui="inline-attachment" data-control-family="inline-attachment" data-slot="action" className={cn(className)} {...props} /> );}Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-inline-attachment-* · 14 knobsHow the cascade resolves--cui-inline-attachment-radius<length>var(--radius-field)--cui-inline-attachment-background<color>var(--card)--cui-inline-attachment-ring-color<color>var(--border)--cui-inline-attachment-padding<length>calc(var(--spacing) * 3)--cui-inline-attachment-document-padding<length>calc(var(--spacing) * 5)--cui-inline-attachment-document-background<color>var(--muted)--cui-inline-attachment-document-background-image*none--cui-inline-attachment-shadow*var(--shadow-sm)--cui-inline-attachment-hover-shadow*var(--shadow-md)--cui-inline-attachment-content-radius<length>max(
var(--radius-sm),
calc(var(--cui-inline-attachment-radius) - var(--cui-inline-attachment-padding))
)--cui-inline-attachment-content-background<color>oklch(from var(--background) l c h / 0.85)--cui-inline-attachment-content-foreground<color>var(--foreground)--cui-inline-attachment-content-shadow*var(--shadow-sm)--cui-inline-attachment-image-outline-color<color>oklch(from var(--foreground) l c h / 0.1)