mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 14:25:02 +00:00
11 KiB
11 KiB
JSON-Driven UI & Atomic Components
Overview
This project demonstrates a comprehensive JSON-driven UI architecture with atomic component design and custom React hooks. Build entire applications from declarative JSON schemas while maintaining clean, maintainable code through small, focused components.
Key Features
🎯 JSON-Driven UI System
- Define complete page layouts using JSON schemas
- Automatic data source management (KV persistence, computed values, static data)
- Declarative action system (CRUD, navigation, toasts)
- Component bindings and event handlers
- Type-safe schema definitions
🧩 Atomic Component Library
- Atoms (< 50 LOC): Basic building blocks
Heading,Text,List,GridStatusBadge
- Molecules (50-100 LOC): Composed components
DataCard,SearchInput,ActionBar
- Organisms (100-150 LOC): Complex compositions
- All components follow strict LOC limits for maintainability
🪝 Custom React Hooks
Data Management
useCRUD<T>- Complete CRUD operations with KV persistenceuseSearch<T>- Multi-field search with filteringuseFilter<T>- Advanced filtering with multiple operatorsuseSort<T>- Multi-key sorting with direction toggleuseJSONData- Flexible data managementusePagination<T>- Pagination logicuseLocalStorage<T>- Browser storage management
UI State
useDialog- Dialog/modal state managementuseToggle- Boolean state with helpersuseForm<T>- Complete form handling with validationuseActionExecutor- Execute JSON-defined actions
Quick Start
Using JSON Schemas
import { PageRenderer } from '@/lib/json-ui/page-renderer'
import { hydrateSchema } from '@/schemas/schema-loader'
import analyticsDashboardJson from '@/schemas/analytics-dashboard.json'
const dashboardSchema = hydrateSchema(analyticsDashboardJson)
export function DashboardPage() {
return <PageRenderer schema={dashboardSchema} />
}
Using Atomic Components
import { DataCard, SearchInput, ActionBar } from '@/components/molecules'
import { Grid, Heading } from '@/components/atoms'
import { useCRUD, useSearch } from '@/hooks/data'
export function MyPage() {
const { items, create, remove } = useCRUD({
key: 'my-items',
defaultValue: [],
persist: true
})
const { query, setQuery, filtered } = useSearch({
items,
searchFields: ['name', 'description']
})
return (
<div className="p-6 space-y-6">
<Heading level={1}>My Page</Heading>
<Grid cols={3} gap={4}>
<DataCard title="Total" value={items.length} />
</Grid>
<SearchInput
value={query}
onChange={setQuery}
placeholder="Search..."
/>
<ActionBar
title="Items"
actions={[
{ label: 'Add', onClick: () => create({...}) }
]}
/>
</div>
)
}
Using Custom Hooks
import { useForm } from '@/hooks/ui'
interface FormData {
name: string
email: string
}
export function MyForm() {
const form = useForm<FormData>({
initialValues: { name: '', email: '' },
validate: (values) => {
const errors: any = {}
if (!values.name) errors.name = 'Name is required'
if (!values.email) errors.email = 'Email is required'
return errors
},
onSubmit: async (values) => {
console.log('Form submitted:', values)
}
})
return (
<form onSubmit={form.handleSubmit}>
<input {...form.getFieldProps('name')} />
{form.errors.name && <span>{form.errors.name}</span>}
<button type="submit" disabled={!form.isValid}>
Submit
</button>
</form>
)
}
JSON Schema Structure
const mySchema: PageSchema = {
id: 'my-page',
name: 'My Page',
layout: { type: 'single' },
// Data sources
dataSources: [
{
id: 'items',
type: 'kv', // Persisted to KV store
key: 'my-items',
defaultValue: []
},
{
id: 'stats',
type: 'computed', // Computed from other data
compute: (data) => ({
total: data.items?.length || 0
}),
dependencies: ['items']
}
],
// UI components
components: [
{
id: 'root',
type: 'div',
props: { className: 'p-6' },
children: [
{
id: 'title',
type: 'Heading',
props: {
level: 1,
children: 'My Page'
}
},
{
id: 'stat-card',
type: 'DataCard',
props: { title: 'Total Items' },
bindings: {
value: { source: 'stats', path: 'total' }
}
},
{
id: 'add-button',
type: 'Button',
props: { children: 'Add Item' },
events: [
{
event: 'click',
actions: [
{
id: 'create-item',
type: 'create',
target: 'items',
compute: (data) => ({
id: Date.now(),
name: 'New Item'
})
},
{
id: 'show-toast',
type: 'show-toast',
message: 'Item added!',
variant: 'success'
}
]
}
]
}
]
}
]
}
Available Components
Atoms
Heading- Semantic headings (h1-h6)Text- Text with variants (body, caption, muted, small)List- Render lists from arraysGrid- Responsive grid layoutsStatusBadge- Colored status indicators
Molecules
DataCard- Stat cards with icon, trend, loading statesSearchInput- Search with clear buttonActionBar- Title with action buttons
Shadcn UI Components
All shadcn components are available: Button, Card, Input, Dialog, Tabs, Badge, Progress, etc.
Action Types
CRUD Actions
- create: Add new items to data source
- update: Modify existing data
- delete: Remove items from data source
UI Actions
- show-toast: Display notification (success, error, info, warning)
- navigate: Navigate to different route
- open-dialog: Open a dialog
- close-dialog: Close a dialog
Value Actions
- set-value: Set data source value
- toggle-value: Toggle boolean value
- increment: Increment numeric value
- decrement: Decrement numeric value
Data Source Types
KV (Persistent)
Persists data between sessions using Spark's KV store.
{
id: 'todos',
type: 'kv',
key: 'app-todos',
defaultValue: []
}
Static (Session Only)
Data lives only in memory, reset on page reload.
{
id: 'searchQuery',
type: 'static',
defaultValue: ''
}
Computed (Derived)
Automatically recomputes when dependencies change.
{
id: 'stats',
type: 'computed',
compute: (data) => ({
total: data.todos.length,
completed: data.todos.filter(t => t.completed).length
}),
dependencies: ['todos']
}
Hook Examples
useCRUD
const { items, create, read, update, remove, clear } = useCRUD({
key: 'todos',
defaultValue: [],
persist: true
})
// ✅ CORRECT - Use functional updates
create({ id: Date.now(), text: 'New todo' })
update(todoId, { completed: true })
remove(todoId)
useSearch
const { query, setQuery, filtered, hasQuery, resultCount } = useSearch({
items: todos,
searchFields: ['text', 'tags'],
caseSensitive: false
})
useFilter
const { filtered, filters, addFilter, removeFilter, clearFilters } = useFilter({
items: todos,
initialFilters: [
{ field: 'status', operator: 'equals', value: 'active' }
]
})
addFilter({ field: 'priority', operator: 'in', value: ['high', 'medium'] })
useForm
const form = useForm({
initialValues: { name: '', email: '' },
validate: (values) => {
const errors: any = {}
if (!values.name) errors.name = 'Required'
return errors
},
onSubmit: async (values) => {
// Handle submission
}
})
// Use in JSX
<input {...form.getFieldProps('name')} />
Demo Pages
- AtomicComponentDemo - Shows all atomic components and hooks in action
- DashboardDemoPage - Complete JSON-driven dashboard with projects
- JSONUIPage - Original JSON UI examples
Best Practices
1. Keep Components Small
- Atoms: < 50 LOC
- Molecules: 50-100 LOC
- Organisms: 100-150 LOC
2. Extract Logic to Hooks
❌ Bad: Logic in component
const [items, setItems] = useState([])
const addItem = () => setItems([...items, newItem])
✅ Good: Logic in hook
const { items, create } = useCRUD({ key: 'items', defaultValue: [] })
3. Use Computed Data Sources
❌ Bad: Computing in render
const completed = todos.filter(t => t.completed).length
✅ Good: Computed data source
{
id: 'stats',
type: 'computed',
compute: (data) => ({
completed: data.todos.filter(t => t.completed).length
}),
dependencies: ['todos']
}
4. Compose Atomic Components
Build complex UIs from simple atoms:
<Card>
<CardHeader>
<CardTitle>Dashboard</CardTitle>
</CardHeader>
<CardContent>
<Grid cols={3} gap={4}>
<DataCard title="Total" value={stats.total} />
<DataCard title="Active" value={stats.active} />
<DataCard title="Done" value={stats.done} />
</Grid>
</CardContent>
</Card>
File Structure
src/
├── components/
│ ├── atoms/ # < 50 LOC components
│ │ ├── Heading.tsx
│ │ ├── Text.tsx
│ │ ├── List.tsx
│ │ ├── Grid.tsx
│ │ └── StatusBadge.tsx
│ ├── molecules/ # 50-100 LOC components
│ │ ├── DataCard.tsx
│ │ ├── SearchInput.tsx
│ │ └── ActionBar.tsx
│ └── ui/ # shadcn components
├── hooks/
│ ├── data/ # Data management hooks
│ │ ├── use-crud.ts
│ │ ├── use-search.ts
│ │ ├── use-filter.ts
│ │ └── use-sort.ts
│ └── ui/ # UI state hooks
│ ├── use-dialog.ts
│ ├── use-toggle.ts
│ └── use-form.ts
├── lib/
│ └── json-ui/ # JSON UI system
│ ├── page-renderer.tsx
│ ├── component-renderer.tsx
│ └── component-registry.tsx
├── schemas/ # JSON page schemas
│ ├── analytics-dashboard.json
│ ├── todo-list.json
│ ├── dashboard-simple.json
│ ├── new-molecules-showcase.json
│ ├── compute-functions.ts
│ └── schema-loader.ts
└── types/
└── json-ui.ts # TypeScript types
Documentation
- JSON_UI_GUIDE.md - Complete JSON UI documentation
- PRD.md - Product requirements and design decisions
Contributing
When adding new components or hooks:
- Follow LOC limits (atoms < 50, molecules < 100, organisms < 150)
- Add TypeScript types
- Update component registry if adding UI components
- Document in README
- Add examples
Built with React, TypeScript, Tailwind CSS, and Shadcn UI