Merge pull request #60 from johndoe6345789/codex/move-seed-data-and-ui-copy-to-json

Extract DataBindingDesigner/UI copy into JSON and split DataSource components
This commit is contained in:
2026-01-18 00:45:35 +00:00
committed by GitHub
15 changed files with 786 additions and 442 deletions

View File

@@ -1,61 +1,39 @@
import { useState } from 'react'
import { DataSourceManager } from '@/components/organisms/DataSourceManager'
import { ComponentBindingDialog } from '@/components/molecules/ComponentBindingDialog'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { DataSource, UIComponent } from '@/types/json-ui'
import { Link, Code } from '@phosphor-icons/react'
import { ScrollArea } from '@/components/ui/scroll-area'
import { DataBindingHeader } from '@/components/data-binding-designer/DataBindingHeader'
import { ComponentBindingsCard } from '@/components/data-binding-designer/ComponentBindingsCard'
import { HowItWorksCard } from '@/components/data-binding-designer/HowItWorksCard'
import dataBindingCopy from '@/data/data-binding-designer.json'
interface SeedDataSource extends Omit<DataSource, 'compute'> {
computeId?: string
}
const computeRegistry: Record<string, (data: Record<string, any>) => any> = {
displayName: (data) => `Welcome, ${data.userProfile?.name || 'Guest'}!`,
}
const buildSeedDataSources = (sources: SeedDataSource[]): DataSource[] => {
return sources.map((source) => {
if (source.type === 'computed' && source.computeId) {
return {
...source,
compute: computeRegistry[source.computeId],
}
}
return source
})
}
export function DataBindingDesigner() {
const [dataSources, setDataSources] = useState<DataSource[]>([
{
id: 'userProfile',
type: 'kv',
key: 'user-profile',
defaultValue: { name: 'John Doe', email: 'john@example.com' },
},
{
id: 'counter',
type: 'kv',
key: 'app-counter',
defaultValue: 0,
},
{
id: 'displayName',
type: 'computed',
compute: (data) => `Welcome, ${data.userProfile?.name || 'Guest'}!`,
dependencies: ['userProfile'],
},
])
const [dataSources, setDataSources] = useState<DataSource[]>(
buildSeedDataSources(dataBindingCopy.seed.dataSources as SeedDataSource[]),
)
const [mockComponents] = useState<UIComponent[]>([
{
id: 'title',
type: 'Heading',
props: { className: 'text-2xl font-bold' },
bindings: {
children: { source: 'displayName' },
},
},
{
id: 'counter-display',
type: 'Text',
props: { className: 'text-lg' },
bindings: {
children: { source: 'counter' },
},
},
{
id: 'email-input',
type: 'Input',
props: { placeholder: 'Enter email' },
bindings: {
value: { source: 'userProfile', path: 'email' },
},
},
])
const [mockComponents] = useState<UIComponent[]>(dataBindingCopy.seed.components)
const [selectedComponent, setSelectedComponent] = useState<UIComponent | null>(null)
const [bindingDialogOpen, setBindingDialogOpen] = useState(false)
@@ -69,21 +47,13 @@ export function DataBindingDesigner() {
console.log('Updated component bindings:', updatedComponent)
}
const getSourceById = (sourceId: string) => {
return dataSources.find(ds => ds.id === sourceId)
}
return (
<div className="h-full overflow-auto">
<div className="p-6 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2 bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
Data Binding Designer
</h1>
<p className="text-muted-foreground">
Connect UI components to KV storage and computed values
</p>
</div>
<DataBindingHeader
title={dataBindingCopy.header.title}
description={dataBindingCopy.header.description}
/>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-6">
@@ -94,121 +64,17 @@ export function DataBindingDesigner() {
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Code className="w-5 h-5" />
Component Bindings
</CardTitle>
<CardDescription>
Example components with data bindings
</CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-[600px] pr-4">
<div className="space-y-3">
{mockComponents.map(component => {
const bindingCount = Object.keys(component.bindings || {}).length
return (
<Card key={component.id} className="bg-card/50 backdrop-blur">
<CardContent className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<Badge variant="outline" className="font-mono text-xs">
{component.type}
</Badge>
<span className="text-sm text-muted-foreground">
#{component.id}
</span>
</div>
<ComponentBindingsCard
components={mockComponents}
dataSources={dataSources}
copy={dataBindingCopy.bindingsCard}
onEditBinding={handleEditBinding}
/>
{bindingCount > 0 ? (
<div className="space-y-1">
{Object.entries(component.bindings || {}).map(([prop, binding]) => {
const source = getSourceById(binding.source)
return (
<div key={prop} className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground font-mono">
{prop}:
</span>
<Badge
variant="secondary"
className="font-mono h-5 text-xs"
>
{binding.source}
{binding.path && `.${binding.path}`}
</Badge>
{source && (
<Badge
variant="outline"
className="h-5 text-xs"
>
{source.type}
</Badge>
)}
</div>
)
})}
</div>
) : (
<p className="text-xs text-muted-foreground">
No bindings configured
</p>
)}
</div>
<Button
size="sm"
variant="ghost"
onClick={() => handleEditBinding(component)}
className="h-8 px-3"
>
<Link className="w-4 h-4 mr-1" />
Bind
</Button>
</div>
</CardContent>
</Card>
)
})}
</div>
</ScrollArea>
</CardContent>
</Card>
<Card className="bg-accent/5 border-accent/20">
<CardHeader>
<CardTitle className="text-base">How It Works</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<div className="flex gap-2">
<div className="w-6 h-6 rounded-full bg-primary/20 text-primary flex items-center justify-center text-xs font-bold flex-shrink-0">
1
</div>
<p className="text-muted-foreground">
Create data sources (KV store for persistence, static for constants)
</p>
</div>
<div className="flex gap-2">
<div className="w-6 h-6 rounded-full bg-primary/20 text-primary flex items-center justify-center text-xs font-bold flex-shrink-0">
2
</div>
<p className="text-muted-foreground">
Add computed sources to derive values from other sources
</p>
</div>
<div className="flex gap-2">
<div className="w-6 h-6 rounded-full bg-primary/20 text-primary flex items-center justify-center text-xs font-bold flex-shrink-0">
3
</div>
<p className="text-muted-foreground">
Bind component properties to data sources for reactive updates
</p>
</div>
</CardContent>
</Card>
<HowItWorksCard
title={dataBindingCopy.howItWorks.title}
steps={dataBindingCopy.howItWorks.steps}
/>
</div>
</div>
</div>

View File

@@ -0,0 +1,115 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { ScrollArea } from '@/components/ui/scroll-area'
import { DataSource, UIComponent } from '@/types/json-ui'
import { Link, Code } from '@phosphor-icons/react'
interface ComponentBindingsCardCopy {
title: string
description: string
emptyState: string
actionLabel: string
}
interface ComponentBindingsCardProps {
components: UIComponent[]
dataSources: DataSource[]
copy: ComponentBindingsCardCopy
onEditBinding: (component: UIComponent) => void
}
export function ComponentBindingsCard({
components,
dataSources,
copy,
onEditBinding,
}: ComponentBindingsCardProps) {
const getSourceById = (sourceId: string) => dataSources.find(ds => ds.id === sourceId)
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Code className="w-5 h-5" />
{copy.title}
</CardTitle>
<CardDescription>
{copy.description}
</CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-[600px] pr-4">
<div className="space-y-3">
{components.map(component => {
const bindingCount = Object.keys(component.bindings || {}).length
return (
<Card key={component.id} className="bg-card/50 backdrop-blur">
<CardContent className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<Badge variant="outline" className="font-mono text-xs">
{component.type}
</Badge>
<span className="text-sm text-muted-foreground">
#{component.id}
</span>
</div>
{bindingCount > 0 ? (
<div className="space-y-1">
{Object.entries(component.bindings || {}).map(([prop, binding]) => {
const source = getSourceById(binding.source)
return (
<div key={prop} className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground font-mono">
{prop}:
</span>
<Badge
variant="secondary"
className="font-mono h-5 text-xs"
>
{binding.source}
{binding.path && `.${binding.path}`}
</Badge>
{source && (
<Badge
variant="outline"
className="h-5 text-xs"
>
{source.type}
</Badge>
)}
</div>
)
})}
</div>
) : (
<p className="text-xs text-muted-foreground">
{copy.emptyState}
</p>
)}
</div>
<Button
size="sm"
variant="ghost"
onClick={() => onEditBinding(component)}
className="h-8 px-3"
>
<Link className="w-4 h-4 mr-1" />
{copy.actionLabel}
</Button>
</div>
</CardContent>
</Card>
)
})}
</div>
</ScrollArea>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,17 @@
interface DataBindingHeaderProps {
title: string
description: string
}
export function DataBindingHeader({ title, description }: DataBindingHeaderProps) {
return (
<div>
<h1 className="text-3xl font-bold mb-2 bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
{title}
</h1>
<p className="text-muted-foreground">
{description}
</p>
</div>
)
}

View File

@@ -0,0 +1,28 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
interface HowItWorksCardProps {
title: string
steps: string[]
}
export function HowItWorksCard({ title, steps }: HowItWorksCardProps) {
return (
<Card className="bg-accent/5 border-accent/20">
<CardHeader>
<CardTitle className="text-base">{title}</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
{steps.map((step, index) => (
<div key={step} className="flex gap-2">
<div className="w-6 h-6 rounded-full bg-primary/20 text-primary flex items-center justify-center text-xs font-bold flex-shrink-0">
{index + 1}
</div>
<p className="text-muted-foreground">
{step}
</p>
</div>
))}
</CardContent>
</Card>
)
}

View File

@@ -1,14 +1,13 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { DataSource, DataSourceType } from '@/types/json-ui'
import { DataSource } from '@/types/json-ui'
import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
import { Badge } from '@/components/ui/badge'
import { X } from '@phosphor-icons/react'
import { DataSourceIdField } from '@/components/molecules/data-source-editor/DataSourceIdField'
import { KvSourceFields } from '@/components/molecules/data-source-editor/KvSourceFields'
import { StaticSourceFields } from '@/components/molecules/data-source-editor/StaticSourceFields'
import { ComputedSourceFields } from '@/components/molecules/data-source-editor/ComputedSourceFields'
import dataSourceEditorCopy from '@/data/data-source-editor-dialog.json'
interface DataSourceEditorDialogProps {
open: boolean
@@ -18,15 +17,19 @@ interface DataSourceEditorDialogProps {
onSave: (dataSource: DataSource) => void
}
export function DataSourceEditorDialog({
open,
dataSource,
export function DataSourceEditorDialog({
open,
dataSource,
allDataSources,
onOpenChange,
onSave
onOpenChange,
onSave,
}: DataSourceEditorDialogProps) {
const [editingSource, setEditingSource] = useState<DataSource | null>(dataSource)
useEffect(() => {
setEditingSource(dataSource)
}, [dataSource])
const handleSave = () => {
if (!editingSource) return
onSave(editingSource)
@@ -55,7 +58,7 @@ export function DataSourceEditorDialog({
if (!editingSource) return null
const availableDeps = allDataSources.filter(
ds => ds.id !== editingSource.id && ds.type !== 'computed'
ds => ds.id !== editingSource.id && ds.type !== 'computed',
)
const selectedDeps = editingSource.dependencies || []
@@ -66,158 +69,59 @@ export function DataSourceEditorDialog({
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Edit Data Source
{dataSourceEditorCopy.title}
<DataSourceBadge type={editingSource.type} />
</DialogTitle>
<DialogDescription>
Configure the data source settings and dependencies
{dataSourceEditorCopy.description}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>ID</Label>
<Input
value={editingSource.id}
onChange={(e) => updateField('id', e.target.value)}
placeholder="unique-id"
className="font-mono"
/>
</div>
<DataSourceIdField
editingSource={editingSource}
label={dataSourceEditorCopy.fields.id.label}
placeholder={dataSourceEditorCopy.fields.id.placeholder}
onChange={(value) => updateField('id', value)}
/>
{editingSource.type === 'kv' && (
<>
<div className="space-y-2">
<Label>KV Store Key</Label>
<Input
value={editingSource.key || ''}
onChange={(e) => updateField('key', e.target.value)}
placeholder="storage-key"
className="font-mono"
/>
<p className="text-xs text-muted-foreground">
Key used for persistent storage in the KV store
</p>
</div>
<div className="space-y-2">
<Label>Default Value (JSON)</Label>
<Textarea
value={JSON.stringify(editingSource.defaultValue, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value)
updateField('defaultValue', parsed)
} catch (err) {
// Invalid JSON, don't update
}
}}
placeholder='{"key": "value"}'
className="font-mono text-sm h-24"
/>
</div>
</>
<KvSourceFields
editingSource={editingSource}
copy={dataSourceEditorCopy.kv}
onUpdateField={updateField}
/>
)}
{editingSource.type === 'static' && (
<div className="space-y-2">
<Label>Value (JSON)</Label>
<Textarea
value={JSON.stringify(editingSource.defaultValue, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value)
updateField('defaultValue', parsed)
} catch (err) {
// Invalid JSON, don't update
}
}}
placeholder='{"key": "value"}'
className="font-mono text-sm h-24"
/>
</div>
<StaticSourceFields
editingSource={editingSource}
label={dataSourceEditorCopy.static.valueLabel}
placeholder={dataSourceEditorCopy.static.valuePlaceholder}
onUpdateField={updateField}
/>
)}
{editingSource.type === 'computed' && (
<>
<div className="space-y-2">
<Label>Compute Function</Label>
<Textarea
value={editingSource.compute?.toString() || ''}
onChange={(e) => {
try {
const fn = new Function('data', `return (${e.target.value})`)()
updateField('compute', fn)
} catch (err) {
// Invalid function
}
}}
placeholder="(data) => data.source1 + data.source2"
className="font-mono text-sm h-24"
/>
<p className="text-xs text-muted-foreground">
Function that computes the value from other data sources
</p>
</div>
<div className="space-y-2">
<Label>Dependencies</Label>
{selectedDeps.length > 0 && (
<div className="flex flex-wrap gap-2 p-3 bg-muted/30 rounded border border-border">
{selectedDeps.map(depId => (
<Badge
key={depId}
variant="secondary"
className="flex items-center gap-1"
>
{depId}
<button
onClick={() => removeDependency(depId)}
className="ml-1 hover:text-destructive"
>
<X className="w-3 h-3" />
</button>
</Badge>
))}
</div>
)}
{unselectedDeps.length > 0 && (
<div className="space-y-1">
<Label className="text-xs text-muted-foreground">Available Sources</Label>
<div className="flex flex-wrap gap-2">
{unselectedDeps.map(ds => (
<Button
key={ds.id}
variant="outline"
size="sm"
onClick={() => addDependency(ds.id)}
className="h-7 text-xs"
>
+ {ds.id}
</Button>
))}
</div>
</div>
)}
{unselectedDeps.length === 0 && selectedDeps.length === 0 && (
<p className="text-sm text-muted-foreground">
No data sources available. Create KV or static sources first.
</p>
)}
</div>
</>
<ComputedSourceFields
editingSource={editingSource}
availableDeps={availableDeps}
selectedDeps={selectedDeps}
unselectedDeps={unselectedDeps}
copy={dataSourceEditorCopy.computed}
onUpdateField={updateField}
onAddDependency={addDependency}
onRemoveDependency={removeDependency}
/>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
{dataSourceEditorCopy.actions.cancel}
</Button>
<Button onClick={handleSave}>
Save Changes
{dataSourceEditorCopy.actions.save}
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -0,0 +1,110 @@
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { DataSource } from '@/types/json-ui'
import { X } from '@phosphor-icons/react'
interface ComputedSourceFieldsCopy {
computeLabel: string
computePlaceholder: string
computeHelp: string
dependenciesLabel: string
availableSourcesLabel: string
emptyDependencies: string
}
interface ComputedSourceFieldsProps {
editingSource: DataSource
availableDeps: DataSource[]
selectedDeps: string[]
unselectedDeps: DataSource[]
copy: ComputedSourceFieldsCopy
onUpdateField: <K extends keyof DataSource>(field: K, value: DataSource[K]) => void
onAddDependency: (depId: string) => void
onRemoveDependency: (depId: string) => void
}
export function ComputedSourceFields({
editingSource,
availableDeps,
selectedDeps,
unselectedDeps,
copy,
onUpdateField,
onAddDependency,
onRemoveDependency,
}: ComputedSourceFieldsProps) {
return (
<>
<div className="space-y-2">
<Label>{copy.computeLabel}</Label>
<Textarea
value={editingSource.compute?.toString() || ''}
onChange={(e) => {
try {
const fn = new Function('data', `return (${e.target.value})`)()
onUpdateField('compute', fn)
} catch (err) {
// Invalid function
}
}}
placeholder={copy.computePlaceholder}
className="font-mono text-sm h-24"
/>
<p className="text-xs text-muted-foreground">
{copy.computeHelp}
</p>
</div>
<div className="space-y-2">
<Label>{copy.dependenciesLabel}</Label>
{selectedDeps.length > 0 && (
<div className="flex flex-wrap gap-2 p-3 bg-muted/30 rounded border border-border">
{selectedDeps.map(depId => (
<Badge
key={depId}
variant="secondary"
className="flex items-center gap-1"
>
{depId}
<button
onClick={() => onRemoveDependency(depId)}
className="ml-1 hover:text-destructive"
>
<X className="w-3 h-3" />
</button>
</Badge>
))}
</div>
)}
{unselectedDeps.length > 0 && (
<div className="space-y-1">
<Label className="text-xs text-muted-foreground">{copy.availableSourcesLabel}</Label>
<div className="flex flex-wrap gap-2">
{unselectedDeps.map(ds => (
<Button
key={ds.id}
variant="outline"
size="sm"
onClick={() => onAddDependency(ds.id)}
className="h-7 text-xs"
>
+ {ds.id}
</Button>
))}
</div>
</div>
)}
{availableDeps.length === 0 && selectedDeps.length === 0 && (
<p className="text-sm text-muted-foreground">
{copy.emptyDependencies}
</p>
)}
</div>
</>
)
}

View File

@@ -0,0 +1,29 @@
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { DataSource } from '@/types/json-ui'
interface DataSourceIdFieldProps {
editingSource: DataSource
label: string
placeholder: string
onChange: (value: string) => void
}
export function DataSourceIdField({
editingSource,
label,
placeholder,
onChange,
}: DataSourceIdFieldProps) {
return (
<div className="space-y-2">
<Label>{label}</Label>
<Input
value={editingSource.id}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="font-mono"
/>
</div>
)
}

View File

@@ -0,0 +1,54 @@
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { DataSource } from '@/types/json-ui'
interface KvSourceFieldsCopy {
keyLabel: string
keyPlaceholder: string
keyHelp: string
defaultLabel: string
defaultPlaceholder: string
}
interface KvSourceFieldsProps {
editingSource: DataSource
copy: KvSourceFieldsCopy
onUpdateField: <K extends keyof DataSource>(field: K, value: DataSource[K]) => void
}
export function KvSourceFields({ editingSource, copy, onUpdateField }: KvSourceFieldsProps) {
return (
<>
<div className="space-y-2">
<Label>{copy.keyLabel}</Label>
<Input
value={editingSource.key || ''}
onChange={(e) => onUpdateField('key', e.target.value)}
placeholder={copy.keyPlaceholder}
className="font-mono"
/>
<p className="text-xs text-muted-foreground">
{copy.keyHelp}
</p>
</div>
<div className="space-y-2">
<Label>{copy.defaultLabel}</Label>
<Textarea
value={JSON.stringify(editingSource.defaultValue, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value)
onUpdateField('defaultValue', parsed)
} catch (err) {
// Invalid JSON, don't update
}
}}
placeholder={copy.defaultPlaceholder}
className="font-mono text-sm h-24"
/>
</div>
</>
)
}

View File

@@ -0,0 +1,36 @@
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { DataSource } from '@/types/json-ui'
interface StaticSourceFieldsProps {
editingSource: DataSource
label: string
placeholder: string
onUpdateField: <K extends keyof DataSource>(field: K, value: DataSource[K]) => void
}
export function StaticSourceFields({
editingSource,
label,
placeholder,
onUpdateField,
}: StaticSourceFieldsProps) {
return (
<div className="space-y-2">
<Label>{label}</Label>
<Textarea
value={JSON.stringify(editingSource.defaultValue, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value)
onUpdateField('defaultValue', parsed)
} catch (err) {
// Invalid JSON, don't update
}
}}
placeholder={placeholder}
className="font-mono text-sm h-24"
/>
</div>
)
}

View File

@@ -1,26 +1,14 @@
import { useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { DataSourceCard } from '@/components/molecules/DataSourceCard'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { DataSourceEditorDialog } from '@/components/molecules/DataSourceEditorDialog'
import { useDataSourceManager } from '@/hooks/data/use-data-source-manager'
import { DataSource, DataSourceType } from '@/types/json-ui'
import { Plus, Database, Function, FileText } from '@phosphor-icons/react'
import { Database, Function, FileText } from '@phosphor-icons/react'
import { toast } from 'sonner'
import {
EmptyState,
ActionButton,
Heading,
Text,
IconText,
Stack,
Section
} from '@/components/atoms'
import { EmptyState, Stack } from '@/components/atoms'
import { DataSourceManagerHeader } from '@/components/organisms/data-source-manager/DataSourceManagerHeader'
import { DataSourceGroupSection } from '@/components/organisms/data-source-manager/DataSourceGroupSection'
import dataSourceManagerCopy from '@/data/data-source-manager.json'
interface DataSourceManagerProps {
dataSources: DataSource[]
@@ -56,21 +44,24 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
const handleDeleteSource = (id: string) => {
const dependents = getDependents(id)
if (dependents.length > 0) {
toast.error('Cannot delete', {
description: `This source is used by ${dependents.length} computed ${dependents.length === 1 ? 'source' : 'sources'}`,
const noun = dependents.length === 1 ? 'source' : 'sources'
toast.error(dataSourceManagerCopy.toasts.deleteBlockedTitle, {
description: dataSourceManagerCopy.toasts.deleteBlockedDescription
.replace('{count}', String(dependents.length))
.replace('{noun}', noun),
})
return
}
deleteDataSource(id)
onChange(localSources.filter(ds => ds.id !== id))
toast.success('Data source deleted')
toast.success(dataSourceManagerCopy.toasts.deleted)
}
const handleSaveSource = (updatedSource: DataSource) => {
updateDataSource(updatedSource.id, updatedSource)
onChange(localSources.map(ds => ds.id === updatedSource.id ? updatedSource : ds))
toast.success('Data source updated')
toast.success(dataSourceManagerCopy.toasts.updated)
}
const groupedSources = {
@@ -83,115 +74,51 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
<div className="space-y-6">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<Stack direction="vertical" spacing="xs">
<Heading level={2}>Data Sources</Heading>
<Text variant="muted">
Manage KV storage, computed values, and static data
</Text>
</Stack>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div>
<ActionButton
icon={<Plus size={16} />}
label="Add Data Source"
variant="default"
onClick={() => {}}
/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleAddDataSource('kv')}>
<Database className="w-4 h-4 mr-2" />
KV Store
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleAddDataSource('computed')}>
<Function className="w-4 h-4 mr-2" />
Computed Value
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleAddDataSource('static')}>
<FileText className="w-4 h-4 mr-2" />
Static Data
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<DataSourceManagerHeader
copy={{
title: dataSourceManagerCopy.header.title,
description: dataSourceManagerCopy.header.description,
addLabel: dataSourceManagerCopy.actions.add,
menu: dataSourceManagerCopy.menu,
}}
onAdd={handleAddDataSource}
/>
</CardHeader>
<CardContent>
{localSources.length === 0 ? (
<EmptyState
icon={<Database size={48} weight="duotone" />}
title="No data sources yet"
description="Create your first data source to start binding data to components"
title={dataSourceManagerCopy.emptyState.title}
description={dataSourceManagerCopy.emptyState.description}
/>
) : (
<Stack direction="vertical" spacing="xl">
{groupedSources.kv.length > 0 && (
<Section>
<IconText
icon={<Database size={16} />}
className="text-sm font-semibold mb-3"
>
KV Store ({groupedSources.kv.length})
</IconText>
<Stack direction="vertical" spacing="sm">
{groupedSources.kv.map(ds => (
<DataSourceCard
key={ds.id}
dataSource={ds}
dependents={getDependents(ds.id)}
onEdit={handleEditSource}
onDelete={handleDeleteSource}
/>
))}
</Stack>
</Section>
)}
<DataSourceGroupSection
icon={<Database size={16} />}
label={dataSourceManagerCopy.groups.kv}
dataSources={groupedSources.kv}
getDependents={getDependents}
onEdit={handleEditSource}
onDelete={handleDeleteSource}
/>
{groupedSources.static.length > 0 && (
<Section>
<IconText
icon={<FileText size={16} />}
className="text-sm font-semibold mb-3"
>
Static Data ({groupedSources.static.length})
</IconText>
<Stack direction="vertical" spacing="sm">
{groupedSources.static.map(ds => (
<DataSourceCard
key={ds.id}
dataSource={ds}
dependents={getDependents(ds.id)}
onEdit={handleEditSource}
onDelete={handleDeleteSource}
/>
))}
</Stack>
</Section>
)}
<DataSourceGroupSection
icon={<FileText size={16} />}
label={dataSourceManagerCopy.groups.static}
dataSources={groupedSources.static}
getDependents={getDependents}
onEdit={handleEditSource}
onDelete={handleDeleteSource}
/>
{groupedSources.computed.length > 0 && (
<Section>
<IconText
icon={<Function size={16} />}
className="text-sm font-semibold mb-3"
>
Computed Values ({groupedSources.computed.length})
</IconText>
<Stack direction="vertical" spacing="sm">
{groupedSources.computed.map(ds => (
<DataSourceCard
key={ds.id}
dataSource={ds}
dependents={getDependents(ds.id)}
onEdit={handleEditSource}
onDelete={handleDeleteSource}
/>
))}
</Stack>
</Section>
)}
<DataSourceGroupSection
icon={<Function size={16} />}
label={dataSourceManagerCopy.groups.computed}
dataSources={groupedSources.computed}
getDependents={getDependents}
onEdit={handleEditSource}
onDelete={handleDeleteSource}
/>
</Stack>
)}
</CardContent>

View File

@@ -0,0 +1,48 @@
import { DataSource } from '@/types/json-ui'
import { DataSourceCard } from '@/components/molecules/DataSourceCard'
import { IconText, Section, Stack } from '@/components/atoms'
import { ReactNode } from 'react'
interface DataSourceGroupSectionProps {
icon: ReactNode
label: string
dataSources: DataSource[]
getDependents: (id: string) => string[]
onEdit: (id: string) => void
onDelete: (id: string) => void
}
export function DataSourceGroupSection({
icon,
label,
dataSources,
getDependents,
onEdit,
onDelete,
}: DataSourceGroupSectionProps) {
if (dataSources.length === 0) {
return null
}
return (
<Section>
<IconText
icon={icon}
className="text-sm font-semibold mb-3"
>
{label} ({dataSources.length})
</IconText>
<Stack direction="vertical" spacing="sm">
{dataSources.map(ds => (
<DataSourceCard
key={ds.id}
dataSource={ds}
dependents={getDependents(ds.id)}
onEdit={onEdit}
onDelete={onDelete}
/>
))}
</Stack>
</Section>
)
}

View File

@@ -0,0 +1,64 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { ActionButton, Heading, Stack, Text } from '@/components/atoms'
import { Plus, Database, Function, FileText } from '@phosphor-icons/react'
import { DataSourceType } from '@/types/json-ui'
interface DataSourceManagerHeaderCopy {
title: string
description: string
addLabel: string
menu: {
kv: string
computed: string
static: string
}
}
interface DataSourceManagerHeaderProps {
copy: DataSourceManagerHeaderCopy
onAdd: (type: DataSourceType) => void
}
export function DataSourceManagerHeader({ copy, onAdd }: DataSourceManagerHeaderProps) {
return (
<div className="flex items-center justify-between">
<Stack direction="vertical" spacing="xs">
<Heading level={2}>{copy.title}</Heading>
<Text variant="muted">
{copy.description}
</Text>
</Stack>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div>
<ActionButton
icon={<Plus size={16} />}
label={copy.addLabel}
variant="default"
onClick={() => {}}
/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onAdd('kv')}>
<Database className="w-4 h-4 mr-2" />
{copy.menu.kv}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onAdd('computed')}>
<Function className="w-4 h-4 mr-2" />
{copy.menu.computed}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onAdd('static')}>
<FileText className="w-4 h-4 mr-2" />
{copy.menu.static}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}

View File

@@ -0,0 +1,84 @@
{
"header": {
"title": "Data Binding Designer",
"description": "Connect UI components to KV storage and computed values"
},
"bindingsCard": {
"title": "Component Bindings",
"description": "Example components with data bindings",
"emptyState": "No bindings configured",
"actionLabel": "Bind"
},
"howItWorks": {
"title": "How It Works",
"steps": [
"Create data sources (KV store for persistence, static for constants)",
"Add computed sources to derive values from other sources",
"Bind component properties to data sources for reactive updates"
]
},
"seed": {
"dataSources": [
{
"id": "userProfile",
"type": "kv",
"key": "user-profile",
"defaultValue": {
"name": "John Doe",
"email": "john@example.com"
}
},
{
"id": "counter",
"type": "kv",
"key": "app-counter",
"defaultValue": 0
},
{
"id": "displayName",
"type": "computed",
"dependencies": ["userProfile"],
"computeId": "displayName"
}
],
"components": [
{
"id": "title",
"type": "Heading",
"props": {
"className": "text-2xl font-bold"
},
"bindings": {
"children": {
"source": "displayName"
}
}
},
{
"id": "counter-display",
"type": "Text",
"props": {
"className": "text-lg"
},
"bindings": {
"children": {
"source": "counter"
}
}
},
{
"id": "email-input",
"type": "Input",
"props": {
"placeholder": "Enter email"
},
"bindings": {
"value": {
"source": "userProfile",
"path": "email"
}
}
}
]
}
}

View File

@@ -0,0 +1,33 @@
{
"title": "Edit Data Source",
"description": "Configure the data source settings and dependencies",
"fields": {
"id": {
"label": "ID",
"placeholder": "unique-id"
}
},
"kv": {
"keyLabel": "KV Store Key",
"keyPlaceholder": "storage-key",
"keyHelp": "Key used for persistent storage in the KV store",
"defaultLabel": "Default Value (JSON)",
"defaultPlaceholder": "{\"key\": \"value\"}"
},
"static": {
"valueLabel": "Value (JSON)",
"valuePlaceholder": "{\"key\": \"value\"}"
},
"computed": {
"computeLabel": "Compute Function",
"computePlaceholder": "(data) => data.source1 + data.source2",
"computeHelp": "Function that computes the value from other data sources",
"dependenciesLabel": "Dependencies",
"availableSourcesLabel": "Available Sources",
"emptyDependencies": "No data sources available. Create KV or static sources first."
},
"actions": {
"cancel": "Cancel",
"save": "Save Changes"
}
}

View File

@@ -0,0 +1,29 @@
{
"header": {
"title": "Data Sources",
"description": "Manage KV storage, computed values, and static data"
},
"actions": {
"add": "Add Data Source"
},
"menu": {
"kv": "KV Store",
"computed": "Computed Value",
"static": "Static Data"
},
"emptyState": {
"title": "No data sources yet",
"description": "Create your first data source to start binding data to components"
},
"groups": {
"kv": "KV Store",
"static": "Static Data",
"computed": "Computed Values"
},
"toasts": {
"deleteBlockedTitle": "Cannot delete",
"deleteBlockedDescription": "This source is used by {count} computed {noun}",
"deleted": "Data source deleted",
"updated": "Data source updated"
}
}