Merge pull request #58 from johndoe6345789/codex/refactor-jsondemopage-to-improve-structure

Refactor JSON demo and showcase into configs and smaller components
This commit is contained in:
2026-01-18 00:44:13 +00:00
committed by GitHub
14 changed files with 797 additions and 468 deletions

View File

@@ -1,16 +1,11 @@
import { JSONUIRenderer } from '@/lib/json-ui'
import { UIComponent } from '@/lib/json-ui/schema'
import { toast } from 'sonner'
import { useKV } from '@/hooks/use-kv'
import { useState } from 'react'
import { buildDemoPageSchema, demoCopy, demoInitialTodos } from '@/components/json-demo/schema'
export function JSONDemoPage() {
const [todos, setTodos] = useKV('json-demo-todos', [
{ id: 1, text: 'Design JSON schema', completed: true },
{ id: 2, text: 'Build atomic components', completed: false },
{ id: 3, text: 'Create custom hooks', completed: false },
])
const [todos, setTodos] = useKV('json-demo-todos', demoInitialTodos)
const [newTodo, setNewTodo] = useState('')
const handleAction = (handler: any, event?: any) => {
@@ -19,13 +14,13 @@ export function JSONDemoPage() {
if (newTodo.trim()) {
setTodos((current: any) => [
...current,
{ id: Date.now(), text: newTodo, completed: false }
{ id: Date.now(), text: newTodo, completed: false },
])
setNewTodo('')
toast.success('Todo added!')
toast.success(demoCopy.toastAdded)
}
break
case 'toggle-todo':
setTodos((current: any) =>
current.map((todo: any) =>
@@ -35,152 +30,21 @@ export function JSONDemoPage() {
)
)
break
case 'delete-todo':
setTodos((current: any) =>
current.filter((todo: any) => todo.id !== handler.params?.id)
)
toast.success('Todo deleted')
toast.success(demoCopy.toastDeleted)
break
case 'update-input':
setNewTodo(event.target.value)
break
}
}
const pageSchema: UIComponent = {
id: 'json-demo-page',
type: 'div',
className: 'h-full overflow-auto p-6',
children: [
{
id: 'header',
type: 'div',
className: 'mb-6',
children: [
{
id: 'title',
type: 'h1',
className: 'text-3xl font-bold mb-2',
children: 'JSON-Driven UI Demo'
},
{
id: 'description',
type: 'p',
className: 'text-muted-foreground',
children: 'This entire page is built from a JSON schema - no JSX required!'
}
]
},
{
id: 'main-card',
type: 'Card',
className: 'max-w-2xl',
children: [
{
id: 'card-header',
type: 'CardHeader',
children: [
{
id: 'card-title',
type: 'CardTitle',
children: 'Todo List'
},
{
id: 'card-description',
type: 'CardDescription',
children: 'Manage your tasks with JSON-powered UI'
}
]
},
{
id: 'card-content',
type: 'CardContent',
className: 'space-y-4',
children: [
{
id: 'input-group',
type: 'div',
className: 'flex gap-2',
children: [
{
id: 'todo-input',
type: 'Input',
props: {
placeholder: 'Enter a new todo...',
value: newTodo
},
events: {
onChange: { action: 'update-input' }
}
},
{
id: 'add-button',
type: 'Button',
props: {
children: 'Add'
},
events: {
onClick: { action: 'add-todo' }
}
}
]
},
{
id: 'todo-list',
type: 'div',
className: 'space-y-2',
children: todos.map((todo: any) => ({
id: `todo-${todo.id}`,
type: 'div',
className: 'flex items-center gap-2 p-3 rounded-lg border bg-card',
children: [
{
id: `checkbox-${todo.id}`,
type: 'Checkbox',
props: {
checked: todo.completed
},
events: {
onCheckedChange: {
action: 'toggle-todo',
params: { id: todo.id }
}
}
},
{
id: `text-${todo.id}`,
type: 'span',
className: todo.completed
? 'flex-1 line-through text-muted-foreground'
: 'flex-1',
children: todo.text
},
{
id: `delete-${todo.id}`,
type: 'Button',
props: {
variant: 'ghost',
size: 'sm',
children: '×'
},
events: {
onClick: {
action: 'delete-todo',
params: { id: todo.id }
}
}
}
]
}))
}
]
}
]
}
]
}
const pageSchema = buildDemoPageSchema(todos, newTodo)
return (
<JSONUIRenderer

View File

@@ -1,214 +1,32 @@
import { ReactNode } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { StatCard } from '@/components/atoms'
import * as Icons from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
import pageRendererCopy from '@/config/json-page-renderer.json'
import { PageSectionRenderer } from '@/components/json-page-renderer/SectionRenderer'
import { ComponentRendererProps } from '@/components/json-page-renderer/types'
export interface PageComponentConfig {
id: string
type: string
[key: string]: any
}
export interface PageLayoutConfig {
type: string
spacing?: string
sections?: PageSectionConfig[]
[key: string]: any
}
export interface PageSectionConfig {
type: string
[key: string]: any
}
export interface LegacyPageSchema {
id: string
layout: PageLayoutConfig
dashboardCards?: any[]
statCards?: any[]
[key: string]: any
}
export interface ComponentRendererProps {
config?: Record<string, any>
schema?: LegacyPageSchema
data?: Record<string, any>
functions?: Record<string, (...args: any[]) => any>
}
function resolveBinding(binding: string, data: Record<string, any>): any {
try {
const func = new Function(...Object.keys(data), `return ${binding}`)
return func(...Object.values(data))
} catch {
return binding
}
}
function getIcon(iconName: string, props?: any) {
const IconComponent = (Icons as any)[iconName]
if (!IconComponent) return null
return <IconComponent size={24} weight="duotone" {...props} />
}
export function JSONPageRenderer({ config, schema, data = {}, functions = {} }: ComponentRendererProps) {
export function JSONPageRenderer({
config,
schema,
data = {},
functions = {},
}: ComponentRendererProps) {
const pageSchema = config || schema
if (!pageSchema) {
return <div>No schema provided</div>
}
const renderSection = (section: PageSectionConfig, index: number): ReactNode => {
switch (section.type) {
case 'header':
return (
<div key={index}>
<h1 className="text-3xl font-bold mb-2">{section.title}</h1>
{section.description && (
<p className="text-muted-foreground">{section.description}</p>
)}
</div>
)
case 'cards': {
const cards = pageSchema[section.items as string] || []
return (
<div key={index} className={cn('space-y-' + (section.spacing || '4'))}>
{cards.map((card: any) => renderCard(card))}
</div>
)
}
case 'grid': {
const gridItems = pageSchema[section.items as string] || []
const { sm = 1, md = 2, lg = 3 } = section.columns || {}
return (
<div
key={index}
className={cn(
'grid gap-' + (section.gap || '4'),
`grid-cols-${sm} md:grid-cols-${md} lg:grid-cols-${lg}`
)}
>
{gridItems.map((item: any) => renderStatCard(item))}
</div>
)
}
default:
return null
}
}
const renderCard = (card: any): ReactNode => {
const icon = card.icon ? getIcon(card.icon) : null
if (card.type === 'gradient-card') {
const computeFn = functions[card.dataSource?.compute]
const computedData = computeFn ? computeFn(data) : {}
return (
<Card
key={card.id}
className={cn(
'bg-gradient-to-br border-primary/20',
card.gradient
)}
>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{icon && <span className="text-primary">{icon}</span>}
{card.title}
</CardTitle>
<CardDescription>{card.description}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{card.components?.map((comp: any, idx: number) =>
renderSubComponent(comp, computedData, idx)
)}
</CardContent>
</Card>
)
}
return (
<Card key={card.id}>
<CardHeader>
<CardTitle>{card.title}</CardTitle>
</CardHeader>
<CardContent>
{card.component && renderCustomComponent(card.component, card.props || {})}
</CardContent>
</Card>
)
}
const renderSubComponent = (comp: any, dataContext: any, key: number): ReactNode => {
const value = dataContext[comp.binding]
switch (comp.type) {
case 'metric':
return (
<div key={key} className="flex items-center justify-between">
<span className={cn(
'font-bold',
comp.size === 'large' ? 'text-4xl' : 'text-2xl'
)}>
{comp.format === 'percentage' ? `${value}%` : value}
</span>
</div>
)
case 'badge': {
const variant = value === 'ready' ? comp.variants?.ready : comp.variants?.inProgress
return (
<Badge key={key} variant={variant?.variant as any}>
{variant?.label}
</Badge>
)
}
case 'progress':
return <Progress key={key} value={value} className="h-3" />
case 'text':
return (
<p key={key} className={comp.className}>
{value}
</p>
)
default:
return null
}
}
const renderStatCard = (stat: any): ReactNode => {
const icon = stat.icon ? getIcon(stat.icon) : undefined
const value = resolveBinding(stat.dataBinding, data)
const description = `${value} ${stat.description}`
return (
<StatCard
key={stat.id}
icon={icon}
title={stat.title}
value={value}
description={description}
color={stat.color}
/>
)
}
const renderCustomComponent = (componentName: string, props: any): ReactNode => {
return <div>Custom component: {componentName}</div>
return <div>{pageRendererCopy.fallbackText}</div>
}
return (
<div className="h-full overflow-auto p-6 space-y-6">
{pageSchema.layout.sections?.map((section, index) => renderSection(section, index))}
{pageSchema.layout.sections?.map((section, index) => (
<PageSectionRenderer
key={index}
index={index}
section={section}
pageSchema={pageSchema}
data={data}
functions={functions}
/>
))}
</div>
)
}
export type { ComponentRendererProps } from '@/components/json-page-renderer/types'

View File

@@ -1,138 +1,64 @@
import { useState } from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { JSONUIPage } from '@/components/JSONUIPage'
import { Separator } from '@/components/ui/separator'
import { Badge } from '@/components/ui/badge'
import { useMemo, useState } from 'react'
import showcaseCopy from '@/config/ui-examples/showcase.json'
import dashboardExample from '@/config/ui-examples/dashboard.json'
import formExample from '@/config/ui-examples/form.json'
import tableExample from '@/config/ui-examples/table.json'
import settingsExample from '@/config/ui-examples/settings.json'
import { FileCode, Eye, Code, ChartBar, ListBullets, Table, Gear } from '@phosphor-icons/react'
import { FileCode, ChartBar, ListBullets, Table, Gear } from '@phosphor-icons/react'
import { ShowcaseHeader } from '@/components/json-ui-showcase/ShowcaseHeader'
import { ShowcaseTabs } from '@/components/json-ui-showcase/ShowcaseTabs'
import { ShowcaseFooter } from '@/components/json-ui-showcase/ShowcaseFooter'
import { ShowcaseExample } from '@/components/json-ui-showcase/types'
const exampleConfigs = {
dashboard: dashboardExample,
form: formExample,
table: tableExample,
settings: settingsExample,
}
const exampleIcons = {
ChartBar,
ListBullets,
Table,
Gear,
}
export function JSONUIShowcase() {
const [selectedExample, setSelectedExample] = useState('dashboard')
const [selectedExample, setSelectedExample] = useState(showcaseCopy.defaultExampleKey)
const [showJSON, setShowJSON] = useState(false)
const examples = {
dashboard: {
name: 'Dashboard',
description: 'Complete dashboard with stats, activity feed, and quick actions',
icon: ChartBar,
config: dashboardExample,
},
form: {
name: 'Form',
description: 'Dynamic form with validation and data binding',
icon: ListBullets,
config: formExample,
},
table: {
name: 'Data Table',
description: 'Interactive table with row actions and looping',
icon: Table,
config: tableExample,
},
settings: {
name: 'Settings',
description: 'Tabbed settings panel with switches and selections',
icon: Gear,
config: settingsExample,
},
}
const examples = useMemo<ShowcaseExample[]>(() => {
return showcaseCopy.examples.map((example) => {
const icon = exampleIcons[example.icon as keyof typeof exampleIcons] || FileCode
const config = exampleConfigs[example.configKey as keyof typeof exampleConfigs]
const currentExample = examples[selectedExample as keyof typeof examples]
return {
key: example.key,
name: example.name,
description: example.description,
icon,
config,
}
})
}, [])
return (
<div className="h-full flex flex-col bg-background">
<div className="border-b border-border bg-card px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">JSON UI System</h1>
<p className="text-sm text-muted-foreground mt-1">
Build complex UIs from declarative JSON configurations
</p>
</div>
<Badge variant="secondary" className="font-mono">
EXPERIMENTAL
</Badge>
</div>
</div>
<ShowcaseHeader copy={showcaseCopy.header} />
<div className="flex-1 overflow-hidden">
<Tabs value={selectedExample} onValueChange={setSelectedExample} className="h-full flex flex-col">
<div className="border-b border-border bg-muted/30 px-6">
<div className="flex items-center justify-between">
<TabsList className="bg-transparent border-0">
{Object.entries(examples).map(([key, example]) => {
const Icon = example.icon
return (
<TabsTrigger key={key} value={key} className="gap-2">
<Icon size={16} />
{example.name}
</TabsTrigger>
)
})}
</TabsList>
<Button
variant="outline"
size="sm"
onClick={() => setShowJSON(!showJSON)}
className="gap-2"
>
{showJSON ? <Eye size={16} /> : <Code size={16} />}
{showJSON ? 'Show Preview' : 'Show JSON'}
</Button>
</div>
</div>
<div className="flex-1 overflow-auto">
{Object.entries(examples).map(([key, example]) => (
<TabsContent key={key} value={key} className="h-full m-0">
{showJSON ? (
<div className="p-6">
<Card>
<CardHeader>
<CardTitle className="text-lg">JSON Configuration</CardTitle>
<CardDescription>
{example.description}
</CardDescription>
</CardHeader>
<CardContent>
<pre className="bg-muted p-4 rounded-lg overflow-auto text-sm max-h-[600px]">
<code>{JSON.stringify(example.config, null, 2)}</code>
</pre>
</CardContent>
</Card>
</div>
) : (
<JSONUIPage jsonConfig={example.config} />
)}
</TabsContent>
))}
</div>
</Tabs>
<ShowcaseTabs
examples={examples}
copy={showcaseCopy.tabs}
selectedExample={selectedExample}
onSelectedExampleChange={setSelectedExample}
showJSON={showJSON}
onShowJSONChange={setShowJSON}
/>
</div>
<div className="border-t border-border bg-card px-6 py-3">
<div className="flex items-center gap-6 text-xs text-muted-foreground">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500" />
<span>Fully declarative - no React code needed</span>
</div>
<Separator orientation="vertical" className="h-4" />
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500" />
<span>Data binding with automatic updates</span>
</div>
<Separator orientation="vertical" className="h-4" />
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-purple-500" />
<span>Event handlers and actions</span>
</div>
</div>
</div>
<ShowcaseFooter items={showcaseCopy.footer.items} />
</div>
)
}

View File

@@ -0,0 +1,101 @@
import demoConfig from '@/config/json-demo.json'
import { UIComponent } from '@/lib/json-ui/schema'
type TodoItem = {
id: number
text: string
completed: boolean
}
type DemoCopy = {
toastAdded: string
toastDeleted: string
deleteButtonLabel: string
}
const baseSchema = demoConfig.schema as UIComponent
const cloneSchema = () => JSON.parse(JSON.stringify(baseSchema)) as UIComponent
const findComponentById = (component: UIComponent, id: string): UIComponent | null => {
if (component.id === id) {
return component
}
if (!Array.isArray(component.children)) {
return null
}
for (const child of component.children) {
if (typeof child === 'object' && child && 'id' in child) {
const match = findComponentById(child as UIComponent, id)
if (match) {
return match
}
}
}
return null
}
const buildTodoItem = (todo: TodoItem, copy: DemoCopy): UIComponent => ({
id: `todo-${todo.id}`,
type: 'div',
className: 'flex items-center gap-2 p-3 rounded-lg border bg-card',
children: [
{
id: `checkbox-${todo.id}`,
type: 'Checkbox',
props: {
checked: todo.completed,
},
events: {
onCheckedChange: {
action: 'toggle-todo',
params: { id: todo.id },
},
},
},
{
id: `text-${todo.id}`,
type: 'span',
className: todo.completed
? 'flex-1 line-through text-muted-foreground'
: 'flex-1',
children: todo.text,
},
{
id: `delete-${todo.id}`,
type: 'Button',
props: {
variant: 'ghost',
size: 'sm',
children: copy.deleteButtonLabel,
},
events: {
onClick: {
action: 'delete-todo',
params: { id: todo.id },
},
},
},
],
})
export const demoCopy = demoConfig.copy as DemoCopy
export const demoInitialTodos = demoConfig.initialTodos as TodoItem[]
export const buildDemoPageSchema = (todos: TodoItem[], newTodo: string): UIComponent => {
const schema = cloneSchema()
const inputComponent = findComponentById(schema, 'todo-input')
if (inputComponent?.props) {
inputComponent.props.value = newTodo
}
const todoList = findComponentById(schema, 'todo-list')
if (todoList) {
todoList.children = todos.map((todo) => buildTodoItem(todo, demoCopy))
}
return schema
}

View File

@@ -0,0 +1,250 @@
import { ReactNode } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { StatCard } from '@/components/atoms'
import { cn } from '@/lib/utils'
import { getIcon, resolveBinding } from './utils'
import { LegacyPageSchema, PageSectionConfig } from './types'
interface PageSectionRendererProps {
index: number
section: PageSectionConfig
pageSchema: LegacyPageSchema
data: Record<string, any>
functions: Record<string, (...args: any[]) => any>
}
export function PageSectionRenderer({
index,
section,
pageSchema,
data,
functions,
}: PageSectionRendererProps): ReactNode {
switch (section.type) {
case 'header':
return (
<HeaderSection
key={index}
title={section.title}
description={section.description}
/>
)
case 'cards':
return (
<CardSection
key={index}
cards={pageSchema[section.items as string] || []}
spacing={section.spacing}
data={data}
functions={functions}
/>
)
case 'grid':
return (
<GridSection
key={index}
items={pageSchema[section.items as string] || []}
columns={section.columns}
gap={section.gap}
data={data}
/>
)
default:
return null
}
}
interface HeaderSectionProps {
title?: string
description?: string
}
function HeaderSection({ title, description }: HeaderSectionProps) {
return (
<div>
<h1 className="text-3xl font-bold mb-2">{title}</h1>
{description && (
<p className="text-muted-foreground">{description}</p>
)}
</div>
)
}
interface CardSectionProps {
cards: any[]
spacing?: string
data: Record<string, any>
functions: Record<string, (...args: any[]) => any>
}
function CardSection({ cards, spacing, data, functions }: CardSectionProps) {
return (
<div className={cn('space-y-' + (spacing || '4'))}>
{cards.map((card) => (
<PageCard
key={card.id}
card={card}
data={data}
functions={functions}
/>
))}
</div>
)
}
interface PageCardProps {
card: any
data: Record<string, any>
functions: Record<string, (...args: any[]) => any>
}
function PageCard({ card, data, functions }: PageCardProps) {
const icon = card.icon ? getIcon(card.icon) : null
if (card.type === 'gradient-card') {
const computeFn = functions[card.dataSource?.compute]
const computedData = computeFn ? computeFn(data) : {}
return (
<Card className={cn('bg-gradient-to-br border-primary/20', card.gradient)}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{icon && <span className="text-primary">{icon}</span>}
{card.title}
</CardTitle>
<CardDescription>{card.description}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{card.components?.map((comp: any, idx: number) => (
<CardSubComponent
key={`${card.id}-${idx}`}
component={comp}
dataContext={computedData}
/>
))}
</CardContent>
</Card>
)
}
return (
<Card>
<CardHeader>
<CardTitle>{card.title}</CardTitle>
</CardHeader>
<CardContent>
<CustomComponentPlaceholder
componentName={card.component}
props={card.props || {}}
/>
</CardContent>
</Card>
)
}
interface CardSubComponentProps {
component: any
dataContext: Record<string, any>
}
function CardSubComponent({ component, dataContext }: CardSubComponentProps) {
const value = dataContext[component.binding]
switch (component.type) {
case 'metric':
return (
<div className="flex items-center justify-between">
<span
className={cn(
'font-bold',
component.size === 'large' ? 'text-4xl' : 'text-2xl'
)}
>
{component.format === 'percentage' ? `${value}%` : value}
</span>
</div>
)
case 'badge': {
const variant =
value === 'ready' ? component.variants?.ready : component.variants?.inProgress
return (
<Badge variant={variant?.variant as any}>
{variant?.label}
</Badge>
)
}
case 'progress':
return <Progress value={value} className="h-3" />
case 'text':
return <p className={component.className}>{value}</p>
default:
return null
}
}
interface GridSectionProps {
items: any[]
columns?: {
sm?: number
md?: number
lg?: number
}
gap?: string
data: Record<string, any>
}
function GridSection({ items, columns, gap, data }: GridSectionProps) {
const { sm = 1, md = 2, lg = 3 } = columns || {}
return (
<div
className={cn(
'grid gap-' + (gap || '4'),
`grid-cols-${sm} md:grid-cols-${md} lg:grid-cols-${lg}`
)}
>
{items.map((item) => (
<StatCardRenderer key={item.id} stat={item} data={data} />
))}
</div>
)
}
interface StatCardRendererProps {
stat: any
data: Record<string, any>
}
function StatCardRenderer({ stat, data }: StatCardRendererProps) {
const icon = stat.icon ? getIcon(stat.icon) : undefined
const value = resolveBinding(stat.dataBinding, data)
const description = `${value} ${stat.description}`
return (
<StatCard
icon={icon}
title={stat.title}
value={value}
description={description}
color={stat.color}
/>
)
}
interface CustomComponentPlaceholderProps {
componentName?: string
props: Record<string, any>
}
function CustomComponentPlaceholder({ componentName }: CustomComponentPlaceholderProps) {
return <div>Custom component: {componentName}</div>
}

View File

@@ -0,0 +1,32 @@
export interface PageComponentConfig {
id: string
type: string
[key: string]: any
}
export interface PageLayoutConfig {
type: string
spacing?: string
sections?: PageSectionConfig[]
[key: string]: any
}
export interface PageSectionConfig {
type: string
[key: string]: any
}
export interface LegacyPageSchema {
id: string
layout: PageLayoutConfig
dashboardCards?: any[]
statCards?: any[]
[key: string]: any
}
export interface ComponentRendererProps {
config?: Record<string, any>
schema?: LegacyPageSchema
data?: Record<string, any>
functions?: Record<string, (...args: any[]) => any>
}

View File

@@ -0,0 +1,16 @@
import * as Icons from '@phosphor-icons/react'
export function resolveBinding(binding: string, data: Record<string, any>): any {
try {
const func = new Function(...Object.keys(data), `return ${binding}`)
return func(...Object.values(data))
} catch {
return binding
}
}
export function getIcon(iconName: string, props?: any) {
const IconComponent = (Icons as any)[iconName]
if (!IconComponent) return null
return <IconComponent size={24} weight="duotone" {...props} />
}

View File

@@ -0,0 +1,27 @@
import { Fragment } from 'react'
import { Separator } from '@/components/ui/separator'
import { ShowcaseFooterItem } from './types'
interface ShowcaseFooterProps {
items: ShowcaseFooterItem[]
}
export function ShowcaseFooter({ items }: ShowcaseFooterProps) {
return (
<div className="border-t border-border bg-card px-6 py-3">
<div className="flex items-center gap-6 text-xs text-muted-foreground">
{items.map((item, index) => (
<Fragment key={item.label}>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${item.colorClass}`} />
<span>{item.label}</span>
</div>
{index < items.length - 1 && (
<Separator orientation="vertical" className="h-4" />
)}
</Fragment>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { Badge } from '@/components/ui/badge'
import { ShowcaseHeaderCopy } from './types'
interface ShowcaseHeaderProps {
copy: ShowcaseHeaderCopy
}
export function ShowcaseHeader({ copy }: ShowcaseHeaderProps) {
return (
<div className="border-b border-border bg-card px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">{copy.title}</h1>
<p className="text-sm text-muted-foreground mt-1">
{copy.description}
</p>
</div>
<Badge variant="secondary" className="font-mono">
{copy.badge}
</Badge>
</div>
</div>
)
}

View File

@@ -0,0 +1,81 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { JSONUIPage } from '@/components/JSONUIPage'
import { Eye, Code } from '@phosphor-icons/react'
import { ShowcaseExample, ShowcaseTabsCopy } from './types'
interface ShowcaseTabsProps {
examples: ShowcaseExample[]
copy: ShowcaseTabsCopy
selectedExample: string
onSelectedExampleChange: (value: string) => void
showJSON: boolean
onShowJSONChange: (value: boolean) => void
}
export function ShowcaseTabs({
examples,
copy,
selectedExample,
onSelectedExampleChange,
showJSON,
onShowJSONChange,
}: ShowcaseTabsProps) {
return (
<Tabs
value={selectedExample}
onValueChange={onSelectedExampleChange}
className="h-full flex flex-col"
>
<div className="border-b border-border bg-muted/30 px-6">
<div className="flex items-center justify-between">
<TabsList className="bg-transparent border-0">
{examples.map((example) => {
const Icon = example.icon
return (
<TabsTrigger key={example.key} value={example.key} className="gap-2">
<Icon size={16} />
{example.name}
</TabsTrigger>
)
})}
</TabsList>
<Button
variant="outline"
size="sm"
onClick={() => onShowJSONChange(!showJSON)}
className="gap-2"
>
{showJSON ? <Eye size={16} /> : <Code size={16} />}
{showJSON ? copy.showPreviewLabel : copy.showJsonLabel}
</Button>
</div>
</div>
<div className="flex-1 overflow-auto">
{examples.map((example) => (
<TabsContent key={example.key} value={example.key} className="h-full m-0">
{showJSON ? (
<div className="p-6">
<Card>
<CardHeader>
<CardTitle className="text-lg">{copy.jsonTitle}</CardTitle>
<CardDescription>{example.description}</CardDescription>
</CardHeader>
<CardContent>
<pre className="bg-muted p-4 rounded-lg overflow-auto text-sm max-h-[600px]">
<code>{JSON.stringify(example.config, null, 2)}</code>
</pre>
</CardContent>
</Card>
</div>
) : (
<JSONUIPage jsonConfig={example.config} />
)}
</TabsContent>
))}
</div>
</Tabs>
)
}

View File

@@ -0,0 +1,26 @@
import { ComponentType } from 'react'
export interface ShowcaseExample {
key: string
name: string
description: string
icon: ComponentType<{ size?: number }>
config: Record<string, any>
}
export interface ShowcaseTabsCopy {
showJsonLabel: string
showPreviewLabel: string
jsonTitle: string
}
export interface ShowcaseHeaderCopy {
title: string
description: string
badge: string
}
export interface ShowcaseFooterItem {
label: string
colorClass: string
}