Artifact Viewer
Side panel that shows one artifact: title and format, open-in, download, expand and close; documents render as paper.
import { ArtifactViewer, ArtifactViewerClose, ArtifactViewerContent, ArtifactViewerDownload, ArtifactViewerExpand, ArtifactViewerHeader, ArtifactViewerOpenIn, ArtifactViewerPaper, ArtifactViewerTitle,} from "@/components/ai/artifact-viewer";import { Button } from "@/components/ui/button";import { useState } from "react";
export default function Demo() { const [open, setOpen] = useState(true); const [expanded, setExpanded] = useState(false);
if (!open) { return ( <Button onClick={() => setOpen(true)} size="sm" variant="outline"> Show artifact </Button> ); }
return ( <ArtifactViewer className={expanded ? "w-full max-w-3xl" : "w-full max-w-xl"} > <ArtifactViewerHeader> <ArtifactViewerTitle format="PDF">Rebalance report</ArtifactViewerTitle> <ArtifactViewerOpenIn /> <ArtifactViewerDownload /> <ArtifactViewerExpand onClick={() => setExpanded((v) => !v)} /> <ArtifactViewerClose onClick={() => setOpen(false)} /> </ArtifactViewerHeader> <ArtifactViewerContent> <ArtifactViewerPaper> <div className="font-mono text-[11px] tracking-[2px] text-[#7a7974]"> RESEARCH · MAG4 · OCT 2026 </div> <div className="mt-2.5 font-serif text-[26px] leading-[1.15] font-bold"> Drift stays inside the band </div> <p className="mt-3 text-[13px] leading-[1.7] text-[#3d3c38]"> The keeper checks weights every hour and trades only when a sleeve drifts past 2%. Slippage stays under 1% at current depth.{" "} <b>No manual step is needed before the next rebalance.</b> </p> <div className="my-4 h-0.5 bg-[#22406b]" /> <div className="font-serif text-[17px] font-bold text-[#22406b]"> What changed this week </div> </ArtifactViewerPaper> </ArtifactViewerContent> </ArtifactViewer> );}<script setup lang="ts">import { ArtifactViewer, ArtifactViewerClose, ArtifactViewerContent, ArtifactViewerDownload, ArtifactViewerExpand, ArtifactViewerHeader, ArtifactViewerOpenIn, ArtifactViewerPaper, ArtifactViewerTitle,} from "@/components/ai/artifact-viewer";import { Button } from "@/components/ui/button";import { ref } from "vue";
const open = ref(true);const expanded = ref(false);</script>
<template> <Button v-if="!open" size="sm" variant="outline" @click="open = true">Show artifact</Button> <ArtifactViewer v-else :class="expanded ? 'w-full max-w-3xl' : 'w-full max-w-xl'"> <ArtifactViewerHeader> <ArtifactViewerTitle format="PDF">Rebalance report</ArtifactViewerTitle> <ArtifactViewerOpenIn /> <ArtifactViewerDownload /> <ArtifactViewerExpand @click="expanded = !expanded" /> <ArtifactViewerClose @click="open = false" /> </ArtifactViewerHeader> <ArtifactViewerContent> <ArtifactViewerPaper> <div class="font-mono text-[11px] tracking-[2px] text-[#7a7974]"> RESEARCH · MAG4 · OCT 2026 </div> <div class="mt-2.5 font-serif text-[26px] leading-[1.15] font-bold"> Drift stays inside the band </div> <p class="mt-3 text-[13px] leading-[1.7] text-[#3d3c38]"> The keeper checks weights every hour and trades only when a sleeve drifts past 2%. Slippage stays under 1% at current depth. <b>No manual step is needed before the next rebalance.</b> </p> <div class="my-4 h-0.5 bg-[#22406b]" /> <div class="font-serif text-[17px] font-bold text-[#22406b]"> What changed this week </div> </ArtifactViewerPaper> </ArtifactViewerContent> </ArtifactViewer></template><script lang="ts"> import { ArtifactViewer, ArtifactViewerClose, ArtifactViewerContent, ArtifactViewerDownload, ArtifactViewerExpand, ArtifactViewerHeader, ArtifactViewerOpenIn, ArtifactViewerPaper, ArtifactViewerTitle, } from "$lib/components/ai/artifact-viewer"; import { Button } from "$lib/components/ui/button";
let open = $state(true); let expanded = $state(false);</script>
{#if !open} <Button size="sm" variant="outline" onclick={() => (open = true)}>Show artifact</Button>{:else}<ArtifactViewer class={expanded ? "w-full max-w-3xl" : "w-full max-w-xl"}> <ArtifactViewerHeader> <ArtifactViewerTitle format="PDF">Rebalance report</ArtifactViewerTitle> <ArtifactViewerOpenIn /> <ArtifactViewerDownload /> <ArtifactViewerExpand onclick={() => (expanded = !expanded)} /> <ArtifactViewerClose onclick={() => (open = false)} /> </ArtifactViewerHeader> <ArtifactViewerContent> <ArtifactViewerPaper> <div class="font-mono text-[11px] tracking-[2px] text-[#7a7974]">RESEARCH · MAG4 · OCT 2026</div> <div class="mt-2.5 font-serif text-[26px] leading-[1.15] font-bold"> Drift stays inside the band </div> <p class="mt-3 text-[13px] leading-[1.7] text-[#3d3c38]"> The keeper checks weights every hour and trades only when a sleeve drifts past 2%. Slippage stays under 1% at current depth. <b>No manual step is needed before the next rebalance.</b> </p> <div class="my-4 h-0.5 bg-[#22406b]"></div> <div class="font-serif text-[17px] font-bold text-[#22406b]">What changed this week</div> </ArtifactViewerPaper> </ArtifactViewerContent></ArtifactViewer>{/if}Edmi ✦ addition. Built on card, button and tooltip.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-artifact-viewerpnpm dlx shadcn@latest add @edmi-ui/ai-artifact-vieweryarn dlx shadcn@latest add @edmi-ui/ai-artifact-viewerbunx shadcn@latest add @edmi-ui/ai-artifact-viewerPick 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,tooltip,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-artifact-viewerpnpm dlx shadcn-vue@latest add @edmi-ui/ai-artifact-vieweryarn dlx shadcn-vue@latest add @edmi-ui/ai-artifact-viewerbunx shadcn-vue@latest add @edmi-ui/ai-artifact-viewerPick 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,tooltip,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-artifact-viewer.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-artifact-viewer.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-artifact-viewer.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-artifact-viewer.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,tooltip,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { ArtifactViewer, ArtifactViewerClose, ArtifactViewerDownload, ArtifactViewerExpand, ArtifactViewerHeader, ArtifactViewerOpenIn, ArtifactViewerTitle,} from "@/components/ai/artifact-viewer";
<ArtifactViewer> <ArtifactViewerHeader> <ArtifactViewerTitle format="PDF">Rebalance report</ArtifactViewerTitle> <ArtifactViewerOpenIn /> <ArtifactViewerDownload /> <ArtifactViewerExpand /> <ArtifactViewerClose /> </ArtifactViewerHeader> {/* … */}</ArtifactViewer><script setup lang="ts">import { Button } from "@/components/ui/button";</script>
<template> <Button size="sm" variant="outline">Show artifact</Button> <!-- … --></template><script lang="ts"> import { Button } from "$lib/components/ui/button";</script>
{#if !open} <Button size="sm" variant="outline">Show artifact</Button>{:else}<!-- … -->Component source
Section titled “Component source”import { cn } from "cn";import type { ComponentProps, ReactNode } from "react";import { ArrowUpRightIcon, DownloadIcon, ArrowsOutSimpleIcon, XIcon } from "@phosphor-icons/react";import { Button } from "@/registry/edmi/ui/button";import { Card } from "@/registry/edmi/ui/card";import type { Elevation } from "@/registry/edmi/ui/elevation";import { Tooltip, TooltipContent, TooltipTrigger,} from "@/registry/edmi/ui/tooltip";
export type ArtifactViewerProps = Omit< ComponentProps<typeof Card>, "elevation"> & { /** ✦ depth: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation;};
/** Side panel that shows one artifact. */export const ArtifactViewer = ({ className, elevation, ...props}: ArtifactViewerProps) => ( <Card data-slot="ai-artifact-viewer" elevation={elevation} className={cn("gap-0 py-0 text-card-foreground", className)} {...props} />);
export type ArtifactViewerHeaderProps = ComponentProps<"div">;
export const ArtifactViewerHeader = ({ className, ...props}: ArtifactViewerHeaderProps) => ( <div data-slot="ai-artifact-viewer-header" className={cn( "flex h-[46px] shrink-0 items-center gap-1 border-b border-border pr-2 pl-4", className, )} {...props} />);
export type ArtifactViewerTitleProps = ComponentProps<"span"> & { /** Format shown after the title in mono (`PDF`). */ format?: ReactNode;};
export const ArtifactViewerTitle = ({ className, children, format, ...props}: ArtifactViewerTitleProps) => ( <span data-slot="ai-artifact-viewer-title" className={cn("min-w-0 flex-1 truncate text-sm", className)} {...props} > {children} {format && ( <span className="font-mono text-muted-foreground"> · {format}</span> )} </span>);
export type ArtifactViewerActionProps = ComponentProps<typeof Button> & { /** Tooltip and accessible name. */ label: string;};
export const ArtifactViewerAction = ({ label, children, ...props}: ArtifactViewerActionProps) => ( <Tooltip> <TooltipTrigger render={ <Button aria-label={label} size="icon-sm" type="button" variant="ghost" {...props} /> } > {children} </TooltipTrigger> <TooltipContent>{label}</TooltipContent> </Tooltip>);
type PresetProps = Omit<ArtifactViewerActionProps, "label" | "children"> & { label?: string;};
export const ArtifactViewerOpenIn = ({ label = "Open in", ...props}: PresetProps) => ( <ArtifactViewerAction label={label} {...props}> <ArrowUpRightIcon className="size-4" /> </ArtifactViewerAction>);
export const ArtifactViewerDownload = ({ label = "Download", ...props}: PresetProps) => ( <ArtifactViewerAction label={label} {...props}> <DownloadIcon className="size-4" /> </ArtifactViewerAction>);
export const ArtifactViewerExpand = ({ label = "Expand", ...props}: PresetProps) => ( <ArtifactViewerAction label={label} {...props}> <ArrowsOutSimpleIcon className="size-4" /> </ArtifactViewerAction>);
export const ArtifactViewerClose = ({ label = "Close", ...props}: PresetProps) => ( <ArtifactViewerAction label={label} {...props}> <XIcon className="size-4" /> </ArtifactViewerAction>);
export type ArtifactViewerContentProps = ComponentProps<"div">;
/** Scroll area on `--muted`; documents sit on it as paper. */export const ArtifactViewerContent = ({ className, ...props}: ArtifactViewerContentProps) => ( <div data-slot="ai-artifact-viewer-content" className={cn("min-h-0 flex-1 overflow-auto bg-muted p-4", className)} {...props} />);
export type ArtifactViewerPaperProps = ComponentProps<"div">;
/** A white page, in light and dark mode alike (DESIGN 5b rule 5). */export const ArtifactViewerPaper = ({ className, ...props}: ArtifactViewerPaperProps) => ( <div data-slot="ai-artifact-viewer-paper" className={cn( "w-full rounded-[calc(var(--radius)*0.6)] border border-border bg-white px-10 py-[34px] text-[#1f1f1d]", className, )} {...props} />);<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-artifact-viewer" :elevation="elevation" :class="cn('gap-0 py-0 text-card-foreground', props.class)"> <slot /> </Card></template><script setup lang="ts">import { Button } from "@/registry/edmi/ui/button"import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/registry/edmi/ui/tooltip"
// Listeners and classes (`@click`, `class`) land on the button, not on the tooltip root.defineOptions({ inheritAttrs: false })
defineProps<{ /** Tooltip and accessible name. */ label: string}>()</script>
<template> <!-- Own provider so the viewer works without a global TooltipProvider. --> <TooltipProvider> <Tooltip> <TooltipTrigger as-child> <Button v-bind="$attrs" :aria-label="label" size="icon-sm" type="button" variant="ghost"> <slot /> </Button> </TooltipTrigger> <TooltipContent>{{ label }}</TooltipContent> </Tooltip> </TooltipProvider></template><script setup lang="ts">import { X } from "@lucide/vue"import ArtifactViewerAction from "./ArtifactViewerAction.vue"
withDefaults(defineProps<{ label?: string}>(), { label: "Close",})</script>
<template> <ArtifactViewerAction :label="label"> <X class="size-4" /> </ArtifactViewerAction></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> <!-- Scroll area on --muted; documents sit on it as paper. --> <div data-slot="ai-artifact-viewer-content" :class="cn('min-h-0 flex-1 overflow-auto bg-muted p-4', props.class)"> <slot /> </div></template><script setup lang="ts">import { DownloadIcon } from "@lucide/vue"import ArtifactViewerAction from "./ArtifactViewerAction.vue"
withDefaults(defineProps<{ label?: string}>(), { label: "Download",})</script>
<template> <ArtifactViewerAction :label="label"> <DownloadIcon class="size-4" /> </ArtifactViewerAction></template><script setup lang="ts">import { MaximizeIcon } from "@lucide/vue"import ArtifactViewerAction from "./ArtifactViewerAction.vue"
withDefaults(defineProps<{ label?: string}>(), { label: "Expand",})</script>
<template> <ArtifactViewerAction :label="label"> <MaximizeIcon class="size-4" /> </ArtifactViewerAction></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-artifact-viewer-header" :class="cn('flex h-[46px] shrink-0 items-center gap-1 border-b border-border pr-2 pl-4', props.class)" > <slot /> </div></template><script setup lang="ts">import { ArrowUpRightIcon } from "@lucide/vue"import ArtifactViewerAction from "./ArtifactViewerAction.vue"
withDefaults(defineProps<{ label?: string}>(), { label: "Open in",})</script>
<template> <ArtifactViewerAction :label="label"> <ArrowUpRightIcon class="size-4" /> </ArtifactViewerAction></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> <!-- A white page, in light and dark mode alike (DESIGN 5b rule 5). --> <div data-slot="ai-artifact-viewer-paper" :class="cn('w-full rounded-[calc(var(--radius)*0.6)] border border-border bg-white px-10 py-[34px] text-[#1f1f1d]', 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"] /** Format shown after the title in mono (`PDF`); the `format` slot overrides it. */ format?: string}>()</script>
<template> <span data-slot="ai-artifact-viewer-title" :class="cn('min-w-0 flex-1 truncate text-sm', props.class)"> <slot /> <span v-if="format || $slots.format" class="font-mono text-muted-foreground"> · <slot name="format">{{ format }}</slot></span> </span></template>export { default as ArtifactViewer } from "./ArtifactViewer.vue";export { default as ArtifactViewerAction } from "./ArtifactViewerAction.vue";export { default as ArtifactViewerClose } from "./ArtifactViewerClose.vue";export { default as ArtifactViewerContent } from "./ArtifactViewerContent.vue";export { default as ArtifactViewerDownload } from "./ArtifactViewerDownload.vue";export { default as ArtifactViewerExpand } from "./ArtifactViewerExpand.vue";export { default as ArtifactViewerHeader } from "./ArtifactViewerHeader.vue";export { default as ArtifactViewerOpenIn } from "./ArtifactViewerOpenIn.vue";export { default as ArtifactViewerPaper } from "./ArtifactViewerPaper.vue";export { default as ArtifactViewerTitle } from "./ArtifactViewerTitle.vue";<script lang="ts"> import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js"; import * as Tooltip from "$lib/registry/ui/tooltip/index.js";
let { ref = $bindable(null), label, children, ...restProps }: Omit<ButtonProps, "href"> & { /** Tooltip and accessible name. */ label: string; } = $props();</script>
<Tooltip.Provider> <Tooltip.Root> <Tooltip.Trigger> {#snippet child({ props })} <Button bind:ref {...props} {...restProps} aria-label={label} size="icon-sm" type="button" variant="ghost" > {@render children?.()} </Button> {/snippet} </Tooltip.Trigger> <Tooltip.Content>{label}</Tooltip.Content> </Tooltip.Root></Tooltip.Provider><script lang="ts"> import XIcon from "phosphor-svelte/lib/X"; import type { ComponentProps } from "svelte"; import ArtifactViewerAction from "./artifact-viewer-action.svelte";
let { label = "Close", ...restProps }: Omit<ComponentProps<typeof ArtifactViewerAction>, "label" | "children"> & { label?: string; } = $props();</script>
<ArtifactViewerAction {label} {...restProps}> <XIcon class="size-4" /></ArtifactViewerAction><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>
<!-- Scroll area on --muted; documents sit on it as paper. --><div bind:this={ref} data-slot="ai-artifact-viewer-content" class={cn("min-h-0 flex-1 overflow-auto bg-muted p-4", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import DownloadIcon from "phosphor-svelte/lib/Download"; import type { ComponentProps } from "svelte"; import ArtifactViewerAction from "./artifact-viewer-action.svelte";
let { label = "Download", ...restProps }: Omit<ComponentProps<typeof ArtifactViewerAction>, "label" | "children"> & { label?: string; } = $props();</script>
<ArtifactViewerAction {label} {...restProps}> <DownloadIcon class="size-4" /></ArtifactViewerAction><script lang="ts"> import ArrowsOutSimpleIcon from "phosphor-svelte/lib/ArrowsOutSimple"; import type { ComponentProps } from "svelte"; import ArtifactViewerAction from "./artifact-viewer-action.svelte";
let { label = "Expand", ...restProps }: Omit<ComponentProps<typeof ArtifactViewerAction>, "label" | "children"> & { label?: string; } = $props();</script>
<ArtifactViewerAction {label} {...restProps}> <ArrowsOutSimpleIcon class="size-4" /></ArtifactViewerAction><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-artifact-viewer-header" class={cn( "flex h-[46px] shrink-0 items-center gap-1 border-b border-border pr-2 pl-4", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import ArrowUpRightIcon from "phosphor-svelte/lib/ArrowUpRight"; import type { ComponentProps } from "svelte"; import ArtifactViewerAction from "./artifact-viewer-action.svelte";
let { label = "Open in", ...restProps }: Omit<ComponentProps<typeof ArtifactViewerAction>, "label" | "children"> & { label?: string; } = $props();</script>
<ArtifactViewerAction {label} {...restProps}> <ArrowUpRightIcon class="size-4" /></ArtifactViewerAction><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>
<!-- A white page, in light and dark mode alike (DESIGN 5b rule 5). --><div bind:this={ref} data-slot="ai-artifact-viewer-paper" class={cn( "w-full rounded-[calc(var(--radius)*0.6)] border border-border bg-white px-10 py-[34px] text-[#1f1f1d]", 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), format, class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> & { /** Format shown after the title in mono (`PDF`). */ format?: string | Snippet; } = $props();</script>
<span bind:this={ref} data-slot="ai-artifact-viewer-title" class={cn("min-w-0 flex-1 truncate text-sm", className)} {...restProps}> {@render children?.()} {#if format} <span class="font-mono text-muted-foreground"> · {#if typeof format === "string"}{format}{:else}{@render format()}{/if}</span> {/if}</span><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-artifact-viewer" {elevation} class={cn("gap-0 py-0 text-card-foreground", className)} {...restProps}> {@render children?.()}</Card>import ArtifactViewer from "./artifact-viewer.svelte";import ArtifactViewerAction from "./artifact-viewer-action.svelte";import ArtifactViewerClose from "./artifact-viewer-close.svelte";import ArtifactViewerContent from "./artifact-viewer-content.svelte";import ArtifactViewerDownload from "./artifact-viewer-download.svelte";import ArtifactViewerExpand from "./artifact-viewer-expand.svelte";import ArtifactViewerHeader from "./artifact-viewer-header.svelte";import ArtifactViewerOpenIn from "./artifact-viewer-open-in.svelte";import ArtifactViewerPaper from "./artifact-viewer-paper.svelte";import ArtifactViewerTitle from "./artifact-viewer-title.svelte";
export { ArtifactViewer, ArtifactViewerAction, ArtifactViewerClose, ArtifactViewerContent, ArtifactViewerDownload, ArtifactViewerExpand, ArtifactViewerHeader, ArtifactViewerOpenIn, ArtifactViewerPaper, ArtifactViewerTitle,};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. The paper inside never changes.
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 { ArtifactViewer, ArtifactViewerContent, ArtifactViewerDownload, ArtifactViewerHeader, ArtifactViewerPaper, ArtifactViewerTitle,} from "@/components/ai/artifact-viewer";
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> <ArtifactViewer elevation={value}> <ArtifactViewerHeader> <ArtifactViewerTitle format="PDF"> Rebalance report </ArtifactViewerTitle> <ArtifactViewerDownload /> </ArtifactViewerHeader> <ArtifactViewerContent> <ArtifactViewerPaper className="px-6 py-5 text-[13px]"> Drift stays inside the band. </ArtifactViewerPaper> </ArtifactViewerContent> </ArtifactViewer> </div> ))} </div> );}<script setup lang="ts">import { ArtifactViewer, ArtifactViewerContent, ArtifactViewerDownload, ArtifactViewerHeader, ArtifactViewerPaper, ArtifactViewerTitle,} from "@/components/ai/artifact-viewer";
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> <ArtifactViewer :elevation="level.value"> <ArtifactViewerHeader> <ArtifactViewerTitle format="PDF">Rebalance report</ArtifactViewerTitle> <ArtifactViewerDownload /> </ArtifactViewerHeader> <ArtifactViewerContent> <ArtifactViewerPaper class="px-6 py-5 text-[13px]"> Drift stays inside the band. </ArtifactViewerPaper> </ArtifactViewerContent> </ArtifactViewer> </div> </div></template><script lang="ts"> import { ArtifactViewer, ArtifactViewerContent, ArtifactViewerDownload, ArtifactViewerHeader, ArtifactViewerPaper, ArtifactViewerTitle, } from "$lib/components/ai/artifact-viewer";
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> <ArtifactViewer elevation={level.value}> <ArtifactViewerHeader> <ArtifactViewerTitle format="PDF">Rebalance report</ArtifactViewerTitle> <ArtifactViewerDownload /> </ArtifactViewerHeader> <ArtifactViewerContent> <ArtifactViewerPaper class="px-6 py-5 text-[13px]"> Drift stays inside the band. </ArtifactViewerPaper> </ArtifactViewerContent> </ArtifactViewer> </div> {/each}</div>| Part | Prop | Type | Default |
|---|---|---|---|
| ArtifactViewer | elevation, Card props |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
| ArtifactViewerTitle | format |
ReactNode (shown after the title in mono) |
|
| ArtifactViewerAction | label |
string (tooltip and accessible name) |
|
| ArtifactViewerOpenIn / Download / Expand / Close | label, Button props |
string |
|
| ArtifactViewerContent | scroll area on --muted |
||
| ArtifactViewerPaper | white page, same in every mode |
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”- Documents render as paper (white page) on the muted body in light and dark mode (DESIGN 5b rule 5). The paper colours are fixed by design (a document renders as paper in every mode), so they are literal colours and not tokens.