Skip to content

Commit 933432d

Browse files
Implement Sandbox component with collapsible sections and tabs; export getStatusBadge function for external use
1 parent 485a75e commit 933432d

File tree

2 files changed

+152
-1
lines changed

2 files changed

+152
-1
lines changed

packages/elements/src/sandbox.tsx

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
'use client';
2+
3+
import type { ToolUIPart } from 'ai';
4+
import { ChevronDownIcon, Code } from 'lucide-react';
5+
import type { ComponentProps } from 'react';
6+
import type { BundledLanguage } from 'shiki';
7+
import {
8+
Collapsible,
9+
CollapsibleContent,
10+
CollapsibleTrigger,
11+
} from '@/components/ui/collapsible';
12+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
13+
import { cn } from '@/lib/utils';
14+
import { CodeBlock, CodeBlockCopyButton } from './code-block';
15+
import { getStatusBadge } from './tool';
16+
17+
export type SandboxRootProps = ComponentProps<typeof Collapsible>;
18+
19+
export const Sandbox = ({ className, ...props }: SandboxRootProps) => (
20+
<Collapsible
21+
className={cn('not-prose group mb-4 w-full rounded-md border', className)}
22+
defaultOpen
23+
{...props}
24+
/>
25+
);
26+
27+
export type SandboxHeaderProps = {
28+
title?: string;
29+
state: ToolUIPart['state'];
30+
className?: string;
31+
};
32+
33+
export const SandboxHeader = ({
34+
className,
35+
title,
36+
state,
37+
...props
38+
}: SandboxHeaderProps) => (
39+
<CollapsibleTrigger
40+
className={cn(
41+
'flex w-full items-center justify-between gap-4 p-3',
42+
className
43+
)}
44+
{...props}
45+
>
46+
<div className="flex items-center gap-2">
47+
<Code className="size-4 text-muted-foreground" />
48+
<span className="font-medium text-sm">{title}</span>
49+
{getStatusBadge(state)}
50+
</div>
51+
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
52+
</CollapsibleTrigger>
53+
);
54+
55+
export type SandboxContentProps = ComponentProps<typeof CollapsibleContent>;
56+
57+
export const SandboxContent = ({
58+
className,
59+
...props
60+
}: SandboxContentProps) => (
61+
<CollapsibleContent
62+
className={cn(
63+
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in',
64+
className
65+
)}
66+
{...props}
67+
/>
68+
);
69+
70+
export type SandboxTabsProps = ComponentProps<typeof Tabs>;
71+
72+
export const SandboxTabs = ({ className, ...props }: SandboxTabsProps) => (
73+
<Tabs className={cn('w-full', className)} {...props} />
74+
);
75+
76+
export type SandboxTabsListProps = ComponentProps<typeof TabsList>;
77+
78+
export const SandboxTabsList = ({
79+
className,
80+
...props
81+
}: SandboxTabsListProps) => (
82+
<div className="flex items-center border-border border-b">
83+
<TabsList
84+
className={cn(
85+
'h-auto rounded-none border-0 bg-transparent p-0',
86+
className
87+
)}
88+
{...props}
89+
/>
90+
</div>
91+
);
92+
93+
export type SandboxTabsTriggerProps = ComponentProps<typeof TabsTrigger>;
94+
95+
export const SandboxTabsTrigger = ({
96+
className,
97+
...props
98+
}: SandboxTabsTriggerProps) => (
99+
<TabsTrigger
100+
className={cn(
101+
'rounded-none border-0 border-transparent border-b-2 px-4 py-2 font-medium text-muted-foreground text-sm transition-colors data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:text-foreground data-[state=active]:shadow-none',
102+
className
103+
)}
104+
{...props}
105+
/>
106+
);
107+
108+
export type SandboxCopyButtonProps = {
109+
code: string;
110+
className?: string;
111+
};
112+
113+
export const SandboxCopyButton = ({
114+
code,
115+
className,
116+
}: SandboxCopyButtonProps) => (
117+
<div className={cn('ml-auto flex items-center pr-2', className)}>
118+
<CodeBlockCopyButton
119+
className="opacity-0 transition-opacity duration-200 group-hover:opacity-100"
120+
onCopy={() => navigator.clipboard.writeText(code)}
121+
size="sm"
122+
/>
123+
</div>
124+
);
125+
126+
export type SandboxCodeProps = {
127+
code: string;
128+
language?: BundledLanguage;
129+
className?: string;
130+
};
131+
132+
export const SandboxCode = ({
133+
code,
134+
language = 'tsx',
135+
className,
136+
}: SandboxCodeProps) => (
137+
<TabsContent className={cn('mt-0 text-sm', className)} value="code">
138+
<CodeBlock code={code} language={language} showLineNumbers />
139+
</TabsContent>
140+
);
141+
142+
export type SandboxOutputProps = {
143+
output: string;
144+
className?: string;
145+
};
146+
147+
export const SandboxOutput = ({ output, className }: SandboxOutputProps) => (
148+
<TabsContent className={cn('mt-0 text-sm', className)} value="output">
149+
<CodeBlock code={output} language="log" showLineNumbers />
150+
</TabsContent>
151+
);

packages/elements/src/tool.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export type ToolHeaderProps = {
3636
className?: string;
3737
};
3838

39-
const getStatusBadge = (status: ToolUIPart["state"]) => {
39+
export const getStatusBadge = (status: ToolUIPart["state"]) => {
4040
const labels: Record<ToolUIPart["state"], string> = {
4141
"input-streaming": "Pending",
4242
"input-available": "Running",

0 commit comments

Comments
 (0)