Chris Tate 5 miesięcy temu
rodzic
commit
b5ccdc92de

+ 0 - 1
apps/web/components/demo.tsx

@@ -227,7 +227,6 @@ export function Demo() {
                 className="flex-1 bg-transparent outline-none placeholder:text-muted-foreground/50 text-base"
                 disabled={isStreaming}
                 maxLength={140}
-                autoFocus
               />
             </form>
           )}

+ 19 - 3
examples/dashboard/app/globals.css

@@ -2,17 +2,28 @@
   box-sizing: border-box;
 }
 
+:root {
+  --background: #000;
+  --foreground: #fafafa;
+  --card: #0a0a0a;
+  --border: #262626;
+  --muted: #a3a3a3;
+  --radius: 8px;
+}
+
 html,
 body {
   margin: 0;
   padding: 0;
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
-    sans-serif;
-  background-color: #f9fafb;
+  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+  background-color: var(--background);
+  color: var(--foreground);
+  -webkit-font-smoothing: antialiased;
 }
 
 button {
   font-family: inherit;
+  cursor: pointer;
 }
 
 input,
@@ -20,3 +31,8 @@ select,
 textarea {
   font-family: inherit;
 }
+
+::selection {
+  background: var(--foreground);
+  color: var(--background);
+}

+ 4 - 13
examples/dashboard/app/layout.tsx

@@ -1,8 +1,9 @@
 import type { Metadata } from 'next';
