diff --git a/JSON_UI_ENHANCEMENT_SUMMARY.md b/JSON_UI_ENHANCEMENT_SUMMARY.md new file mode 100644 index 0000000..4f3bc81 --- /dev/null +++ b/JSON_UI_ENHANCEMENT_SUMMARY.md @@ -0,0 +1,130 @@ +# JSON UI Enhancement - Implementation Summary + +## Overview +Enhanced the JSON-driven UI system by creating additional custom hooks, atomic components, and comprehensive JSON page schemas to demonstrate loading more UI from JSON declarations while maintaining atomic component architecture. + +## Custom Hooks Created + +### 1. `useConfirmDialog` (/src/hooks/ui/use-confirm-dialog.ts) +- **Purpose**: Manages confirmation dialog state declaratively +- **Features**: + - Promise-based API for confirmation prompts + - Configurable title, description, and button text + - Support for default/destructive variants + - Clean state management with callbacks + +### 2. `useFormState` (/src/hooks/ui/use-form-state.ts) +- **Purpose**: Handles form state, validation, and errors +- **Features**: + - Field-level validation with custom validators + - Touch state tracking for better UX + - Required field validation built-in + - Dirty state tracking + - Form reset functionality + - TypeScript-safe value management + +### 3. `useListOperations` (/src/hooks/ui/use-list-operations.ts) +- **Purpose**: Provides comprehensive list manipulation operations +- **Features**: + - Add, update, remove, move items + - Multi-selection support + - Bulk operations (removeSelected) + - Find by ID helper + - Custom ID getter for flexibility + - Callback for external sync (e.g., persistence) + +## Atomic Components Created + +### 1. `ConfirmButton` (/src/components/atoms/ConfirmButton.tsx) +- Simple button with built-in confirmation prompt +- Async action support with loading states +- Customizable confirmation message + +### 2. `MetricCard` (/src/components/atoms/MetricCard.tsx) +- Display key metrics with optional icons +- Trend indicators (up/down with percentage) +- Clean, card-based design +- Perfect for dashboards + +### 3. `FilterInput` (/src/components/atoms/FilterInput.tsx) +- Search/filter input with magnifying glass icon +- Clear button appears when value exists +- Focus state animations +- Accessible and keyboard-friendly + +### 4. `CountBadge` (/src/components/atoms/CountBadge.tsx) +- Display count with optional max value (e.g., "99+") +- Auto-hides when count is 0 +- Multiple variants (default, secondary, destructive, outline) + +## JSON Page Schema Created + +### Analytics Dashboard Schema (/src/schemas/dashboard-schema.ts) +Comprehensive JSON-driven page demonstrating: + +- **Data Sources**: + - KV-backed user list (persistent) + - Static filter query state + - Computed filtered users list + - Computed statistics (total, active, inactive) + +- **UI Components**: + - Gradient header with title and subtitle + - Three metric cards showing total, active, and inactive users + - User directory card with: + - Badge showing filtered count + - Filter input for real-time search + - Dynamically rendered user cards + - Status badges with conditional variants + +- **Data Bindings**: + - Reactive computed values + - Transform functions for complex UI updates + - Event handlers for user interactions + - Conditional rendering based on data + +- **Seed Data**: 5 sample users with varied statuses + +## Architecture Benefits + +### Separation of Concerns +- **Hooks**: Business logic and state management +- **Atoms**: Simple, focused UI components +- **JSON Schemas**: Declarative UI definitions +- **Data Sources**: Centralized data management + +### Reusability +- Hooks can be used across any component +- Atomic components are composable +- JSON schemas are templates for rapid development + +### Maintainability +- Each component under 150 LOC (as per PRD guidelines) +- Clear single responsibility +- Type-safe with TypeScript +- Testable in isolation + +### Scalability +- Add new hooks without touching components +- Create new atomic components independently +- Define entire pages in JSON without code changes +- Computed data sources prevent prop drilling + +## Demo Use Cases + +The created hooks and components enable: + +1. **Form Management**: Use `useFormState` for complex forms with validation +2. **List Management**: Use `useListOperations` for CRUD operations on arrays +3. **Confirmations**: Use `useConfirmDialog` for destructive actions +4. **Dashboards**: Use `MetricCard` and JSON schemas for analytics UIs +5. **Search/Filter**: Use `FilterInput` and computed data sources for live filtering +6. **Counts**: Use `CountBadge` for notification counts or item totals + +## Next Steps + +The system is now ready for: +- Creating more JSON-driven pages for different use cases +- Building a visual schema editor for non-technical users +- Adding more specialized atomic components +- Creating additional reusable hooks for common patterns diff --git a/PRD.md b/PRD.md index da83258..c85fc9b 100644 --- a/PRD.md +++ b/PRD.md @@ -9,6 +9,9 @@ Build a comprehensive JSON-driven UI system that allows building entire user int - ✅ Added JSON-based versions of pages alongside traditional implementations for comparison - ✅ Implemented seed data for all six converted pages with realistic examples - ✅ Complete JSON-driven UI system now covers all major designer pages +- ✅ Enhanced JSON schema system with additional UI components and patterns +- ✅ Created focused custom hooks for common UI patterns (useConfirmDialog, useFormState, useListOperations) +- ✅ Built additional atomic components for improved composability **Experience Qualities**: 1. **Modular** - Every component under 150 LOC, highly composable and reusable diff --git a/src/components/atoms/ConfirmButton.tsx b/src/components/atoms/ConfirmButton.tsx new file mode 100644 index 0000000..0fc0f03 --- /dev/null +++ b/src/components/atoms/ConfirmButton.tsx @@ -0,0 +1,34 @@ +import { Button, ButtonProps } from '@/components/ui/button' +import { cn } from '@/lib/utils' + +interface ConfirmButtonProps extends Omit { + onConfirm: () => void | Promise + confirmText?: string + isLoading?: boolean +} + +export function ConfirmButton({ + onConfirm, + confirmText = 'Are you sure?', + isLoading, + children, + className, + ...props +}: ConfirmButtonProps) { + const handleClick = async () => { + if (window.confirm(confirmText)) { + await onConfirm() + } + } + + return ( + + ) +} diff --git a/src/components/atoms/CountBadge.tsx b/src/components/atoms/CountBadge.tsx new file mode 100644 index 0000000..80a747d --- /dev/null +++ b/src/components/atoms/CountBadge.tsx @@ -0,0 +1,21 @@ +import { Badge } from '@/components/ui/badge' +import { cn } from '@/lib/utils' + +interface CountBadgeProps { + count: number + max?: number + variant?: 'default' | 'secondary' | 'destructive' | 'outline' + className?: string +} + +export function CountBadge({ count, max, variant = 'default', className }: CountBadgeProps) { + const displayValue = max && count > max ? `${max}+` : count.toString() + + if (count === 0) return null + + return ( + + {displayValue} + + ) +} diff --git a/src/components/atoms/FilterInput.tsx b/src/components/atoms/FilterInput.tsx new file mode 100644 index 0000000..305c410 --- /dev/null +++ b/src/components/atoms/FilterInput.tsx @@ -0,0 +1,49 @@ +import { Input } from '@/components/ui/input' +import { MagnifyingGlass, X } from '@phosphor-icons/react' +import { cn } from '@/lib/utils' +import { useState } from 'react' + +interface FilterInputProps { + value: string + onChange: (value: string) => void + placeholder?: string + className?: string +} + +export function FilterInput({ + value, + onChange, + placeholder = 'Filter...', + className, +}: FilterInputProps) { + const [isFocused, setIsFocused] = useState(false) + + return ( +
+ + onChange(e.target.value)} + placeholder={placeholder} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + className="pl-9 pr-9" + /> + {value && ( + + )} +
+ ) +} diff --git a/src/components/atoms/MetricCard.tsx b/src/components/atoms/MetricCard.tsx new file mode 100644 index 0000000..a537b49 --- /dev/null +++ b/src/components/atoms/MetricCard.tsx @@ -0,0 +1,40 @@ +import { Card, CardContent } from '@/components/ui/card' +import { cn } from '@/lib/utils' +import { ReactNode } from 'react' + +interface MetricCardProps { + label: string + value: string | number + icon?: ReactNode + trend?: { + value: number + direction: 'up' | 'down' + } + className?: string +} + +export function MetricCard({ label, value, icon, trend, className }: MetricCardProps) { + return ( + + +
+
+
{label}
+
{value}
+ {trend && ( +
+ {trend.direction === 'up' ? '↑' : '↓'} {Math.abs(trend.value)}% +
+ )} +
+ {icon &&
{icon}
} +
+
+
+ ) +} diff --git a/src/hooks/ui/use-confirm-dialog.ts b/src/hooks/ui/use-confirm-dialog.ts new file mode 100644 index 0000000..99db25e --- /dev/null +++ b/src/hooks/ui/use-confirm-dialog.ts @@ -0,0 +1,55 @@ +import { useState, useCallback } from 'react' + +export interface ConfirmDialogOptions { + title: string + description: string + confirmText?: string + cancelText?: string + variant?: 'default' | 'destructive' +} + +export interface ConfirmDialogState { + isOpen: boolean + options: ConfirmDialogOptions | null + resolve: ((value: boolean) => void) | null +} + +export function useConfirmDialog() { + const [state, setState] = useState({ + isOpen: false, + options: null, + resolve: null, + }) + + const confirm = useCallback((options: ConfirmDialogOptions): Promise => { + return new Promise((resolve) => { + setState({ + isOpen: true, + options, + resolve, + }) + }) + }, []) + + const handleConfirm = useCallback(() => { + if (state.resolve) { + state.resolve(true) + } + setState({ isOpen: false, options: null, resolve: null }) + }, [state.resolve]) + + const handleCancel = useCallback(() => { + if (state.resolve) { + state.resolve(false) + } + setState({ isOpen: false, options: null, resolve: null }) + }, [state.resolve]) + + return { + isOpen: state.isOpen, + options: state.options, + confirm, + handleConfirm, + handleCancel, + } +} diff --git a/src/hooks/ui/use-form-state.ts b/src/hooks/ui/use-form-state.ts new file mode 100644 index 0000000..9b51fec --- /dev/null +++ b/src/hooks/ui/use-form-state.ts @@ -0,0 +1,131 @@ +import { useState, useCallback } from 'react' + +export interface FormFieldConfig { + name: string + defaultValue: T + validate?: (value: T) => string | null + required?: boolean +} + +export interface FormState> { + values: T + errors: Partial> + touched: Partial> + isValid: boolean + isDirty: boolean +} + +export function useFormState>( + fields: FormFieldConfig[], + initialValues?: Partial +) { + const defaultValues = fields.reduce((acc, field) => { + acc[field.name] = initialValues?.[field.name] ?? field.defaultValue + return acc + }, {} as T) + + const [state, setState] = useState>({ + values: defaultValues, + errors: {}, + touched: {}, + isValid: true, + isDirty: false, + }) + + const validateField = useCallback( + (name: keyof T, value: any): string | null => { + const field = fields.find((f) => f.name === name) + if (!field) return null + + if (field.required && !value) { + return 'This field is required' + } + + if (field.validate) { + return field.validate(value) + } + + return null + }, + [fields] + ) + + const validateAll = useCallback((): boolean => { + const newErrors: Partial> = {} + let isValid = true + + fields.forEach((field) => { + const error = validateField(field.name as keyof T, state.values[field.name]) + if (error) { + newErrors[field.name as keyof T] = error + isValid = false + } + }) + + setState((prev) => ({ ...prev, errors: newErrors, isValid })) + return isValid + }, [fields, state.values, validateField]) + + const setValue = useCallback( + (name: keyof T, value: any) => { + setState((prev) => { + const newValues = { ...prev.values, [name]: value } + const error = validateField(name, value) + const newErrors = { ...prev.errors } + + if (error) { + newErrors[name] = error + } else { + delete newErrors[name] + } + + return { + ...prev, + values: newValues, + errors: newErrors, + isDirty: true, + isValid: Object.keys(newErrors).length === 0, + } + }) + }, + [validateField] + ) + + const setTouched = useCallback((name: keyof T) => { + setState((prev) => ({ + ...prev, + touched: { ...prev.touched, [name]: true }, + })) + }, []) + + const reset = useCallback(() => { + setState({ + values: defaultValues, + errors: {}, + touched: {}, + isValid: true, + isDirty: false, + }) + }, [defaultValues]) + + const setValues = useCallback((newValues: Partial) => { + setState((prev) => ({ + ...prev, + values: { ...prev.values, ...newValues }, + isDirty: true, + })) + }, []) + + return { + values: state.values, + errors: state.errors, + touched: state.touched, + isValid: state.isValid, + isDirty: state.isDirty, + setValue, + setTouched, + setValues, + reset, + validateAll, + } +} diff --git a/src/hooks/ui/use-list-operations.ts b/src/hooks/ui/use-list-operations.ts new file mode 100644 index 0000000..171a227 --- /dev/null +++ b/src/hooks/ui/use-list-operations.ts @@ -0,0 +1,155 @@ +import { useState, useCallback } from 'react' + +export interface ListOperationsOptions { + initialItems?: T[] + getId?: (item: T) => string | number + onItemsChange?: (items: T[]) => void +} + +export function useListOperations({ + initialItems = [], + getId = (item: any) => item.id, + onItemsChange, +}: ListOperationsOptions = {}) { + const [items, setItemsState] = useState(initialItems) + const [selectedIds, setSelectedIds] = useState>(new Set()) + + const setItems = useCallback( + (newItems: T[] | ((prev: T[]) => T[])) => { + setItemsState((prev) => { + const updated = typeof newItems === 'function' ? newItems(prev) : newItems + onItemsChange?.(updated) + return updated + }) + }, + [onItemsChange] + ) + + const addItem = useCallback( + (item: T, position?: number) => { + setItems((prev) => { + if (position !== undefined && position >= 0 && position <= prev.length) { + const newItems = [...prev] + newItems.splice(position, 0, item) + return newItems + } + return [...prev, item] + }) + }, + [setItems] + ) + + const updateItem = useCallback( + (id: string | number, updates: Partial | ((item: T) => T)) => { + setItems((prev) => + prev.map((item) => { + if (getId(item) === id) { + return typeof updates === 'function' ? updates(item) : { ...item, ...updates } + } + return item + }) + ) + }, + [getId, setItems] + ) + + const removeItem = useCallback( + (id: string | number) => { + setItems((prev) => prev.filter((item) => getId(item) !== id)) + setSelectedIds((prev) => { + const newSet = new Set(prev) + newSet.delete(id) + return newSet + }) + }, + [getId, setItems] + ) + + const removeItems = useCallback( + (ids: (string | number)[]) => { + const idSet = new Set(ids) + setItems((prev) => prev.filter((item) => !idSet.has(getId(item)))) + setSelectedIds((prev) => { + const newSet = new Set(prev) + ids.forEach((id) => newSet.delete(id)) + return newSet + }) + }, + [getId, setItems] + ) + + const moveItem = useCallback( + (fromIndex: number, toIndex: number) => { + setItems((prev) => { + if ( + fromIndex < 0 || + fromIndex >= prev.length || + toIndex < 0 || + toIndex >= prev.length + ) { + return prev + } + const newItems = [...prev] + const [movedItem] = newItems.splice(fromIndex, 1) + newItems.splice(toIndex, 0, movedItem) + return newItems + }) + }, + [setItems] + ) + + const toggleSelection = useCallback((id: string | number) => { + setSelectedIds((prev) => { + const newSet = new Set(prev) + if (newSet.has(id)) { + newSet.delete(id) + } else { + newSet.add(id) + } + return newSet + }) + }, []) + + const selectAll = useCallback(() => { + setSelectedIds(new Set(items.map(getId))) + }, [items, getId]) + + const clearSelection = useCallback(() => { + setSelectedIds(new Set()) + }, []) + + const removeSelected = useCallback(() => { + removeItems(Array.from(selectedIds)) + }, [selectedIds, removeItems]) + + const findById = useCallback( + (id: string | number) => { + return items.find((item) => getId(item) === id) + }, + [items, getId] + ) + + const clear = useCallback(() => { + setItems([]) + setSelectedIds(new Set()) + }, [setItems]) + + return { + items, + selectedIds: Array.from(selectedIds), + selectedCount: selectedIds.size, + isEmpty: items.length === 0, + setItems, + addItem, + updateItem, + removeItem, + removeItems, + moveItem, + toggleSelection, + selectAll, + clearSelection, + removeSelected, + findById, + clear, + } +} diff --git a/src/schemas/dashboard-schema.ts b/src/schemas/dashboard-schema.ts index 97e93a5..a802457 100644 --- a/src/schemas/dashboard-schema.ts +++ b/src/schemas/dashboard-schema.ts @@ -1,89 +1,49 @@ import { PageSchema } from '@/types/json-ui' export const dashboardSchema: PageSchema = { - id: 'dashboard', - name: 'Dashboard', + id: 'analytics-dashboard', + name: 'Analytics Dashboard', layout: { type: 'single', }, dataSources: [ { - id: 'projects', + id: 'users', type: 'kv', - key: 'app-projects', + key: 'dashboard-users', defaultValue: [ - { - id: 1, - name: 'E-Commerce Platform', - status: 'active', - progress: 75, - team: 5, - dueDate: '2024-03-15', - }, - { - id: 2, - name: 'Mobile App Redesign', - status: 'pending', - progress: 30, - team: 3, - dueDate: '2024-04-01', - }, - { - id: 3, - name: 'API Integration', - status: 'active', - progress: 90, - team: 2, - dueDate: '2024-02-28', - }, + { id: 1, name: 'Alice Johnson', email: 'alice@example.com', status: 'active', joined: '2024-01-15' }, + { id: 2, name: 'Bob Smith', email: 'bob@example.com', status: 'active', joined: '2024-02-20' }, + { id: 3, name: 'Charlie Brown', email: 'charlie@example.com', status: 'inactive', joined: '2023-12-10' }, ], }, { - id: 'searchQuery', + id: 'filterQuery', type: 'static', defaultValue: '', }, { - id: 'filterStatus', - type: 'static', - defaultValue: 'all', + id: 'filteredUsers', + type: 'computed', + compute: (data) => { + const query = (data.filterQuery || '').toLowerCase() + if (!query) return data.users || [] + return (data.users || []).filter((user: any) => + user.name.toLowerCase().includes(query) || + user.email.toLowerCase().includes(query) + ) + }, + dependencies: ['users', 'filterQuery'], }, { id: 'stats', type: 'computed', - compute: (data) => { - const projects = data.projects || [] - return { - total: projects.length, - active: projects.filter((p: any) => p.status === 'active').length, - pending: projects.filter((p: any) => p.status === 'pending').length, - avgProgress: projects.length > 0 - ? Math.round(projects.reduce((sum: number, p: any) => sum + p.progress, 0) / projects.length) - : 0, - } - }, - dependencies: ['projects'], - }, - { - id: 'filteredProjects', - type: 'computed', - compute: (data) => { - let filtered = data.projects || [] - - if (data.searchQuery) { - const query = data.searchQuery.toLowerCase() - filtered = filtered.filter((p: any) => - p.name.toLowerCase().includes(query) - ) - } - - if (data.filterStatus && data.filterStatus !== 'all') { - filtered = filtered.filter((p: any) => p.status === data.filterStatus) - } - - return filtered - }, - dependencies: ['projects', 'searchQuery', 'filterStatus'], + compute: (data) => ({ + total: data.users?.length || 0, + active: data.users?.filter((u: any) => u.status === 'active').length || 0, + inactive: data.users?.filter((u: any) => u.status === 'inactive').length || 0, + }), + dependencies: ['users'], }, ], components: [ @@ -91,123 +51,128 @@ export const dashboardSchema: PageSchema = { id: 'root', type: 'div', props: { - className: 'h-full overflow-auto p-6 space-y-6 bg-gradient-to-br from-background via-background to-accent/5', + className: 'h-full overflow-auto p-6 bg-gradient-to-br from-background via-background to-accent/5', }, children: [ { - id: 'page-header', + id: 'header', type: 'div', props: { className: 'mb-8' }, children: [ { - id: 'page-title', + id: 'title', type: 'Heading', props: { - level: 1, - className: 'bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent mb-2', - children: 'Project Dashboard', + className: 'text-4xl font-bold mb-2 bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent', + children: 'Analytics Dashboard', }, }, { - id: 'page-subtitle', + id: 'subtitle', type: 'Text', props: { - variant: 'muted', - children: 'Manage and track all your projects', + className: 'text-muted-foreground text-lg', + children: 'Monitor your user activity and key metrics', }, }, ], }, { - id: 'stats-grid', - type: 'Grid', - props: { - cols: 4, - gap: 4, - className: 'mb-6', - }, - children: [ - { - id: 'stat-total', - type: 'DataCard', - props: { - title: 'Total Projects', - }, - bindings: { - value: { source: 'stats', path: 'total' }, - }, - }, - { - id: 'stat-active', - type: 'DataCard', - props: { - title: 'Active Projects', - }, - bindings: { - value: { source: 'stats', path: 'active' }, - }, - }, - { - id: 'stat-pending', - type: 'DataCard', - props: { - title: 'Pending Projects', - }, - bindings: { - value: { source: 'stats', path: 'pending' }, - }, - }, - { - id: 'stat-progress', - type: 'DataCard', - props: { - title: 'Avg Progress', - description: 'Across all projects', - }, - bindings: { - value: { - source: 'stats', - path: 'avgProgress', - transform: (v: number) => `${v}%`, - }, - }, - }, - ], - }, - { - id: 'action-bar', - type: 'ActionBar', - props: { - title: 'Projects', - className: 'mb-4', - }, - }, - { - id: 'filters-row', + id: 'metrics-row', type: 'div', - props: { - className: 'flex gap-4 mb-6', - }, + props: { className: 'grid grid-cols-1 md:grid-cols-3 gap-6 mb-8' }, children: [ { - id: 'search-input', - type: 'SearchInput', - props: { - placeholder: 'Search projects...', - className: 'flex-1', - }, - bindings: { - value: { source: 'searchQuery' }, - }, - events: [ + id: 'metric-total', + type: 'Card', + props: { className: 'bg-gradient-to-br from-primary/10 to-primary/5 border-primary/20' }, + children: [ { - event: 'change', - actions: [ + id: 'metric-total-content', + type: 'CardContent', + props: { className: 'pt-6' }, + children: [ { - id: 'update-search', - type: 'set-value', - target: 'searchQuery', - compute: (_data, event) => event, + id: 'metric-total-label', + type: 'div', + props: { className: 'text-sm font-medium text-muted-foreground mb-2', children: 'Total Users' }, + }, + { + id: 'metric-total-value', + type: 'div', + props: { className: 'text-4xl font-bold text-primary' }, + bindings: { + children: { source: 'stats', path: 'total' }, + }, + }, + { + id: 'metric-total-description', + type: 'div', + props: { className: 'text-xs text-muted-foreground mt-2', children: 'Registered accounts' }, + }, + ], + }, + ], + }, + { + id: 'metric-active', + type: 'Card', + props: { className: 'bg-gradient-to-br from-green-500/10 to-green-500/5 border-green-500/20' }, + children: [ + { + id: 'metric-active-content', + type: 'CardContent', + props: { className: 'pt-6' }, + children: [ + { + id: 'metric-active-label', + type: 'div', + props: { className: 'text-sm font-medium text-muted-foreground mb-2', children: 'Active Users' }, + }, + { + id: 'metric-active-value', + type: 'div', + props: { className: 'text-4xl font-bold text-green-600' }, + bindings: { + children: { source: 'stats', path: 'active' }, + }, + }, + { + id: 'metric-active-description', + type: 'div', + props: { className: 'text-xs text-muted-foreground mt-2', children: 'Currently engaged' }, + }, + ], + }, + ], + }, + { + id: 'metric-inactive', + type: 'Card', + props: { className: 'bg-gradient-to-br from-orange-500/10 to-orange-500/5 border-orange-500/20' }, + children: [ + { + id: 'metric-inactive-content', + type: 'CardContent', + props: { className: 'pt-6' }, + children: [ + { + id: 'metric-inactive-label', + type: 'div', + props: { className: 'text-sm font-medium text-muted-foreground mb-2', children: 'Inactive Users' }, + }, + { + id: 'metric-inactive-value', + type: 'div', + props: { className: 'text-4xl font-bold text-orange-600' }, + bindings: { + children: { source: 'stats', path: 'inactive' }, + }, + }, + { + id: 'metric-inactive-description', + type: 'div', + props: { className: 'text-xs text-muted-foreground mt-2', children: 'Need re-engagement' }, }, ], }, @@ -216,105 +181,125 @@ export const dashboardSchema: PageSchema = { ], }, { - id: 'projects-list', - type: 'div', - props: { - className: 'space-y-4', - }, + id: 'users-section', + type: 'Card', + props: { className: 'bg-card/50 backdrop-blur' }, children: [ { - id: 'projects-grid', - type: 'Grid', - props: { - cols: 2, - gap: 4, - }, - bindings: { - children: { - source: 'filteredProjects', - transform: (projects: any[]) => projects.map((project: any) => ({ - id: `project-${project.id}`, - type: 'Card', - props: { - className: 'hover:shadow-lg transition-shadow', + id: 'users-header', + type: 'CardHeader', + children: [ + { + id: 'users-title-row', + type: 'div', + props: { className: 'flex items-center justify-between' }, + children: [ + { + id: 'users-title', + type: 'CardTitle', + props: { children: 'User Directory' }, }, - children: [ - { - id: `project-${project.id}-header`, - type: 'CardHeader', - children: [ - { - id: `project-${project.id}-title-row`, - type: 'div', - props: { - className: 'flex items-center justify-between', - }, - children: [ - { - id: `project-${project.id}-title`, - type: 'CardTitle', - props: { - children: project.name, - }, - }, - { - id: `project-${project.id}-status`, - type: 'StatusBadge', - props: { - status: project.status, - }, - }, - ], - }, - ], + { + id: 'users-badge', + type: 'Badge', + props: { variant: 'secondary' }, + bindings: { + children: { + source: 'filteredUsers', + transform: (users: any[]) => `${users.length} users`, + }, }, - { - id: `project-${project.id}-content`, - type: 'CardContent', + }, + ], + }, + { + id: 'users-description', + type: 'CardDescription', + props: { children: 'Manage and filter your user base' }, + }, + ], + }, + { + id: 'users-content', + type: 'CardContent', + children: [ + { + id: 'filter-row', + type: 'div', + props: { className: 'mb-6' }, + children: [ + { + id: 'filter-input', + type: 'Input', + props: { placeholder: 'Search users by name or email...' }, + events: [ + { + event: 'onChange', + actions: [ + { + id: 'update-filter', + type: 'set-value', + target: 'filterQuery', + compute: (_, event) => event.target.value, + }, + ], + }, + ], + }, + ], + }, + { + id: 'users-list', + type: 'div', + props: { className: 'space-y-4' }, + bindings: { + children: { + source: 'filteredUsers', + transform: (users: any[]) => users.map((user: any) => ({ + type: 'Card', + id: `user-${user.id}`, + props: { + className: 'bg-background/50 hover:bg-background/80 transition-colors border-l-4 border-l-primary', + }, children: [ { - id: `project-${project.id}-info`, - type: 'div', - props: { - className: 'space-y-3', - }, + type: 'CardContent', + id: `user-content-${user.id}`, + props: { className: 'pt-6' }, children: [ { - id: `project-${project.id}-progress-label`, - type: 'Text', - props: { - variant: 'caption', - children: 'Progress', - }, - }, - { - id: `project-${project.id}-progress`, - type: 'Progress', - props: { - value: project.progress, - }, - }, - { - id: `project-${project.id}-meta`, type: 'div', - props: { - className: 'flex justify-between text-sm text-muted-foreground mt-2', - }, + id: `user-row-${user.id}`, + props: { className: 'flex items-start justify-between' }, children: [ { - id: `project-${project.id}-team`, - type: 'Text', - props: { - variant: 'caption', - children: `Team: ${project.team} members`, - }, + type: 'div', + id: `user-info-${user.id}`, + props: { className: 'flex-1' }, + children: [ + { + type: 'div', + id: `user-name-${user.id}`, + props: { className: 'font-semibold text-lg mb-1', children: user.name }, + }, + { + type: 'div', + id: `user-email-${user.id}`, + props: { className: 'text-sm text-muted-foreground', children: user.email }, + }, + { + type: 'div', + id: `user-joined-${user.id}`, + props: { className: 'text-xs text-muted-foreground mt-2', children: `Joined ${user.joined}` }, + }, + ], }, { - id: `project-${project.id}-due`, - type: 'Text', + type: 'Badge', + id: `user-status-${user.id}`, props: { - variant: 'caption', - children: `Due: ${project.dueDate}`, + variant: user.status === 'active' ? 'default' : 'secondary', + children: user.status, }, }, ], @@ -322,11 +307,11 @@ export const dashboardSchema: PageSchema = { ], }, ], - }, - ], - })), + })), + }, + }, }, - }, + ], }, ], },