Artifact
The container for a generated deliverable, with the web original's header chrome: title, description, actions, close, scrollable content. No ArtifactTrigger and no versioning ship — neither exists upstream or in the PRD verdict. The PRD's full-screen sheet is the HOST's presentation (the web presents the same container as a side panel): the consumer mounts it inside this registry's Sheet and wires Close to the dismissal. The web action's optional tooltip is dropped — hover surfaces are dead under a thumb; label becomes the accessibility label.
- v0.2.1
- component
- nativewind
- uniwind
- runs in Expo Go
Install
npx @react-native-reusables/cli@latest add https://rnraielements.com/r/nativewind/artifact.jsonnpx @react-native-reusables/cli@latest add https://rnraielements.com/r/uniwind/artifact.jsonDependencies
No required npm packages — this item installs through the registry dependencies below.
https://reactnativereusables.com/r/nativewind/button.json |
https://reactnativereusables.com/r/nativewind/card.json |
https://reactnativereusables.com/r/nativewind/icon.json |
https://reactnativereusables.com/r/nativewind/text.json |
https://reactnativereusables.com/r/uniwind/button.json |
https://reactnativereusables.com/r/uniwind/card.json |
https://reactnativereusables.com/r/uniwind/icon.json |
https://reactnativereusables.com/r/uniwind/text.json |
AI SDK peers:ai@^7.0.89@ai-sdk/react@^4.0.92
Source
import { Button } from '@/registry/nativewind/components/ui/button';
import { Card } from '@/registry/nativewind/components/ui/card';
import { Icon } from '@/registry/nativewind/components/ui/icon';
import { Text } from '@/registry/nativewind/components/ui/text';
import { cn } from '@/registry/nativewind/lib/utils';
import { XIcon, type LucideIcon } from 'lucide-react-native';
import * as React from 'react';
import { ScrollView, View, type ViewProps } from 'react-native';
/**
* Artifact — the container for a generated deliverable (code, document, output), with
* the header chrome the web original ships: title, description, actions, close, and a
* scrollable content area.
*
* TWO BRIEF CORRECTIONS AGAINST THE TASK'S STARTING POSITION, on the record:
* 1. NO ArtifactTrigger AND NO VERSIONING — verified against the current upstream
* artifact.tsx (Artifact/Header/Close/Title/Description/Actions/Action/Content —
* nothing else) and against the KB (same part set). The PRD's verdict says nothing
* of versions either. There is no version state or onVersionChange to port or to
* test, so none ships; inventing one would be an API the web never had.
* 2. THE FULL-SCREEN SHEET IS THE HOST'S PRESENTATION, NOT THIS COMPONENT. The PRD
* verdict ("a phone has no side; it becomes a full-screen sheet with a return
* control") describes how the container is PRESENTED — on the web the same container
* is presented as a side panel, also host layout. The component ships the container
* plus the close/return control; the consumer mounts it inside this registry's Sheet
* (which backs exactly this: "drawer, command, sidebar, panel…") and wires Close to
* the sheet's dismissal, exactly as the web host wires ArtifactClose.
*
* DROPPED: the web ArtifactAction's optional Tooltip wrapper — a hover surface, dead
* under a thumb (queue.tsx's always-visible precedent). The action's `label` becomes
* the accessibility label instead.
*/
type ArtifactProps = ViewProps & { children?: React.ReactNode };
function Artifact({ className, children, ...props }: ArtifactProps) {
return (
// Composed Card per the porting verdict, overridden to the web container's shape:
// no default padding/gap (the header owns its own), clipped corners, background on
// `background` exactly as the web writes it.
<Card className={cn('gap-0 overflow-hidden rounded-lg bg-background py-0', className)} {...props}>
{children}
</Card>
);
}
function ArtifactHeader({ className, ...props }: ViewProps) {
return (
<View
className={cn('flex-row items-center justify-between gap-2 border-b border-border bg-muted/50 px-4 py-3', className)}
{...props}
/>
);
}
type ArtifactCloseProps = Omit<React.ComponentProps<typeof Button>, 'children'> & {
/** Overrides the default X mark. */
children?: React.ReactNode;
className?: string;
};
/** The return control. The consumer wires onPress to whatever presented the artifact. */
function ArtifactClose({ children, className, ...props }: ArtifactCloseProps) {
return (
<Button
variant="ghost"
size="icon"
// House formula (queue/confirmation precedent): RNR's h-10 + hitSlop 2/side is
// the 44pt platform minimum, with no pixel of extra chrome.
hitSlop={{ top: 2, bottom: 2, left: 2, right: 2 }}
accessibilityLabel="Close"
className={cn('rounded-full', className)}
{...props}
>
{children ?? <Icon as={XIcon} size={16} className="text-muted-foreground" />}
</Button>
);
}
function ArtifactTitle({ className, ...props }: React.ComponentProps<typeof Text>) {
return (
<Text
numberOfLines={1}
className={cn('flex-1 text-sm font-medium text-foreground', className)}
{...props}
/>
);
}
function ArtifactDescription({ className, ...props }: React.ComponentProps<typeof Text>) {
return <Text numberOfLines={1} className={cn('text-sm text-muted-foreground', className)} {...props} />;
}
function ArtifactActions({ className, ...props }: ViewProps) {
return <View className={cn('flex-row items-center gap-1', className)} {...props} />;
}
type ArtifactActionProps = {
/** The icon. Renders alone unless children are supplied (web precedence). */
icon?: LucideIcon;
/** The accessibility label — the web's tooltip/sr-only pair, on a touch surface. */
label: string;
onPress?: () => void;
disabled?: boolean;
/** Overrides the icon. */
children?: React.ReactNode;
className?: string;
};
function ArtifactAction({ icon, label, onPress, disabled, children, className }: ArtifactActionProps) {
return (
<Button
variant="ghost"
size="icon"
onPress={onPress}
disabled={disabled}
accessibilityLabel={label}
accessibilityState={{ disabled: Boolean(disabled) }}
hitSlop={{ top: 2, bottom: 2, left: 2, right: 2 }}
className={cn('rounded-full', className)}
>
{children ?? (icon ? <Icon as={icon} size={16} className="text-muted-foreground" /> : null)}
</Button>
);
}
/**
* The content area. The web's `flex-1 overflow-auto p-4` — on React Native the
* overflow-auto leg IS a ScrollView, so long artifacts scroll inside the container
* (terminal.tsx's different-axis rule keeps it from fighting the transcript scroller).
*/
function ArtifactContent({ className, children, ...props }: ViewProps & { children?: React.ReactNode }) {
return (
<ScrollView
nestedScrollEnabled
showsVerticalScrollIndicator={false}
className={cn('flex-1', className)}
contentContainerClassName="p-4"
{...props}
>
{children}
</ScrollView>
);
}
export { Artifact, ArtifactAction, ArtifactActions, ArtifactClose, ArtifactContent, ArtifactDescription, ArtifactHeader, ArtifactTitle };
Props
| Name | Type | Default | Description |
|---|---|---|---|
| children | React.ReactNode | — | — |
Theming
Themed via bg-background, border-border, bg-muted, text-muted-foreground, text-foreground.
Usage
() => (
<Artifact>
<ArtifactHeader>
<ArtifactTitle>gallery-manifest.ts</ArtifactTitle>
<ArtifactDescription>The derived gallery item list — 56 entries, never hand-edited</ArtifactDescription>
<ArtifactActions>
<ArtifactAction icon={undefined} label="Download" onPress={() => {}} />
<ArtifactClose />
</ArtifactActions>
</ArtifactHeader>
<ArtifactContent>
<Text className="text-sm text-muted-foreground">Generated by scripts/build-gallery-manifest.ts</Text>
</ArtifactContent>
</Artifact>
)