+import './globals.css';
 
 export const metadata: Metadata = {
-  title: 'json-render Dashboard Demo',
-  description: 'AI-powered dashboard widget generator with guardrails',
+  title: 'Dashboard | json-render',
+  description: 'AI-generated dashboard widgets with guardrails',
 };
 
 export default function RootLayout({
@@ -12,17 +13,7 @@ export default function RootLayout({
 }) {
   return (
     <html lang="en">
-      <body
-        style={{
-          margin: 0,
-          fontFamily:
-            '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
-          backgroundColor: '#f9fafb',
-          minHeight: '100vh',
-        }}
-      >
-        {children}
-      </body>
+      <body>{children}</body>
     </html>
   );
 }

+ 106 - 200
examples/dashboard/app/page.tsx

@@ -8,9 +8,8 @@ import {
   useUIStream,
   Renderer,
 } from '@json-render/react';
-import { componentRegistry } from '@/components/ui-components';
+import { componentRegistry } from '@/components/ui';
 
-// Sample data for the dashboard
 const INITIAL_DATA = {
   analytics: {
     revenue: 125000,
@@ -36,32 +35,16 @@ const INITIAL_DATA = {
   },
 };
 
-// Action handlers
 const ACTION_HANDLERS = {
-  export_report: () => {
-    alert('Exporting report... (demo)');
-  },
-  refresh_data: () => {
-    alert('Refreshing data... (demo)');
-  },
-  view_details: (params: Record<string, unknown>) => {
-    alert(`Viewing details for: ${JSON.stringify(params)}`);
-  },
-  apply_filter: () => {
-    alert('Applying filters... (demo)');
-  },
+  export_report: () => alert('Exporting report...'),
+  refresh_data: () => alert('Refreshing data...'),
+  view_details: (params: Record<string, unknown>) => alert(`Details: ${JSON.stringify(params)}`),
+  apply_filter: () => alert('Applying filters...'),
 };
 
 function DashboardContent() {
   const [prompt, setPrompt] = useState('');
-
-  const {
-    tree,
-    isStreaming,
-    error,
-    send,
-    clear,
-  } = useUIStream({
+  const { tree, isStreaming, error, send, clear } = useUIStream({
     api: '/api/generate',
     onError: (err) => console.error('Generation error:', err),
   });
@@ -75,231 +58,154 @@ function DashboardContent() {
     [prompt, send]
   );
 
-  const examplePrompts = [
-    'Show me a revenue dashboard with key metrics and a chart of sales by region',
-    'Create a transactions table with recent orders',
-    'Build a widget showing customer count with a trend indicator',
-    'Make a simple card with total revenue and an export button',
+  const examples = [
+    'Revenue dashboard with metrics and chart',
+    'Recent transactions table',
+    'Customer count with trend',
   ];
 
   const hasElements = tree && Object.keys(tree.elements).length > 0;
 
   return (
-    <div style={{ maxWidth: '1200px', margin: '0 auto', padding: '24px' }}>
-      {/* Header */}
-      <div style={{ marginBottom: '32px' }}>
-        <h1
-          style={{
-            margin: '0 0 8px 0',
-            fontSize: '28px',
-            fontWeight: 700,
-            color: '#111827',
-          }}
-        >
-          json-render Dashboard Demo
+    <div style={{ maxWidth: 960, margin: '0 auto', padding: '48px 24px' }}>
+      <header style={{ marginBottom: 48 }}>
+        <h1 style={{ margin: 0, fontSize: 32, fontWeight: 600, letterSpacing: '-0.02em' }}>
+          Dashboard
         </h1>
-        <p style={{ margin: 0, color: '#6b7280', fontSize: '16px' }}>
-          AI-powered widget generation with guardrails. Only catalog components can be generated.
+        <p style={{ margin: '8px 0 0', color: 'var(--muted)', fontSize: 16 }}>
+          Generate widgets from prompts. Constrained to your catalog.
         </p>
-      </div>
-
-      {/* Input Form */}
-      <div
-        style={{
-          backgroundColor: '#fff',
-          borderRadius: '12px',
-          border: '1px solid #e5e7eb',
-          padding: '20px',
-          marginBottom: '24px',
-        }}
-      >
-        <form onSubmit={handleSubmit}>
-          <div style={{ display: 'flex', gap: '12px', marginBottom: '16px' }}>
-            <input
-              type="text"
-              value={prompt}
-              onChange={(e) => setPrompt(e.target.value)}
-              placeholder="Describe the widget you want to create..."
-              style={{
-                flex: 1,
-                padding: '12px 16px',
-                borderRadius: '8px',
-                border: '1px solid #d1d5db',
-                fontSize: '15px',
-                outline: 'none',
-              }}
-              disabled={isStreaming}
-            />
+      </header>
+
+      <form onSubmit={handleSubmit} style={{ marginBottom: 32 }}>
+        <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
+          <input
+            type="text"
+            value={prompt}
+            onChange={(e) => setPrompt(e.target.value)}
+            placeholder="Describe what you want..."
+            disabled={isStreaming}
+            style={{
+              flex: 1,
+              padding: '12px 16px',
+              background: 'var(--card)',
+              border: '1px solid var(--border)',
+              borderRadius: 'var(--radius)',
+              color: 'var(--foreground)',
+              fontSize: 15,
+              outline: 'none',
+            }}
+          />
+          <button
+            type="submit"
+            disabled={isStreaming || !prompt.trim()}
+            style={{
+              padding: '12px 24px',
+              background: isStreaming ? 'var(--border)' : 'var(--foreground)',
+              color: 'var(--background)',
+              border: 'none',
+              borderRadius: 'var(--radius)',
+              fontSize: 15,
+              fontWeight: 500,
+              opacity: isStreaming || !prompt.trim() ? 0.5 : 1,
+            }}
+          >
+            {isStreaming ? 'Generating...' : 'Generate'}
+          </button>
+          {hasElements && (
             <button
-              type="submit"
-              disabled={isStreaming || !prompt.trim()}
+              type="button"
+              onClick={clear}
               style={{
-                padding: '12px 24px',
-                backgroundColor: isStreaming ? '#9ca3af' : '#6366f1',
-                color: '#fff',
-                border: 'none',
-                borderRadius: '8px',
-                fontSize: '15px',
-                fontWeight: 500,
-                cursor: isStreaming ? 'not-allowed' : 'pointer',
+                padding: '12px 16px',
+                background: 'transparent',
+                color: 'var(--muted)',
+                border: '1px solid var(--border)',
+                borderRadius: 'var(--radius)',
+                fontSize: 15,
               }}
             >
-              {isStreaming ? 'Generating...' : 'Generate'}
+              Clear
             </button>
+          )}
+        </div>
+
+        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
+          {examples.map((ex) => (
             <button
+              key={ex}
               type="button"
-              onClick={clear}
+              onClick={() => setPrompt(ex)}
               style={{
-                padding: '12px 20px',
-                backgroundColor: '#fff',
-                color: '#374151',
-                border: '1px solid #d1d5db',
-                borderRadius: '8px',
-                fontSize: '15px',
-                fontWeight: 500,
-                cursor: 'pointer',
+                padding: '6px 12px',
+                background: 'var(--card)',
+                color: 'var(--muted)',
+                border: '1px solid var(--border)',
+                borderRadius: 'var(--radius)',
+                fontSize: 13,
               }}
             >
-              Clear
+              {ex}
             </button>
-          </div>
-
-          {/* Example Prompts */}
-          <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
-            <span style={{ fontSize: '13px', color: '#6b7280', marginRight: '4px' }}>
-              Try:
-            </span>
-            {examplePrompts.map((example, i) => (
-              <button
-                key={i}
-                type="button"
-                onClick={() => setPrompt(example)}
-                style={{
-                  padding: '4px 10px',
-                  backgroundColor: '#f3f4f6',
-                  border: 'none',
-                  borderRadius: '4px',
-                  fontSize: '12px',
-                  color: '#4b5563',
-                  cursor: 'pointer',
-                }}
-              >
-                {example.slice(0, 40)}...
-              </button>
-            ))}
-          </div>
-        </form>
-      </div>
+          ))}
+        </div>
+      </form>
 
-      {/* Error Display */}
       {error && (
         <div
           style={{
-            padding: '16px',
-            backgroundColor: '#fef2f2',
-            border: '1px solid #fca5a5',
-            borderRadius: '8px',
-            marginBottom: '24px',
-            color: '#991b1b',
+            padding: 16,
+            marginBottom: 24,
+            background: 'var(--card)',
+            border: '1px solid var(--border)',
+            borderRadius: 'var(--radius)',
+            color: '#ef4444',
+            fontSize: 14,
           }}
         >
-          <strong>Error:</strong> {error.message}
+          {error.message}
         </div>
       )}
 
-      {/* Generated UI */}
       <div
         style={{
-          backgroundColor: '#fff',
-          borderRadius: '12px',
-          border: '1px solid #e5e7eb',
-          padding: '24px',
-          minHeight: '300px',
+          minHeight: 300,
+          padding: 24,
+          background: 'var(--card)',
+          border: '1px solid var(--border)',
+          borderRadius: 'var(--radius)',
         }}
       >
         {!hasElements && !isStreaming ? (
-          <div style={{ textAlign: 'center', padding: '60px 20px', color: '#9ca3af' }}>
-            <div style={{ fontSize: '48px', marginBottom: '16px' }}>🎨</div>
-            <p style={{ fontSize: '16px', margin: 0 }}>
-              Enter a prompt above to generate a dashboard widget
-            </p>
+          <div style={{ textAlign: 'center', padding: '60px 20px', color: 'var(--muted)' }}>
+            <p style={{ margin: 0 }}>Enter a prompt to generate a widget</p>
           </div>
         ) : tree ? (
-          <Renderer
-            tree={tree}
-            registry={componentRegistry}
-            loading={isStreaming}
-          />
+          <Renderer tree={tree} registry={componentRegistry} loading={isStreaming} />
         ) : null}
       </div>
 
-      {/* Debug: Show generated JSON */}
       {hasElements && (
-        <details style={{ marginTop: '24px' }}>
-          <summary
-            style={{
-              cursor: 'pointer',
-              fontSize: '14px',
-              color: '#6b7280',
-              marginBottom: '8px',
-            }}
-          >
-            View Generated JSON
+        <details style={{ marginTop: 24 }}>
+          <summary style={{ cursor: 'pointer', fontSize: 14, color: 'var(--muted)' }}>
+            View JSON
           </summary>
           <pre
             style={{
-              backgroundColor: '#1f2937',
-              color: '#e5e7eb',
-              padding: '16px',
-              borderRadius: '8px',
+              marginTop: 8,
+              padding: 16,
+              background: 'var(--card)',
+              border: '1px solid var(--border)',
+              borderRadius: 'var(--radius)',
               overflow: 'auto',
-              fontSize: '12px',
+              fontSize: 12,
+              color: 'var(--muted)',
             }}
           >
             {JSON.stringify(tree, null, 2)}
           </pre>
         </details>
       )}
-
-      {/* Info Box */}
-      <div
-        style={{
-          marginTop: '32px',
-          padding: '20px',
-          backgroundColor: '#eff6ff',
-          borderRadius: '8px',
-          border: '1px solid #bfdbfe',
-        }}
-      >
-        <h3 style={{ margin: '0 0 12px 0', fontSize: '16px', color: '#1e40af' }}>
-          How json-render Works
-        </h3>
-        <ul
-          style={{
-            margin: 0,
-            paddingLeft: '20px',
-            color: '#1e40af',
-            fontSize: '14px',
-            lineHeight: 1.6,
-          }}
-        >
-          <li>
-            <strong>Guardrails:</strong> AI can only generate components from the catalog (Card,
-            Metric, Chart, etc.)
-          </li>
-          <li>
-            <strong>Data Binding:</strong> Components reference data paths like{' '}
-            <code>/analytics/revenue</code>
-          </li>
-          <li>
-            <strong>Named Actions:</strong> Buttons declare actions like{' '}
-            <code>export_report</code> - you control what they do
-          </li>
-          <li>
-            <strong>Your Design System:</strong> The catalog maps to your actual React components
-          </li>
-        </ul>
-      </div>
     </div>
   );
 }

+ 0 - 716
examples/dashboard/components/ui-components.tsx

@@ -1,716 +0,0 @@
-'use client';
-
-import React, { type ReactNode } from 'react';
-import { type ComponentRenderProps } from '@json-render/react';
-import { useData, useFieldValidation } from '@json-render/react';
-import { getByPath } from '@json-render/core';
-
-// Helper to get value from data model
-function useResolvedValue<T>(value: T | { path: string } | null | undefined): T | undefined {
-  const { data } = useData();
-  
-  if (value === null || value === undefined) {
-    return undefined;
-  }
-  
-  if (typeof value === 'object' && 'path' in value) {
-    return getByPath(data, value.path) as T | undefined;
-  }
-  
-  return value as T;
-}
-
-// Styles
-const styles = {
-  card: {
-    backgroundColor: '#fff',
-    borderRadius: '8px',
-    border: '1px solid #e5e7eb',
-    overflow: 'hidden',
-  },
-  cardHeader: {
-    padding: '16px 20px',
-    borderBottom: '1px solid #e5e7eb',
-  },
-  cardBody: (padding: string | null | undefined) => ({
-    padding:
-      padding === 'none' ? 0 :
-      padding === 'sm' ? '12px' :
-      padding === 'lg' ? '24px' : '16px',
-  }),
-  metric: {
-    display: 'flex',
-    flexDirection: 'column' as const,
-    gap: '4px',
-  },
-  metricLabel: {
-    fontSize: '14px',
-    color: '#6b7280',
-  },
-  metricValue: {
-    fontSize: '32px',
-    fontWeight: 600,
-    color: '#111827',
-  },
-  metricTrend: (trend: string | null | undefined) => ({
-    fontSize: '14px',
-    color: trend === 'up' ? '#10b981' : trend === 'down' ? '#ef4444' : '#6b7280',
-  }),
-};
-
-// Card component
-export function Card({ element, children }: ComponentRenderProps) {
-  const { title, description, padding } = element.props as {
-    title?: string | null;
-    description?: string | null;
-    padding?: string | null;
-  };
-
-  return (
-    <div style={styles.card}>
-      {(title || description) && (
-        <div style={styles.cardHeader}>
-          {title && (
-            <h3 style={{ margin: 0, fontSize: '16px', fontWeight: 600 }}>{title}</h3>
-          )}
-          {description && (
-            <p style={{ margin: '4px 0 0', fontSize: '14px', color: '#6b7280' }}>
-              {description}
-            </p>
-          )}
-        </div>
-      )}
-      <div style={styles.cardBody(padding)}>{children}</div>
-    </div>
-  );
-}
-
-// Heading component
-export function Heading({ element }: ComponentRenderProps) {
-  const { text, level } = element.props as {
-    text: string;
-    level?: string | null;
-  };
-
-  const Tag = (level || 'h2') as keyof React.JSX.IntrinsicElements;
-  const sizes: Record<string, string> = {
-    h1: '28px',
-    h2: '24px',
-    h3: '20px',
-    h4: '16px',
-  };
-
-  return (
-    <Tag style={{ margin: '0 0 16px', fontSize: sizes[level || 'h2'], fontWeight: 600 }}>
-      {text}
-    </Tag>
-  );
-}
-
-// Text component
-export function Text({ element }: ComponentRenderProps) {
-  const { content, variant } = element.props as {
-    content: string;
-    variant?: string | null;
-  };
-
-  const colors: Record<string, string> = {
-    default: '#111827',
-    muted: '#6b7280',
-    success: '#10b981',
-    warning: '#f59e0b',
-    error: '#ef4444',
-  };
-
-  return (
-    <p style={{ margin: 0, color: colors[variant || 'default'] }}>{content}</p>
-  );
-}
-
-// Metric component
-export function Metric({ element }: ComponentRenderProps) {
-  const { label, valuePath, format, trend, trendValue } = element.props as {
-    label: string;
-    valuePath: string;
-    format?: string | null;
-    trend?: string | null;
-    trendValue?: string | null;
-  };
-
-  const { data } = useData();
-  const rawValue = getByPath(data, valuePath);
-
-  let displayValue = String(rawValue ?? '—');
-  if (format === 'currency' && typeof rawValue === 'number') {
-    displayValue = new Intl.NumberFormat('en-US', {
-      style: 'currency',
-      currency: 'USD',
-    }).format(rawValue);
-  } else if (format === 'percent' && typeof rawValue === 'number') {
-    displayValue = new Intl.NumberFormat('en-US', {
-      style: 'percent',
-      minimumFractionDigits: 1,
-    }).format(rawValue);
-  } else if (format === 'number' && typeof rawValue === 'number') {
-    displayValue = new Intl.NumberFormat('en-US').format(rawValue);
-  }
-
-  return (
-    <div style={styles.metric}>
-      <span style={styles.metricLabel}>{label}</span>
-      <span style={styles.metricValue}>{displayValue}</span>
-      {(trend || trendValue) && (
-        <span style={styles.metricTrend(trend)}>
-          {trend === 'up' ? '↑' : trend === 'down' ? '↓' : ''} {trendValue}
-        </span>
-      )}
-    </div>
-  );
-}
-
-// Chart component (simplified placeholder)
-export function Chart({ element }: ComponentRenderProps) {
-  const { title, dataPath, type } = element.props as {
-    title?: string | null;
-    dataPath: string;
-    type?: string | null;
-  };
-
-  const { data } = useData();
-  const chartData = getByPath(data, dataPath) as Array<{ label: string; value: number }> | undefined;
-
-  if (!chartData || !Array.isArray(chartData)) {
-    return <div style={{ padding: '20px', color: '#6b7280' }}>No chart data</div>;
-  }
-
-  const maxValue = Math.max(...chartData.map((d) => d.value));
-
-  return (
-    <div>
-      {title && (
-        <h4 style={{ margin: '0 0 16px', fontSize: '14px', fontWeight: 600 }}>{title}</h4>
-      )}
-      <div style={{ display: 'flex', gap: '8px', alignItems: 'flex-end', height: '120px' }}>
-        {chartData.map((d, i) => (
-          <div
-            key={i}
-            style={{
-              flex: 1,
-              display: 'flex',
-              flexDirection: 'column',
-              alignItems: 'center',
-              gap: '4px',
-            }}
-          >
-            <div
-              style={{
-                width: '100%',
-                height: `${(d.value / maxValue) * 100}%`,
-                backgroundColor: '#6366f1',
-                borderRadius: '4px 4px 0 0',
-                minHeight: '4px',
-              }}
-            />
-            <span style={{ fontSize: '12px', color: '#6b7280' }}>{d.label}</span>
-          </div>
-        ))}
-      </div>
-    </div>
-  );
-}
-
-// Table component
-export function Table({ element }: ComponentRenderProps) {
-  const { title, dataPath, columns } = element.props as {
-    title?: string | null;
-    dataPath: string;
-    columns: Array<{ key: string; label: string; format?: string | null }>;
-  };
-
-  const { data } = useData();
-  const tableData = getByPath(data, dataPath) as Array<Record<string, unknown>> | undefined;
-
-  if (!tableData || !Array.isArray(tableData)) {
-    return <div style={{ padding: '20px', color: '#6b7280' }}>No data</div>;
-  }
-
-  const formatCell = (value: unknown, format?: string | null) => {
-    if (value === null || value === undefined) return '—';
-    if (format === 'currency' && typeof value === 'number') {
-      return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value);
-    }
-    if (format === 'date' && typeof value === 'string') {
-      return new Date(value).toLocaleDateString();
-    }
-    if (format === 'badge') {
-      const colors: Record<string, { bg: string; text: string }> = {
-        completed: { bg: '#d1fae5', text: '#065f46' },
-        pending: { bg: '#fef3c7', text: '#92400e' },
-        failed: { bg: '#fee2e2', text: '#991b1b' },
-      };
-      const style = colors[String(value).toLowerCase()] || { bg: '#f3f4f6', text: '#374151' };
-      return (
-        <span
-          style={{
-            padding: '2px 8px',
-            borderRadius: '12px',
-            fontSize: '12px',
-            fontWeight: 500,
-            backgroundColor: style.bg,
-            color: style.text,
-          }}
-        >
-          {String(value)}
-        </span>
-      );
-    }
-    return String(value);
-  };
-
-  return (
-    <div>
-      {title && (
-        <h4 style={{ margin: '0 0 16px', fontSize: '14px', fontWeight: 600 }}>{title}</h4>
-      )}
-      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
-        <thead>
-          <tr>
-            {columns.map((col) => (
-              <th
-                key={col.key}
-                style={{
-                  textAlign: 'left',
-                  padding: '12px 8px',
-                  borderBottom: '1px solid #e5e7eb',
-                  fontSize: '12px',
-                  fontWeight: 600,
-                  color: '#6b7280',
-                  textTransform: 'uppercase',
-                }}
-              >
-                {col.label}
-              </th>
-            ))}
-          </tr>
-        </thead>
-        <tbody>
-          {tableData.map((row, i) => (
-            <tr key={i}>
-              {columns.map((col) => (
-                <td
-                  key={col.key}
-                  style={{
-                    padding: '12px 8px',
-                    borderBottom: '1px solid #e5e7eb',
-                    fontSize: '14px',
-                  }}
-                >
-                  {formatCell(row[col.key], col.format)}
-                </td>
-              ))}
-            </tr>
-          ))}
-        </tbody>
-      </table>
-    </div>
-  );
-}
-
-// Button component
-export function Button({ element, onAction, loading }: ComponentRenderProps) {
-  const { label, variant, action, disabled } = element.props as {
-    label: string;
-    variant?: string | null;
-    action: { name: string };
-    disabled?: boolean | null;
-  };
-
-  const variants: Record<string, React.CSSProperties> = {
-    primary: { backgroundColor: '#6366f1', color: '#fff', border: 'none' },
-    secondary: { backgroundColor: '#fff', color: '#374151', border: '1px solid #d1d5db' },
-    danger: { backgroundColor: '#dc2626', color: '#fff', border: 'none' },
-    ghost: { backgroundColor: 'transparent', color: '#6b7280', border: 'none' },
-  };
-
-  const handleClick = () => {
-    if (!disabled && action && onAction) {
-      onAction(action);
-    }
-  };
-
-  return (
-    <button
-      onClick={handleClick}
-      disabled={!!disabled || loading}
-      style={{
-        padding: '8px 16px',
-        borderRadius: '6px',
-        fontSize: '14px',
-        fontWeight: 500,
-        cursor: disabled ? 'not-allowed' : 'pointer',
-        opacity: disabled ? 0.5 : 1,
-        ...variants[variant || 'primary'],
-      }}
-    >
-      {loading ? 'Loading...' : label}
-    </button>
-  );
-}
-
-// Alert component
-export function Alert({ element }: ComponentRenderProps) {
-  const { message, variant, dismissible } = element.props as {
-    message: string | { path: string };
-    variant?: string | null;
-    dismissible?: boolean | null;
-  };
-
-  const resolvedMessage = useResolvedValue(message);
-
-  const defaultStyle = { bg: '#eff6ff', border: '#bfdbfe', text: '#1e40af' };
-  const variants: Record<string, { bg: string; border: string; text: string }> = {
-    info: defaultStyle,
-    success: { bg: '#d1fae5', border: '#a7f3d0', text: '#065f46' },
-    warning: { bg: '#fef3c7', border: '#fcd34d', text: '#92400e' },
-    error: { bg: '#fee2e2', border: '#fca5a5', text: '#991b1b' },
-  };
-
-  const style = variants[variant || 'info'] ?? defaultStyle;
-
-  return (
-    <div
-      style={{
-        padding: '12px 16px',
-        borderRadius: '8px',
-        backgroundColor: style.bg,
-        border: `1px solid ${style.border}`,
-        color: style.text,
-        fontSize: '14px',
-      }}
-    >
-      {resolvedMessage}
-    </div>
-  );
-}
-
-// Grid component
-export function Grid({ element, children }: ComponentRenderProps) {
-  const { columns, gap } = element.props as {
-    columns?: number | null;
-    gap?: string | null;
-  };
-
-  const gaps: Record<string, string> = {
-    none: '0',
-    sm: '8px',
-    md: '16px',
-    lg: '24px',
-  };
-
-  return (
-    <div
-      style={{
-        display: 'grid',
-        gridTemplateColumns: `repeat(${columns || 2}, 1fr)`,
-        gap: gaps[gap || 'md'],
-      }}
-    >
-      {children}
-    </div>
-  );
-}
-
-// Stack component
-export function Stack({ element, children }: ComponentRenderProps) {
-  const { direction, gap, align } = element.props as {
-    direction?: string | null;
-    gap?: string | null;
-    align?: string | null;
-  };
-
-  const gaps: Record<string, string> = {
-    none: '0',
-    sm: '8px',
-    md: '16px',
-    lg: '24px',
-  };
-
-  const alignments: Record<string, string> = {
-    start: 'flex-start',
-    center: 'center',
-    end: 'flex-end',
-    stretch: 'stretch',
-  };
-
-  return (
-    <div
-      style={{
-        display: 'flex',
-        flexDirection: direction === 'horizontal' ? 'row' : 'column',
-        gap: gaps[gap || 'md'],
-        alignItems: alignments[align || 'stretch'],
-      }}
-    >
-      {children}
-    </div>
-  );
-}
-
-// Divider component
-export function Divider({ element }: ComponentRenderProps) {
-  const { orientation } = element.props as {
-    orientation?: string | null;
-  };
-
-  if (orientation === 'vertical') {
-    return (
-      <div
-        style={{
-          width: '1px',
-          backgroundColor: '#e5e7eb',
-          alignSelf: 'stretch',
-        }}
-      />
-    );
-  }
-
-  return (
-    <hr
-      style={{
-        border: 'none',
-        borderTop: '1px solid #e5e7eb',
-        margin: '16px 0',
-      }}
-    />
-  );
-}
-
-// Badge component
-export function Badge({ element }: ComponentRenderProps) {
-  const { text, variant } = element.props as {
-    text: string | { path: string };
-    variant?: string | null;
-  };
-
-  const resolvedText = useResolvedValue(text);
-
-  const defaultBadgeStyle = { bg: '#f3f4f6', text: '#374151' };
-  const variants: Record<string, { bg: string; text: string }> = {
-    default: defaultBadgeStyle,
-    success: { bg: '#d1fae5', text: '#065f46' },
-    warning: { bg: '#fef3c7', text: '#92400e' },
-    error: { bg: '#fee2e2', text: '#991b1b' },
-    info: { bg: '#dbeafe', text: '#1e40af' },
-  };
-
-  const style = variants[variant || 'default'] ?? defaultBadgeStyle;
-
-  return (
-    <span
-      style={{
-        display: 'inline-block',
-        padding: '2px 8px',
-        borderRadius: '12px',
-        fontSize: '12px',
-        fontWeight: 500,
-        backgroundColor: style.bg,
-        color: style.text,
-      }}
-    >
-      {resolvedText}
-    </span>
-  );
-}
-
-// TextField component
-export function TextField({ element }: ComponentRenderProps) {
-  const { label, valuePath, placeholder, type, checks, validateOn } = element.props as {
-    label: string;
-    valuePath: string;
-    placeholder?: string | null;
-    type?: string | null;
-    checks?: Array<{ fn: string; message: string }> | null;
-    validateOn?: string | null;
-  };
-
-  const { data, set } = useData();
-  const value = getByPath(data, valuePath) as string | undefined;
-  
-  const { errors, validate, touch } = useFieldValidation(valuePath, {
-    checks: checks ?? undefined,
-    validateOn: (validateOn as 'change' | 'blur' | 'submit') ?? 'blur',
-  });
-
-  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-    set(valuePath, e.target.value);
-    if (validateOn === 'change') {
-      validate();
-    }
-  };
-
-  const handleBlur = () => {
-    touch();
-    if (validateOn === 'blur' || !validateOn) {
-      validate();
-    }
-  };
-
-  return (
-    <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
-      <label style={{ fontSize: '14px', fontWeight: 500, color: '#374151' }}>
-        {label}
-      </label>
-      <input
-        type={type || 'text'}
-        value={value ?? ''}
-        onChange={handleChange}
-        onBlur={handleBlur}
-        placeholder={placeholder ?? ''}
-        style={{
-          padding: '8px 12px',
-          borderRadius: '6px',
-          border: errors.length > 0 ? '1px solid #ef4444' : '1px solid #d1d5db',
-          fontSize: '14px',
-          outline: 'none',
-        }}
-      />
-      {errors.map((error, i) => (
-        <span key={i} style={{ fontSize: '12px', color: '#ef4444' }}>
-          {error}
-        </span>
-      ))}
-    </div>
-  );
-}
-
-// Select component
-export function Select({ element }: ComponentRenderProps) {
-  const { label, valuePath, options, placeholder } = element.props as {
-    label: string;
-    valuePath: string;
-    options: Array<{ value: string; label: string }>;
-    placeholder?: string | null;
-  };
-
-  const { data, set } = useData();
-  const value = getByPath(data, valuePath) as string | undefined;
-
-  return (
-    <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
-      <label style={{ fontSize: '14px', fontWeight: 500, color: '#374151' }}>
-        {label}
-      </label>
-      <select
-        value={value ?? ''}
-        onChange={(e) => set(valuePath, e.target.value)}
-        style={{
-          padding: '8px 12px',
-          borderRadius: '6px',
-          border: '1px solid #d1d5db',
-          fontSize: '14px',
-          outline: 'none',
-          backgroundColor: '#fff',
-        }}
-      >
-        {placeholder && <option value="">{placeholder}</option>}
-        {options.map((opt) => (
-          <option key={opt.value} value={opt.value}>
-            {opt.label}
-          </option>
-        ))}
-      </select>
-    </div>
-  );
-}
-
-// DatePicker component (simplified)
-export function DatePicker({ element }: ComponentRenderProps) {
-  const { label, valuePath } = element.props as {
-    label: string;
-    valuePath: string;
-  };
-
-  const { data, set } = useData();
-  const value = getByPath(data, valuePath) as string | undefined;
-
-  return (
-    <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
-      <label style={{ fontSize: '14px', fontWeight: 500, color: '#374151' }}>
-        {label}
-      </label>
-      <input
-        type="date"
-        value={value ?? ''}
-        onChange={(e) => set(valuePath, e.target.value)}
-        style={{
-          padding: '8px 12px',
-          borderRadius: '6px',
-          border: '1px solid #d1d5db',
-          fontSize: '14px',
-          outline: 'none',
-        }}
-      />
-    </div>
-  );
-}
-
-// Empty component
-export function Empty({ element }: ComponentRenderProps) {
-  const { icon, title, description } = element.props as {
-    icon?: string | null;
-    title: string;
-    description?: string | null;
-  };
-
-  return (
-    <div style={{ textAlign: 'center', padding: '40px 20px' }}>
-      {icon && <div style={{ fontSize: '48px', marginBottom: '16px' }}>{icon}</div>}
-      <h3 style={{ margin: '0 0 8px', fontSize: '16px', fontWeight: 600, color: '#111827' }}>
-        {title}
-      </h3>
-      {description && (
-        <p style={{ margin: 0, fontSize: '14px', color: '#6b7280' }}>{description}</p>
-      )}
-    </div>
-  );
-}
-
-// List component
-export function List({ element, children }: ComponentRenderProps) {
-  const { dataPath } = element.props as {
-    dataPath: string;
-    itemKey?: string | null;
-  };
-
-  const { data } = useData();
-  const listData = getByPath(data, dataPath) as Array<unknown> | undefined;
-
-  if (!listData || !Array.isArray(listData)) {
-    return <div style={{ color: '#6b7280' }}>No items</div>;
-  }
-
-  // For now, just render children
-  // In a full implementation, children would be cloned for each item
-  return <div>{children}</div>;
-}
-
-// Export all components as a registry
-export const componentRegistry = {
-  Card,
-  Heading,
-  Text,
-  Metric,
-  Chart,
-  Table,
-  Button,
-  Alert,
-  Grid,
-  Stack,
-  Divider,
-  Badge,
-  TextField,
-  Select,
-  DatePicker,
-  Empty,
-  List,
-};

+ 41 - 0
examples/dashboard/components/ui/alert.tsx

@@ -0,0 +1,41 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+import { useData } from '@json-render/react';
+import { getByPath } from '@json-render/core';
+
+function useResolvedValue<T>(value: T | { path: string } | null | undefined): T | undefined {
+  const { data } = useData();
+  if (value === null || value === undefined) return undefined;
+  if (typeof value === 'object' && 'path' in value) {
+    return getByPath(data, value.path) as T | undefined;
+  }
+  return value as T;
+}
+
+export function Alert({ element }: ComponentRenderProps) {
+  const { message, variant } = element.props as { message: string | { path: string }; variant?: string | null };
+  const resolvedMessage = useResolvedValue(message);
+
+  const colors: Record<string, string> = {
+    info: 'var(--muted)',
+    success: '#22c55e',
+    warning: '#eab308',
+    error: '#ef4444',
+  };
+
+  return (
+    <div
+      style={{
+        padding: '12px 16px',
+        borderRadius: 'var(--radius)',
+        background: 'var(--card)',
+        border: '1px solid var(--border)',
+        fontSize: 14,
+        color: colors[variant || 'info'],
+      }}
+    >
+      {resolvedMessage}
+    </div>
+  );
+}

+ 43 - 0
examples/dashboard/components/ui/badge.tsx

@@ -0,0 +1,43 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+import { useData } from '@json-render/react';
+import { getByPath } from '@json-render/core';
+
+function useResolvedValue<T>(value: T | { path: string } | null | undefined): T | undefined {
+  const { data } = useData();
+  if (value === null || value === undefined) return undefined;
+  if (typeof value === 'object' && 'path' in value) {
+    return getByPath(data, value.path) as T | undefined;
+  }
+  return value as T;
+}
+
+export function Badge({ element }: ComponentRenderProps) {
+  const { text, variant } = element.props as { text: string | { path: string }; variant?: string | null };
+  const resolvedText = useResolvedValue(text);
+
+  const colors: Record<string, string> = {
+    default: 'var(--foreground)',
+    success: '#22c55e',
+    warning: '#eab308',
+    error: '#ef4444',
+    info: 'var(--muted)',
+  };
+
+  return (
+    <span
+      style={{
+        display: 'inline-block',
+        padding: '2px 8px',
+        borderRadius: 12,
+        fontSize: 12,
+        fontWeight: 500,
+        background: 'var(--border)',
+        color: colors[variant || 'default'],
+      }}
+    >
+      {resolvedText}
+    </span>
+  );
+}

+ 37 - 0
examples/dashboard/components/ui/button.tsx

@@ -0,0 +1,37 @@
+'use client';
+
+import React from 'react';
+import { type ComponentRenderProps } from '@json-render/react';
+
+export function Button({ element, onAction, loading }: ComponentRenderProps) {
+  const { label, variant, action, disabled } = element.props as {
+    label: string;
+    variant?: string | null;
+    action: { name: string };
+    disabled?: boolean | null;
+  };
+
+  const variants: Record<string, React.CSSProperties> = {
+    primary: { background: 'var(--foreground)', color: 'var(--background)', border: 'none' },
+    secondary: { background: 'transparent', color: 'var(--foreground)', border: '1px solid var(--border)' },
+    danger: { background: '#dc2626', color: '#fff', border: 'none' },
+    ghost: { background: 'transparent', color: 'var(--muted)', border: 'none' },
+  };
+
+  return (
+    <button
+      onClick={() => !disabled && action && onAction?.(action)}
+      disabled={!!disabled || loading}
+      style={{
+        padding: '8px 16px',
+        borderRadius: 'var(--radius)',
+        fontSize: 14,
+        fontWeight: 500,
+        opacity: disabled ? 0.5 : 1,
+        ...variants[variant || 'primary'],
+      }}
+    >
+      {loading ? 'Loading...' : label}
+    </button>
+  );
+}

+ 25 - 0
examples/dashboard/components/ui/card.tsx

@@ -0,0 +1,25 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+
+export function Card({ element, children }: ComponentRenderProps) {
+  const { title, description, padding } = element.props as {
+    title?: string | null;
+    description?: string | null;
+    padding?: string | null;
+  };
+
+  const paddings: Record<string, string> = { none: '0', sm: '12px', lg: '24px' };
+
+  return (
+    <div style={{ background: 'var(--card)', border: '1px solid var(--border)', borderRadius: 'var(--radius)' }}>
+      {(title || description) && (
+        <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--border)' }}>
+          {title && <h3 style={{ margin: 0, fontSize: 16, fontWeight: 600 }}>{title}</h3>}
+          {description && <p style={{ margin: '4px 0 0', fontSize: 14, color: 'var(--muted)' }}>{description}</p>}
+        </div>
+      )}
+      <div style={{ padding: paddings[padding || ''] || '16px' }}>{children}</div>
+    </div>
+  );
+}

+ 39 - 0
examples/dashboard/components/ui/chart.tsx

@@ -0,0 +1,39 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+import { useData } from '@json-render/react';
+import { getByPath } from '@json-render/core';
+
+export function Chart({ element }: ComponentRenderProps) {
+  const { title, dataPath } = element.props as { title?: string | null; dataPath: string };
+  const { data } = useData();
+  const chartData = getByPath(data, dataPath) as Array<{ label: string; value: number }> | undefined;
+
+  if (!chartData || !Array.isArray(chartData)) {
+    return <div style={{ padding: 20, color: 'var(--muted)' }}>No data</div>;
+  }
+
+  const maxValue = Math.max(...chartData.map((d) => d.value));
+
+  return (
+    <div>
+      {title && <h4 style={{ margin: '0 0 16px', fontSize: 14, fontWeight: 600 }}>{title}</h4>}
+      <div style={{ display: 'flex', gap: 8, alignItems: 'flex-end', height: 120 }}>
+        {chartData.map((d, i) => (
+          <div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
+            <div
+              style={{
+                width: '100%',
+                height: `${(d.value / maxValue) * 100}%`,
+                background: 'var(--foreground)',
+                borderRadius: '4px 4px 0 0',
+                minHeight: 4,
+              }}
+            />
+            <span style={{ fontSize: 12, color: 'var(--muted)' }}>{d.label}</span>
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+}

+ 31 - 0
examples/dashboard/components/ui/date-picker.tsx

@@ -0,0 +1,31 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+import { useData } from '@json-render/react';
+import { getByPath } from '@json-render/core';
+
+export function DatePicker({ element }: ComponentRenderProps) {
+  const { label, valuePath } = element.props as { label: string; valuePath: string };
+  const { data, set } = useData();
+  const value = getByPath(data, valuePath) as string | undefined;
+
+  return (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
+      <label style={{ fontSize: 14, fontWeight: 500 }}>{label}</label>
+      <input
+        type="date"
+        value={value ?? ''}
+        onChange={(e) => set(valuePath, e.target.value)}
+        style={{
+          padding: '8px 12px',
+          borderRadius: 'var(--radius)',
+          border: '1px solid var(--border)',
+          background: 'var(--card)',
+          color: 'var(--foreground)',
+          fontSize: 14,
+          outline: 'none',
+        }}
+      />
+    </div>
+  );
+}

+ 11 - 0
examples/dashboard/components/ui/divider.tsx

@@ -0,0 +1,11 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+
+export function Divider({ element }: ComponentRenderProps) {
+  const { orientation } = element.props as { orientation?: string | null };
+  if (orientation === 'vertical') {
+    return <div style={{ width: 1, background: 'var(--border)', alignSelf: 'stretch' }} />;
+  }
+  return <hr style={{ border: 'none', borderTop: '1px solid var(--border)', margin: '16px 0' }} />;
+}

+ 14 - 0
examples/dashboard/components/ui/empty.tsx

@@ -0,0 +1,14 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+
+export function Empty({ element }: ComponentRenderProps) {
+  const { title, description } = element.props as { title: string; description?: string | null };
+
+  return (
+    <div style={{ textAlign: 'center', padding: '40px 20px' }}>
+      <h3 style={{ margin: '0 0 8px', fontSize: 16, fontWeight: 600 }}>{title}</h3>
+      {description && <p style={{ margin: 0, fontSize: 14, color: 'var(--muted)' }}>{description}</p>}
+    </div>
+  );
+}

+ 14 - 0
examples/dashboard/components/ui/grid.tsx

@@ -0,0 +1,14 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+
+export function Grid({ element, children }: ComponentRenderProps) {
+  const { columns, gap } = element.props as { columns?: number | null; gap?: string | null };
+  const gaps: Record<string, string> = { none: '0', sm: '8px', md: '16px', lg: '24px' };
+
+  return (
+    <div style={{ display: 'grid', gridTemplateColumns: `repeat(${columns || 2}, 1fr)`, gap: gaps[gap || 'md'] }}>
+      {children}
+    </div>
+  );
+}

+ 11 - 0
examples/dashboard/components/ui/heading.tsx

@@ -0,0 +1,11 @@
+'use client';
+
+import React from 'react';
+import { type ComponentRenderProps } from '@json-render/react';
+
+export function Heading({ element }: ComponentRenderProps) {
+  const { text, level } = element.props as { text: string; level?: string | null };
+  const Tag = (level || 'h2') as keyof React.JSX.IntrinsicElements;
+  const sizes: Record<string, string> = { h1: '28px', h2: '24px', h3: '20px', h4: '16px' };
+  return <Tag style={{ margin: '0 0 16px', fontSize: sizes[level || 'h2'], fontWeight: 600 }}>{text}</Tag>;
+}

+ 55 - 0
examples/dashboard/components/ui/index.ts

@@ -0,0 +1,55 @@
+export { Alert } from './alert';
+export { Badge } from './badge';
+export { Button } from './button';
+export { Card } from './card';
+export { Chart } from './chart';
+export { DatePicker } from './date-picker';
+export { Divider } from './divider';
+export { Empty } from './empty';
+export { Grid } from './grid';
+export { Heading } from './heading';
+export { List } from './list';
+export { Metric } from './metric';
+export { Select } from './select';
+export { Stack } from './stack';
+export { Table } from './table';
+export { Text } from './text';
+export { TextField } from './text-field';
+
+import { Alert } from './alert';
+import { Badge } from './badge';
+import { Button } from './button';
+import { Card } from './card';
+import { Chart } from './chart';
+import { DatePicker } from './date-picker';
+import { Divider } from './divider';
+import { Empty } from './empty';
+import { Grid } from './grid';
+import { Heading } from './heading';
+import { List } from './list';
+import { Metric } from './metric';
+import { Select } from './select';
+import { Stack } from './stack';
+import { Table } from './table';
+import { Text } from './text';
+import { TextField } from './text-field';
+
+export const componentRegistry = {
+  Alert,
+  Badge,
+  Button,
+  Card,
+  Chart,
+  DatePicker,
+  Divider,
+  Empty,
+  Grid,
+  Heading,
+  List,
+  Metric,
+  Select,
+  Stack,
+  Table,
+  Text,
+  TextField,
+};

+ 17 - 0
examples/dashboard/components/ui/list.tsx

@@ -0,0 +1,17 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+import { useData } from '@json-render/react';
+import { getByPath } from '@json-render/core';
+
+export function List({ element, children }: ComponentRenderProps) {
+  const { dataPath } = element.props as { dataPath: string };
+  const { data } = useData();
+  const listData = getByPath(data, dataPath) as Array<unknown> | undefined;
+
+  if (!listData || !Array.isArray(listData)) {
+    return <div style={{ color: 'var(--muted)' }}>No items</div>;
+  }
+
+  return <div>{children}</div>;
+}

+ 39 - 0
examples/dashboard/components/ui/metric.tsx

@@ -0,0 +1,39 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+import { useData } from '@json-render/react';
+import { getByPath } from '@json-render/core';
+
+export function Metric({ element }: ComponentRenderProps) {
+  const { label, valuePath, format, trend, trendValue } = element.props as {
+    label: string;
+    valuePath: string;
+    format?: string | null;
+    trend?: string | null;
+    trendValue?: string | null;
+  };
+
+  const { data } = useData();
+  const rawValue = getByPath(data, valuePath);
+
+  let displayValue = String(rawValue ?? '-');
+  if (format === 'currency' && typeof rawValue === 'number') {
+    displayValue = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(rawValue);
+  } else if (format === 'percent' && typeof rawValue === 'number') {
+    displayValue = new Intl.NumberFormat('en-US', { style: 'percent', minimumFractionDigits: 1 }).format(rawValue);
+  } else if (format === 'number' && typeof rawValue === 'number') {
+    displayValue = new Intl.NumberFormat('en-US').format(rawValue);
+  }
+
+  return (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
+      <span style={{ fontSize: 14, color: 'var(--muted)' }}>{label}</span>
+      <span style={{ fontSize: 32, fontWeight: 600 }}>{displayValue}</span>
+      {(trend || trendValue) && (
+        <span style={{ fontSize: 14, color: trend === 'up' ? '#22c55e' : trend === 'down' ? '#ef4444' : 'var(--muted)' }}>
+          {trend === 'up' ? '+' : trend === 'down' ? '-' : ''}{trendValue}
+        </span>
+      )}
+    </div>
+  );
+}

+ 41 - 0
examples/dashboard/components/ui/select.tsx

@@ -0,0 +1,41 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+import { useData } from '@json-render/react';
+import { getByPath } from '@json-render/core';
+
+export function Select({ element }: ComponentRenderProps) {
+  const { label, valuePath, options, placeholder } = element.props as {
+    label: string;
+    valuePath: string;
+    options: Array<{ value: string; label: string }>;
+    placeholder?: string | null;
+  };
+
+  const { data, set } = useData();
+  const value = getByPath(data, valuePath) as string | undefined;
+
+  return (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
+      <label style={{ fontSize: 14, fontWeight: 500 }}>{label}</label>
+      <select
+        value={value ?? ''}
+        onChange={(e) => set(valuePath, e.target.value)}
+        style={{
+          padding: '8px 12px',
+          borderRadius: 'var(--radius)',
+          border: '1px solid var(--border)',
+          background: 'var(--card)',
+          color: 'var(--foreground)',
+          fontSize: 14,
+          outline: 'none',
+        }}
+      >
+        {placeholder && <option value="">{placeholder}</option>}
+        {options.map((opt) => (
+          <option key={opt.value} value={opt.value}>{opt.label}</option>
+        ))}
+      </select>
+    </div>
+  );
+}

+ 22 - 0
examples/dashboard/components/ui/stack.tsx

@@ -0,0 +1,22 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+
+export function Stack({ element, children }: ComponentRenderProps) {
+  const { direction, gap, align } = element.props as { direction?: string | null; gap?: string | null; align?: string | null };
+  const gaps: Record<string, string> = { none: '0', sm: '8px', md: '16px', lg: '24px' };
+  const alignments: Record<string, string> = { start: 'flex-start', center: 'center', end: 'flex-end', stretch: 'stretch' };
+
+  return (
+    <div
+      style={{
+        display: 'flex',
+        flexDirection: direction === 'horizontal' ? 'row' : 'column',
+        gap: gaps[gap || 'md'],
+        alignItems: alignments[align || 'stretch'],
+      }}
+    >
+      {children}
+    </div>
+  );
+}

+ 94 - 0
examples/dashboard/components/ui/table.tsx

@@ -0,0 +1,94 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+import { useData } from '@json-render/react';
+import { getByPath } from '@json-render/core';
+
+export function Table({ element }: ComponentRenderProps) {
+  const { title, dataPath, columns } = element.props as {
+    title?: string | null;
+    dataPath: string;
+    columns: Array<{ key: string; label: string; format?: string | null }>;
+  };
+
+  const { data } = useData();
+  const tableData = getByPath(data, dataPath) as Array<Record<string, unknown>> | undefined;
+
+  if (!tableData || !Array.isArray(tableData)) {
+    return <div style={{ padding: 20, color: 'var(--muted)' }}>No data</div>;
+  }
+
+  const formatCell = (value: unknown, format?: string | null) => {
+    if (value === null || value === undefined) return '-';
+    if (format === 'currency' && typeof value === 'number') {
+      return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value);
+    }
+    if (format === 'date' && typeof value === 'string') {
+      return new Date(value).toLocaleDateString();
+    }
+    if (format === 'badge') {
+      return (
+        <span
+          style={{
+            padding: '2px 8px',
+            borderRadius: 12,
+            fontSize: 12,
+            fontWeight: 500,
+            background: 'var(--border)',
+            color: 'var(--foreground)',
+          }}
+        >
+          {String(value)}
+        </span>
+      );
+    }
+    return String(value);
+  };
+
+  return (
+    <div>
+      {title && <h4 style={{ margin: '0 0 16px', fontSize: 14, fontWeight: 600 }}>{title}</h4>}
+      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
+        <thead>
+          <tr>
+            {columns.map((col) => (
+              <th
+                key={col.key}
+                style={{
+                  textAlign: 'left',
+                  padding: '12px 8px',
+                  borderBottom: '1px solid var(--border)',
+                  fontSize: 12,
+                  fontWeight: 500,
+                  color: 'var(--muted)',
+                  textTransform: 'uppercase',
+                  letterSpacing: '0.05em',
+                }}
+              >
+                {col.label}
+              </th>
+            ))}
+          </tr>
+        </thead>
+        <tbody>
+          {tableData.map((row, i) => (
+            <tr key={i}>
+              {columns.map((col) => (
+                <td
+                  key={col.key}
+                  style={{
+                    padding: '12px 8px',
+                    borderBottom: '1px solid var(--border)',
+                    fontSize: 14,
+                  }}
+                >
+                  {formatCell(row[col.key], col.format)}
+                </td>
+              ))}
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    </div>
+  );
+}

+ 54 - 0
examples/dashboard/components/ui/text-field.tsx

@@ -0,0 +1,54 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+import { useData, useFieldValidation } from '@json-render/react';
+import { getByPath } from '@json-render/core';
+
+export function TextField({ element }: ComponentRenderProps) {
+  const { label, valuePath, placeholder, type, checks, validateOn } = element.props as {
+    label: string;
+    valuePath: string;
+    placeholder?: string | null;
+    type?: string | null;
+    checks?: Array<{ fn: string; message: string }> | null;
+    validateOn?: string | null;
+  };
+
+  const { data, set } = useData();
+  const value = getByPath(data, valuePath) as string | undefined;
+  const { errors, validate, touch } = useFieldValidation(valuePath, {
+    checks: checks ?? undefined,
+    validateOn: (validateOn as 'change' | 'blur' | 'submit') ?? 'blur',
+  });
+
+  return (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
+      <label style={{ fontSize: 14, fontWeight: 500 }}>{label}</label>
+      <input
+        type={type || 'text'}
+        value={value ?? ''}
+        onChange={(e) => {
+          set(valuePath, e.target.value);
+          if (validateOn === 'change') validate();
+        }}
+        onBlur={() => {
+          touch();
+          if (validateOn === 'blur' || !validateOn) validate();
+        }}
+        placeholder={placeholder ?? ''}
+        style={{
+          padding: '8px 12px',
+          borderRadius: 'var(--radius)',
+          border: errors.length > 0 ? '1px solid #ef4444' : '1px solid var(--border)',
+          background: 'var(--card)',
+          color: 'var(--foreground)',
+          fontSize: 14,
+          outline: 'none',
+        }}
+      />
+      {errors.map((error, i) => (
+        <span key={i} style={{ fontSize: 12, color: '#ef4444' }}>{error}</span>
+      ))}
+    </div>
+  );
+}

+ 15 - 0
examples/dashboard/components/ui/text.tsx

@@ -0,0 +1,15 @@
+'use client';
+
+import { type ComponentRenderProps } from '@json-render/react';
+
+export function Text({ element }: ComponentRenderProps) {
+  const { content, variant } = element.props as { content: string; variant?: string | null };
+  const colors: Record<string, string> = {
+    default: 'var(--foreground)',
+    muted: 'var(--muted)',
+    success: '#22c55e',
+    warning: '#eab308',
+    error: '#ef4444',
+  };
+  return <p style={{ margin: 0, color: colors[variant || 'default'] }}>{content}</p>;
+}