Merge pull request #335 from johndoe6345789/codex/create-fieldtypes,-propertypanels,-and-rendernode-files-ufu1c8

Refactor rendering components into modular panels
This commit is contained in:
2025-12-29 17:10:52 +00:00
committed by GitHub
5 changed files with 403 additions and 331 deletions

View File

@@ -1,16 +1,11 @@
import { useState, useEffect } from 'react'
import { ScrollArea } from '@/components/ui'
import { Input } from '@/components/ui'
import { Label } from '@/components/ui'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui'
import { Button } from '@/components/ui'
import { Separator } from '@/components/ui'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui'
import type { ComponentInstance } from '@/lib/builder-types'
import { Separator, Button } from '@/components/ui'
import type { ComponentInstance } from '@/lib/types/builder-types'
import { componentCatalog } from '@/lib/component-catalog'
import { Code, PaintBrush, Trash, Palette } from '@phosphor-icons/react'
import { Trash } from '@phosphor-icons/react'
import { CssClassBuilder } from '@/components/CssClassBuilder'
import { Database, DropdownConfig } from '@/lib/database'
import { PropertyPanels } from './components/PropertyPanels'
interface PropertyInspectorProps {
component: ComponentInstance | null
@@ -67,131 +62,14 @@ export function PropertyInspector({ component, onUpdate, onDelete, onCodeEdit }:
<p className="text-xs text-muted-foreground">Component Properties</p>
</div>
<Tabs defaultValue="props" className="flex-1 flex flex-col">
<TabsList className="w-full rounded-none border-b">
<TabsTrigger value="props" className="flex-1">
<PaintBrush className="mr-2" size={16} />
Props
</TabsTrigger>
<TabsTrigger value="code" className="flex-1">
<Code className="mr-2" size={16} />
Code
</TabsTrigger>
</TabsList>
<TabsContent value="props" className="flex-1 mt-0">
<ScrollArea className="h-full p-4">
<div className="space-y-4">
{componentDef?.propSchema.map(propDef => {
const dynamicDropdown = propDef.type === 'dynamic-select'
? dynamicDropdowns.find(d => d.name === propDef.dynamicSource)
: null
return (
<div key={propDef.name} className="space-y-2">
<Label className="text-xs uppercase tracking-wider">{propDef.label}</Label>
{propDef.name === 'className' ? (
<div className="flex gap-2">
<Input
value={component.props[propDef.name] || ''}
onChange={(e) => handlePropChange(propDef.name, e.target.value)}
className="flex-1 font-mono text-xs"
/>
<Button
size="sm"
variant="outline"
onClick={() => openCssBuilder(propDef.name)}
>
<Palette size={16} />
</Button>
</div>
) : propDef.type === 'string' ? (
<Input
value={component.props[propDef.name] || ''}
onChange={(e) => handlePropChange(propDef.name, e.target.value)}
/>
) : propDef.type === 'number' ? (
<Input
type="number"
value={component.props[propDef.name] || ''}
onChange={(e) => handlePropChange(propDef.name, Number(e.target.value))}
/>
) : propDef.type === 'boolean' ? (
<Select
value={String(component.props[propDef.name] || false)}
onValueChange={(value) => handlePropChange(propDef.name, value === 'true')}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">True</SelectItem>
<SelectItem value="false">False</SelectItem>
</SelectContent>
</Select>
) : propDef.type === 'select' && propDef.options ? (
<Select
value={component.props[propDef.name] || propDef.defaultValue}
onValueChange={(value) => handlePropChange(propDef.name, value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{propDef.options.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : propDef.type === 'dynamic-select' && dynamicDropdown ? (
<Select
value={component.props[propDef.name] || ''}
onValueChange={(value) => handlePropChange(propDef.name, value)}
>
<SelectTrigger>
<SelectValue placeholder={`Select ${dynamicDropdown.label}`} />
</SelectTrigger>
<SelectContent>
{dynamicDropdown.options.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : null}
{propDef.description && (
<p className="text-xs text-muted-foreground">{propDef.description}</p>
)}
</div>
)
})}
{(!componentDef?.propSchema || componentDef.propSchema.length === 0) && (
<p className="text-sm text-muted-foreground">This component has no configurable properties.</p>
)}
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="code" className="flex-1 mt-0">
<div className="p-4 h-full flex flex-col items-center justify-center text-center space-y-4">
<Code size={48} className="text-muted-foreground" />
<div>
<p className="text-sm text-muted-foreground mb-2">
Add custom JavaScript code for this component
</p>
<Button onClick={onCodeEdit} variant="outline">
Open Code Editor
</Button>
</div>
</div>
</TabsContent>
</Tabs>
<PropertyPanels
component={component}
componentDef={componentDef}
dynamicDropdowns={dynamicDropdowns}
onPropChange={handlePropChange}
onCodeEdit={onCodeEdit}
onOpenCssBuilder={openCssBuilder}
/>
<Separator />

View File

@@ -1,32 +1,16 @@
import type { ComponentInstance } from '@/lib/builder-types'
import { Button } from '@/components/ui'
import { Input } from '@/components/ui'
import { Textarea } from '@/components/ui'
import { Label } from '@/components/ui'
import { Badge } from '@/components/ui'
import { Card } from '@/components/ui'
import { Switch } from '@/components/ui'
import { Checkbox } from '@/components/ui'
import { Separator } from '@/components/ui'
import { Alert } from '@/components/ui'
import { Progress } from '@/components/ui'
import { Slider } from '@/components/ui'
import { Avatar, AvatarFallback } from '@/components/ui'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui'
import { IRCWebchatDeclarative } from '@/components/IRCWebchatDeclarative'
import { NotificationSummaryCard } from '@/components/NotificationSummaryCard'
import type React from 'react'
import type { ComponentInstance } from '@/lib/types/builder-types'
import type { User } from '@/lib/level-types'
import { getDeclarativeRenderer } from '@/lib/declarative-component-renderer'
import { RenderNode } from './components/RenderNode'
interface RenderComponentProps {
component: ComponentInstance
isSelected: boolean
onSelect: (id: string) => void
user?: User
contextData?: Record<string, any>
}
export function RenderComponent({ component, isSelected, onSelect, user, contextData }: RenderComponentProps) {
export function RenderComponent({ component, isSelected, onSelect, user }: RenderComponentProps) {
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation()
onSelect(component.id)
@@ -41,191 +25,19 @@ export function RenderComponent({ component, isSelected, onSelect, user, context
isSelected={isSelected}
onSelect={onSelect}
user={user}
contextData={contextData}
/>
))
}
const wrapperClass = `relative ${isSelected ? 'ring-2 ring-accent ring-offset-2' : 'hover:ring-1 hover:ring-accent/50'} transition-all cursor-pointer`
const renderComponentByType = () => {
const { type, props } = component
const renderer = getDeclarativeRenderer()
if (renderer.hasComponentConfig(type)) {
if (type === 'IRCWebchat' && user) {
return (
<IRCWebchatDeclarative
user={user}
channelName={props.channelName || 'general'}
onClose={props.onClose}
/>
)
}
return (
<div className="p-4 border-2 border-dashed border-accent">
Declarative Component: {type}
<div className="text-xs text-muted-foreground mt-2">
This is a package-defined component
</div>
</div>
)
}
switch (type) {
case 'Container':
return (
<div className={props.className || 'p-4'}>
{renderChildren()}
</div>
)
case 'Flex':
return (
<div className={props.className || 'flex gap-4'}>
{renderChildren()}
</div>
)
case 'Grid':
return (
<div className={props.className || 'grid grid-cols-2 gap-4'}>
{renderChildren()}
</div>
)
case 'Stack':
return (
<div className={props.className || 'flex flex-col gap-2'}>
{renderChildren()}
</div>
)
case 'Card':
return (
<Card className={props.className || 'p-6'}>
{renderChildren()}
</Card>
)
case 'NotificationSummary':
return (
<NotificationSummaryCard
title={props.title}
subtitle={props.subtitle}
totalLabel={props.totalLabel}
items={props.items}
/>
)
case 'Button':
return (
<Button variant={props.variant} size={props.size}>
{props.children || 'Button'}
</Button>
)
case 'Input':
return (
<Input
placeholder={props.placeholder}
type={props.type}
/>
)
case 'Textarea':
return (
<Textarea
placeholder={props.placeholder}
rows={props.rows}
/>
)
case 'Label':
return <Label>{props.children || 'Label'}</Label>
case 'Heading': {
const level = props.level || '1'
const className = props.className
const text = props.children || 'Heading'
if (level === '1') return <h1 className={className}>{text}</h1>
if (level === '2') return <h2 className={className}>{text}</h2>
if (level === '3') return <h3 className={className}>{text}</h3>
if (level === '4') return <h4 className={className}>{text}</h4>
return <h1 className={className}>{text}</h1>
}
case 'Text':
return (
<p className={props.className}>
{props.children || 'Text'}
</p>
)
case 'Badge':
return (
<Badge variant={props.variant}>
{props.children || 'Badge'}
</Badge>
)
case 'Switch':
return <Switch />
case 'Checkbox':
return <Checkbox />
case 'Separator':
return <Separator />
case 'Alert':
return (
<Alert variant={props.variant}>
{renderChildren()}
</Alert>
)
case 'Progress':
return <Progress value={props.value || 50} />
case 'Slider':
return <Slider defaultValue={props.defaultValue || [50]} max={props.max || 100} step={props.step || 1} />
case 'Avatar':
return (
<Avatar>
<AvatarFallback>U</AvatarFallback>
</Avatar>
)
case 'Table':
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Column 1</TableHead>
<TableHead>Column 2</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Data 1</TableCell>
<TableCell>Data 2</TableCell>
</TableRow>
</TableBody>
</Table>
)
default:
return <div className="p-4 border-2 border-dashed">Unknown Component: {type}</div>
}
}
return (
<div className={wrapperClass} onClick={handleClick}>
{renderComponentByType()}
<RenderNode
component={component}
renderChildren={renderChildren}
user={user}
/>
</div>
)
}

View File

@@ -0,0 +1,114 @@
import { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui'
import type { PropDefinition } from '@/lib/types/builder-types'
import type { DropdownConfig } from '@/lib/database'
import { Palette } from '@phosphor-icons/react'
interface FieldTypesProps {
propDef: PropDefinition
value: any
onChange: (value: any) => void
dynamicDropdown?: DropdownConfig | null
onOpenCssBuilder?: () => void
}
export function FieldTypes({ propDef, value, onChange, dynamicDropdown, onOpenCssBuilder }: FieldTypesProps) {
const renderInputByType = () => {
if (propDef.name === 'className') {
return (
<div className="flex gap-2">
<Input
value={value || ''}
onChange={(e) => onChange(e.target.value)}
className="flex-1 font-mono text-xs"
/>
{onOpenCssBuilder && (
<Button size="sm" variant="outline" onClick={onOpenCssBuilder}>
<Palette size={16} />
</Button>
)}
</div>
)
}
switch (propDef.type) {
case 'string':
return (
<Input
value={value || ''}
onChange={(e) => onChange(e.target.value)}
/>
)
case 'number':
return (
<Input
type="number"
value={value || ''}
onChange={(e) => onChange(Number(e.target.value))}
/>
)
case 'boolean':
return (
<Select
value={String(value || false)}
onValueChange={(val) => onChange(val === 'true')}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">True</SelectItem>
<SelectItem value="false">False</SelectItem>
</SelectContent>
</Select>
)
case 'select':
return (
<Select
value={value || propDef.defaultValue}
onValueChange={(val) => onChange(val)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{propDef.options?.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
)
case 'dynamic-select':
return (
<Select
value={value || ''}
onValueChange={(val) => onChange(val)}
>
<SelectTrigger>
<SelectValue placeholder={dynamicDropdown ? `Select ${dynamicDropdown.label}` : 'Select option'} />
</SelectTrigger>
<SelectContent>
{dynamicDropdown?.options.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
)
default:
return null
}
}
return (
<div className="space-y-2">
<Label className="text-xs uppercase tracking-wider">{propDef.label}</Label>
{renderInputByType()}
{propDef.description && (
<p className="text-xs text-muted-foreground">{propDef.description}</p>
)}
</div>
)
}

View File

@@ -0,0 +1,80 @@
import { ScrollArea, Tabs, TabsContent, TabsList, TabsTrigger, Button } from '@/components/ui'
import type { ComponentDefinition, ComponentInstance } from '@/lib/types/builder-types'
import type { DropdownConfig } from '@/lib/database'
import { Code, PaintBrush } from '@phosphor-icons/react'
import { FieldTypes } from './FieldTypes'
interface PropertyPanelsProps {
component: ComponentInstance
componentDef?: ComponentDefinition
dynamicDropdowns: DropdownConfig[]
onPropChange: (propName: string, value: any) => void
onCodeEdit: () => void
onOpenCssBuilder: (propName: string) => void
}
export function PropertyPanels({
component,
componentDef,
dynamicDropdowns,
onPropChange,
onCodeEdit,
onOpenCssBuilder,
}: PropertyPanelsProps) {
return (
<Tabs defaultValue="props" className="flex-1 flex flex-col">
<TabsList className="w-full rounded-none border-b">
<TabsTrigger value="props" className="flex-1">
<PaintBrush className="mr-2" size={16} />
Props
</TabsTrigger>
<TabsTrigger value="code" className="flex-1">
<Code className="mr-2" size={16} />
Code
</TabsTrigger>
</TabsList>
<TabsContent value="props" className="flex-1 mt-0">
<ScrollArea className="h-full p-4">
<div className="space-y-4">
{componentDef?.propSchema?.length ? (
componentDef.propSchema.map(propDef => {
const dynamicDropdown =
propDef.type === 'dynamic-select'
? dynamicDropdowns.find(d => d.name === propDef.dynamicSource)
: null
return (
<FieldTypes
key={propDef.name}
propDef={propDef}
value={component.props[propDef.name] || ''}
onChange={(value) => onPropChange(propDef.name, value)}
dynamicDropdown={dynamicDropdown}
onOpenCssBuilder={() => onOpenCssBuilder(propDef.name)}
/>
)
})
) : (
<p className="text-sm text-muted-foreground">This component has no configurable properties.</p>
)}
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="code" className="flex-1 mt-0">
<div className="p-4 h-full flex flex-col items-center justify-center text-center space-y-4">
<Code size={48} className="text-muted-foreground" />
<div>
<p className="text-sm text-muted-foreground mb-2">
Add custom JavaScript code for this component
</p>
<Button onClick={onCodeEdit} variant="outline">
Open Code Editor
</Button>
</div>
</div>
</TabsContent>
</Tabs>
)
}

View File

@@ -0,0 +1,188 @@
import type React from 'react'
import type { ComponentInstance } from '@/lib/types/builder-types'
import { Button, Input, Textarea, Label, Badge, Card, Switch, Checkbox, Separator, Alert, Progress, Slider, Avatar, AvatarFallback, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui'
import { IRCWebchatDeclarative } from '@/components/IRCWebchatDeclarative'
import { NotificationSummaryCard } from '@/components/NotificationSummaryCard'
import type { User } from '@/lib/level-types'
import { getDeclarativeRenderer } from '@/lib/declarative-component-renderer'
interface RenderNodeProps {
component: ComponentInstance
renderChildren: () => React.ReactNode
user?: User
}
export function RenderNode({ component, renderChildren, user }: RenderNodeProps) {
const { type, props } = component
const renderer = getDeclarativeRenderer()
if (renderer.hasComponentConfig(type)) {
if (type === 'IRCWebchat' && user) {
return (
<IRCWebchatDeclarative
user={user}
channelName={props.channelName || 'general'}
onClose={props.onClose}
/>
)
}
return (
<div className="p-4 border-2 border-dashed border-accent">
Declarative Component: {type}
<div className="text-xs text-muted-foreground mt-2">
This is a package-defined component
</div>
</div>
)
}
switch (type) {
case 'Container':
return (
<div className={props.className || 'p-4'}>
{renderChildren()}
</div>
)
case 'Flex':
return (
<div className={props.className || 'flex gap-4'}>
{renderChildren()}
</div>
)
case 'Grid':
return (
<div className={props.className || 'grid grid-cols-2 gap-4'}>
{renderChildren()}
</div>
)
case 'Stack':
return (
<div className={props.className || 'flex flex-col gap-2'}>
{renderChildren()}
</div>
)
case 'Card':
return (
<Card className={props.className || 'p-6'}>
{renderChildren()}
</Card>
)
case 'NotificationSummary':
return (
<NotificationSummaryCard
title={props.title}
subtitle={props.subtitle}
totalLabel={props.totalLabel}
items={props.items}
/>
)
case 'Button':
return (
<Button variant={props.variant} size={props.size}>
{props.children || 'Button'}
</Button>
)
case 'Input':
return (
<Input
placeholder={props.placeholder}
type={props.type}
/>
)
case 'Textarea':
return (
<Textarea
placeholder={props.placeholder}
rows={props.rows}
/>
)
case 'Label':
return <Label>{props.children || 'Label'}</Label>
case 'Heading': {
const level = props.level || '1'
const className = props.className
const text = props.children || 'Heading'
if (level === '1') return <h1 className={className}>{text}</h1>
if (level === '2') return <h2 className={className}>{text}</h2>
if (level === '3') return <h3 className={className}>{text}</h3>
if (level === '4') return <h4 className={className}>{text}</h4>
return <h1 className={className}>{text}</h1>
}
case 'Text':
return (
<p className={props.className}>
{props.children || 'Text'}
</p>
)
case 'Badge':
return (
<Badge variant={props.variant}>
{props.children || 'Badge'}
</Badge>
)
case 'Switch':
return <Switch />
case 'Checkbox':
return <Checkbox />
case 'Separator':
return <Separator />
case 'Alert':
return (
<Alert variant={props.variant}>
{renderChildren()}
</Alert>
)
case 'Progress':
return <Progress value={props.value || 50} />
case 'Slider':
return <Slider defaultValue={props.defaultValue || [50]} max={props.max || 100} step={props.step || 1} />
case 'Avatar':
return (
<Avatar>
<AvatarFallback>U</AvatarFallback>
</Avatar>
)
case 'Table':
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Column 1</TableHead>
<TableHead>Column 2</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Data 1</TableCell>
<TableCell>Data 2</TableCell>
</TableRow>
</TableBody>
</Table>
)
default:
return <div className="p-4 border-2 border-dashed">Unknown Component: {type}</div>
}
}