Skip to content
edmi-ui

Artifact

Container for generated output with a header, actions and scrollable content.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

rebalance.ts

Generated · 8 lines

import { rebalance } from "@/lib/keeper"
export async function run(index: string) {  // only when drift is above the limit  const drift = await getDrift(index)  if (drift < 0.02) return  return rebalance(index, { slippage: 0.01 })}

rebalance.ts

Generated · 8 lines

import { rebalance } from "@/lib/keeper"
export async function run(index: string) {  // only when drift is above the limit  const drift = await getDrift(index)  if (drift < 0.02) return  return rebalance(index, { slippage: 0.01 })}

rebalance.ts

Generated · 8 lines

import { rebalance } from "@/lib/keeper"
export async function run(index: string) {  // only when drift is above the limit  const drift = await getDrift(index)  if (drift < 0.02) return  return rebalance(index, { slippage: 0.01 })}

Built on card, button and tooltip.

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

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

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

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

import {
Artifact,
ArtifactAction,
ArtifactActions,
ArtifactClose,
ArtifactDescription,
ArtifactHeader,
ArtifactTitle,
} from "@/components/ai/artifact";
<Artifact className="max-w-xl">
<ArtifactHeader>
<div>
<ArtifactTitle>rebalance.ts</ArtifactTitle>
<ArtifactDescription>Generated · 8 lines</ArtifactDescription>
</div>
<ArtifactActions>
<ArtifactAction
tooltip="Copy"
/>
<ArtifactAction
icon={
<DownloadIcon className="size-4" />
}
tooltip="Download"
/>
<ArtifactAction
icon={
<ArrowSquareOutIcon className="size-4" />
}
tooltip="Open"
/>
<ArtifactClose />
</ArtifactActions>
</ArtifactHeader>
{/* … */}
</Artifact>
<script setup lang="ts">
import { Button } from "@/components/ui/button";
import {
Artifact,
ArtifactAction,
ArtifactActions,
ArtifactClose,
ArtifactContent,
ArtifactDescription,
ArtifactHeader,
ArtifactTitle,
} from "@/components/ai/artifact";
import { CodeBlock } from "@/components/ai/code-block";
</script>
<template>
<Button variant="outline" size="sm">Reopen rebalance.ts</Button>
<Artifact v-else class="max-w-xl">
<ArtifactHeader>
<div>
<ArtifactTitle>rebalance.ts</ArtifactTitle>
<ArtifactDescription>Generated · 8 lines</ArtifactDescription>
</div>
<ArtifactActions>
<ArtifactAction tooltip="Copy" @click="copy">
<CheckIcon class="size-4" />
<CopyIcon v-else class="size-4" />
</ArtifactAction>
<ArtifactAction tooltip="Download"><DownloadIcon class="size-4" /></ArtifactAction>
<ArtifactAction tooltip="Open"><ArrowUpRightIcon class="size-4" /></ArtifactAction>
<ArtifactClose />
</ArtifactActions>
</ArtifactHeader>
<ArtifactContent>
<CodeBlock class="rounded-none border-0" language="typescript" show-line-numbers />
</ArtifactContent>
</Artifact>
</template>
<script lang="ts">
import { Button } from "$lib/components/ui/button";
</script>
{#if !open}
<Button variant="outline" size="sm">Reopen rebalance.ts</Button>
{:else}
<!-- … -->
registry/components/ai/artifact.tsx
"use client";
import { cn } from "cn";
import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
import { XIcon } from "@phosphor-icons/react";
import { Button } from "@/registry/edmi/ui/button";
import { Card } from "@/registry/edmi/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/registry/edmi/ui/tooltip";
export type ArtifactProps = ComponentProps<typeof Card>;
/** Container for generated output. Built on the ui card; `elevation` ✦ is forwarded to the card. */
export const Artifact = ({ className, ...props }: ArtifactProps) => (
<Card
data-slot="ai-artifact"
className={cn("w-full gap-0 py-0", className)}
{...props}
/>
);
export type ArtifactHeaderProps = HTMLAttributes<HTMLDivElement>;
export const ArtifactHeader = ({
className,
...props
}: ArtifactHeaderProps) => (
<div
data-slot="ai-artifact-header"
className={cn(
"flex items-center justify-between gap-2.5 border-b border-border px-3.5 py-3",
className,
)}
{...props}
/>
);
export type ArtifactCloseProps = ComponentProps<typeof Button>;
/** Close button; a 1px divider separates it from the actions before it. */
export const ArtifactClose = ({
className,
children,
size = "icon-sm",
variant = "ghost",
...props
}: ArtifactCloseProps) => (
<Button
className={cn(
"relative ml-2 text-muted-foreground before:absolute before:top-1/2 before:-left-1.5 before:h-[18px] before:w-px before:-translate-y-1/2 before:bg-border hover:text-foreground",
className,
)}
size={size}
type="button"
variant={variant}
{...props}
>
{children ?? (
<XIcon
/>
)}
<span className="sr-only">Close</span>
</Button>
);
export type ArtifactTitleProps = HTMLAttributes<HTMLParagraphElement>;
export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
<p
className={cn("text-sm font-semibold text-foreground", className)}
{...props}
/>
);
export type ArtifactDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
export const ArtifactDescription = ({
className,
...props
}: ArtifactDescriptionProps) => (
<p className={cn("text-xs text-muted-foreground", className)} {...props} />
);
export type ArtifactActionsProps = HTMLAttributes<HTMLDivElement>;
export const ArtifactActions = ({
className,
...props
}: ArtifactActionsProps) => (
<div className={cn("flex items-center gap-1", className)} {...props} />
);
export type ArtifactActionProps = ComponentProps<typeof Button> & {
tooltip?: string;
label?: string;
/** The icon element (an icon element); `children` works too. */
icon?: ReactNode;
};
export const ArtifactAction = ({
tooltip,
label,
icon,
children,
className,
size = "icon-sm",
variant = "ghost",
...props
}: ArtifactActionProps) => {
const button = (
<Button
className={cn("text-muted-foreground hover:text-foreground", className)}
size={size}
type="button"
variant={variant}
{...props}
>
{icon ?? children}
<span className="sr-only">{label || tooltip}</span>
</Button>
);
if (tooltip) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={button} />
<TooltipContent>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return button;
};
export type ArtifactContentProps = HTMLAttributes<HTMLDivElement>;
/** Scrollable content. No padding by default (code blocks and images run edge to edge); add `p-4` for prose. */
export const ArtifactContent = ({
className,
...props
}: ArtifactContentProps) => (
<div
data-slot="ai-artifact-content"
className={cn("flex-1 overflow-auto", className)}
{...props}
/>
);
registry/components/ai/artifact/Artifact.vue
<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"
// Container for generated output. Built on the ui card; `elevation` ✦ is forwarded to the card.
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-artifact"
:elevation="props.elevation"
:class="cn('w-full gap-0 py-0', props.class)"
>
<slot />
</Card>
</template>
registry/components/ai/artifact/ArtifactAction.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { ButtonVariants } from "@/registry/edmi/ui/button"
import { cn } from "@/registry/edmi/lib/utils"
import { Button } from "@/registry/edmi/ui/button"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/registry/edmi/ui/tooltip"
defineOptions({ inheritAttrs: false })
// The icon goes in the default slot (an icon component), e.g. `<ArtifactAction tooltip="Copy"><CopyIcon /></ArtifactAction>`.
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
tooltip?: string
label?: string
variant?: ButtonVariants["variant"]
size?: ButtonVariants["size"]
}>(), {
variant: "ghost",
size: "icon-sm",
})
</script>
<template>
<TooltipProvider v-if="props.tooltip">
<Tooltip>
<TooltipTrigger as-child>
<Button
v-bind="$attrs"
type="button"
:variant="props.variant"
:size="props.size"
:class="cn('text-muted-foreground hover:text-foreground', props.class)"
>
<slot />
<span class="sr-only">{{ props.label || props.tooltip }}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ props.tooltip }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
v-else
v-bind="$attrs"
type="button"
:variant="props.variant"
:size="props.size"
:class="cn('text-muted-foreground hover:text-foreground', props.class)"
>
<slot />
<span class="sr-only">{{ props.label }}</span>
</Button>
</template>
registry/components/ai/artifact/ArtifactActions.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 :class="cn('flex items-center gap-1', props.class)">
<slot />
</div>
</template>
registry/components/ai/artifact/ArtifactClose.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { ButtonVariants } from "@/registry/edmi/ui/button"
import { XIcon } from "@lucide/vue"
import { cn } from "@/registry/edmi/lib/utils"
import { Button } from "@/registry/edmi/ui/button"
// Close button; a 1px divider separates it from the actions before it.
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
variant?: ButtonVariants["variant"]
size?: ButtonVariants["size"]
}>(), {
variant: "ghost",
size: "icon-sm",
})
</script>
<template>
<Button
type="button"
:variant="props.variant"
:size="props.size"
:class="cn(
'relative ml-2 text-muted-foreground before:absolute before:top-1/2 before:-left-1.5 before:h-[18px] before:w-px before:-translate-y-1/2 before:bg-border hover:text-foreground',
props.class,
)"
>
<slot>
<XIcon />
</slot>
<span class="sr-only">Close</span>
</Button>
</template>
registry/components/ai/artifact/ArtifactContent.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
// Scrollable content. No padding by default (code blocks and images run edge to edge); add `p-4` for prose.
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div data-slot="ai-artifact-content" :class="cn('flex-1 overflow-auto', props.class)">
<slot />
</div>
</template>
registry/components/ai/artifact/ArtifactDescription.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>
<p :class="cn('text-xs text-muted-foreground', props.class)">
<slot />
</p>
</template>
registry/components/ai/artifact/ArtifactHeader.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-artifact-header"
:class="cn('flex items-center justify-between gap-2.5 border-b border-border px-3.5 py-3', props.class)"
>
<slot />
</div>
</template>
registry/components/ai/artifact/ArtifactTitle.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>
<p :class="cn('text-sm font-semibold text-foreground', props.class)">
<slot />
</p>
</template>
registry/components/ai/artifact/index.ts
export { default as Artifact } from "./Artifact.vue";
export { default as ArtifactAction } from "./ArtifactAction.vue";
export { default as ArtifactActions } from "./ArtifactActions.vue";
export { default as ArtifactClose } from "./ArtifactClose.vue";
export { default as ArtifactContent } from "./ArtifactContent.vue";
export { default as ArtifactDescription } from "./ArtifactDescription.vue";
export { default as ArtifactHeader } from "./ArtifactHeader.vue";
export { default as ArtifactTitle } from "./ArtifactTitle.vue";
src/lib/registry/ai/artifact/artifact-action.svelte
<script lang="ts">
import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js";
import * as Tooltip from "$lib/registry/ui/tooltip/index.js";
import { cn } from "$lib/utils.js";
// The icon goes in `children` (an `IconPlaceholder`).
let {
tooltip,
label,
variant = "ghost",
size = "icon-sm",
class: className,
children,
...restProps
}: Omit<ButtonProps, "href"> & { tooltip?: string; label?: string } = $props();
</script>
{#snippet inner()}
{@render children?.()}
<span class="sr-only">{label || tooltip}</span>
{/snippet}
{#if tooltip}
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
{#snippet child({ props })}
<Button
{...props}
type="button"
{variant}
{size}
class={cn("text-muted-foreground hover:text-foreground", className)}
{...restProps}
>
{@render inner()}
</Button>
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content>
<p>{tooltip}</p>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
{:else}
<Button
type="button"
{variant}
{size}
class={cn("text-muted-foreground hover:text-foreground", className)}
{...restProps}
>
{@render inner()}
</Button>
{/if}
src/lib/registry/ai/artifact/artifact-actions.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
let {
class: className,
children,
...restProps
}: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
</script>
<div class={cn("flex items-center gap-1", className)} {...restProps}>
{@render children?.()}
</div>
src/lib/registry/ai/artifact/artifact-close.svelte
<script lang="ts">
import XIcon from "phosphor-svelte/lib/X";
import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js";
import { cn } from "$lib/utils.js";
// Close button; a 1px divider separates it from the actions before it.
let {
variant = "ghost",
size = "icon-sm",
class: className,
children,
...restProps
}: Omit<ButtonProps, "href"> = $props();
</script>
<Button
type="button"
{variant}
{size}
class={cn(
"relative ml-2 text-muted-foreground before:absolute before:top-1/2 before:-left-1.5 before:h-[18px] before:w-px before:-translate-y-1/2 before:bg-border hover:text-foreground",
className
)}
{...restProps}
>
{#if children}
{@render children()}
{:else}
<XIcon
/>
{/if}
<span class="sr-only">Close</span>
</Button>
src/lib/registry/ai/artifact/artifact-content.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
let {
class: className,
children,
...restProps
}: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
</script>
<div data-slot="ai-artifact-content"
class={cn("flex-1 overflow-auto", className)} {...restProps}>
{@render children?.()}
</div>
src/lib/registry/ai/artifact/artifact-description.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
let {
class: className,
children,
...restProps
}: HTMLAttributes<HTMLParagraphElement> & { children?: Snippet } = $props();
</script>
<p class={cn("text-xs text-muted-foreground", className)} {...restProps}>
{@render children?.()}
</p>
src/lib/registry/ai/artifact/artifact-header.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
let {
class: className,
children,
...restProps
}: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
</script>
<div data-slot="ai-artifact-header"
class={cn("flex items-center justify-between gap-2.5 border-b border-border px-3.5 py-3", className)} {...restProps}>
{@render children?.()}
</div>
src/lib/registry/ai/artifact/artifact-title.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
let {
class: className,
children,
...restProps
}: HTMLAttributes<HTMLParagraphElement> & { children?: Snippet } = $props();
</script>
<p class={cn("text-sm font-semibold text-foreground", className)} {...restProps}>
{@render children?.()}
</p>
src/lib/registry/ai/artifact/artifact.svelte
<script lang="ts">
import { Card } from "$lib/registry/ui/card/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
// Container for generated output. Built on the ui card; `elevation` ✦ is forwarded to the card (Card props).
let {
class: className,
children,
...restProps
}: ComponentProps<typeof Card> = $props();
</script>
<Card data-slot="ai-artifact" class={cn("w-full gap-0 py-0", className)} {...restProps}>
{@render children?.()}
</Card>
src/lib/registry/ai/artifact/index.ts
import Root from "./artifact.svelte";
import Action from "./artifact-action.svelte";
import Actions from "./artifact-actions.svelte";
import Close from "./artifact-close.svelte";
import Content from "./artifact-content.svelte";
import Description from "./artifact-description.svelte";
import Header from "./artifact-header.svelte";
import Title from "./artifact-title.svelte";
export {
Action as ArtifactAction,
Actions as ArtifactActions,
Close as ArtifactClose,
Content as ArtifactContent,
Description as ArtifactDescription,
Header as ArtifactHeader,
Root as Artifact,
Title as ArtifactTitle,
};

See the Elevation guide for the levels, roles and layered mode.

The container are flat by default and take the surface level in layered mode (raised). sunken (-1) is a soft inset, raised (+1) a bevel and floating (+2) a bevel with a soft drop.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Sunken (-1)

rebalance.ts

Generated · 8 lines

export async function run(index: string) { … }

Flat (0)

rebalance.ts

Generated · 8 lines

export async function run(index: string) { … }

Raised (+1)

rebalance.ts

Generated · 8 lines

export async function run(index: string) { … }

Floating (+2)

rebalance.ts

Generated · 8 lines

export async function run(index: string) { … }

Sunken (-1)

rebalance.ts

Generated · 8 lines

export async function run(index: string) { … }

Flat (0)

rebalance.ts

Generated · 8 lines

export async function run(index: string) { … }

Raised (+1)

rebalance.ts

Generated · 8 lines

export async function run(index: string) { … }

Floating (+2)

rebalance.ts

Generated · 8 lines

export async function run(index: string) { … }

Sunken (-1)

rebalance.ts

Generated · 8 lines

export async function run(index: string) { … }

Flat (0)

rebalance.ts

Generated · 8 lines

export async function run(index: string) { … }

Raised (+1)

rebalance.ts

Generated · 8 lines

export async function run(index: string) { … }

Floating (+2)

rebalance.ts

Generated · 8 lines

export async function run(index: string) { … }

Part Prop Type Default
Artifact elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"
ArtifactAction tooltip, label, icon string, string, ReactNode
ArtifactClose Button props variant="ghost"
ArtifactContent className string no padding
  • elevation: depth of the container (forwarded to the Card).
  • ArtifactContent has no padding by default so code blocks and images run edge to edge; add p-4 for prose.
  • ArtifactAction takes the icon as a node (icon={<IconPlaceholder … />}) instead of a component.
  • Vue and Svelte: the action icon goes in the default slot / children instead of an icon prop.