Chris Tate 5 miesięcy temu
rodzic
commit
a6bf3ef3da
68 zmienionych plików z 1761 dodań i 957 usunięć
  1. 3 3
      apps/web/app/api/generate/route.ts
  2. 17 5
      apps/web/app/docs/actions/page.tsx
  3. 13 3
      apps/web/app/docs/ai-sdk/page.tsx
  4. 4 2
      apps/web/app/docs/api/core/page.tsx
  5. 25 6
      apps/web/app/docs/catalog/page.tsx
  6. 8 1
      apps/web/app/docs/components/page.tsx
  7. 12 4
      apps/web/app/docs/data-binding/page.tsx
  8. 11 4
      apps/web/app/docs/installation/page.tsx
  9. 1 3
      apps/web/app/docs/layout.tsx
  10. 27 12
      apps/web/app/docs/page.tsx
  11. 48 7
      apps/web/app/docs/quick-start/page.tsx
  12. 21 7
      apps/web/app/docs/streaming/page.tsx
  13. 53 15
      apps/web/app/docs/validation/page.tsx
  14. 14 3
      apps/web/app/docs/visibility/page.tsx
  15. 54 22
      apps/web/app/page.tsx
  16. 30 12
      apps/web/components/code-block.tsx
  17. 24 5
      apps/web/components/code.tsx
  18. 20 2
      apps/web/components/copy-button.tsx
  19. 178 29
      apps/web/components/demo.tsx
  20. 3 1
      apps/web/components/demo/alert.tsx
  21. 5 4
      apps/web/components/demo/bar-graph.tsx
  22. 6 6
      apps/web/components/demo/line-graph.tsx
  23. 2 2
      apps/web/components/demo/utils.ts
  24. 4 1
      apps/web/components/footer.tsx
  25. 4 1
      apps/web/components/header.tsx
  26. 9 9
      apps/web/components/ui/badge.tsx
  27. 10 10
      apps/web/components/ui/button.tsx
  28. 13 13
      apps/web/components/ui/card.tsx
  29. 7 7
      apps/web/components/ui/sonner.tsx
  30. 11 11
      apps/web/components/ui/tabs.tsx
  31. 3 3
      apps/web/lib/utils.ts
  32. 4 4
      examples/dashboard/app/api/generate/route.ts
  33. 4 4
      examples/dashboard/app/layout.tsx
  34. 111 67
      examples/dashboard/app/page.tsx
  35. 21 16
      examples/dashboard/components/ui/alert.tsx
  36. 21 16
      examples/dashboard/components/ui/badge.tsx
  37. 19 11
      examples/dashboard/components/ui/button.tsx
  38. 35 8
      examples/dashboard/components/ui/card.tsx
  39. 36 14
      examples/dashboard/components/ui/chart.tsx
  40. 16 13
      examples/dashboard/components/ui/date-picker.tsx
  41. 17 5
      examples/dashboard/components/ui/divider.tsx
  42. 15 6
      examples/dashboard/components/ui/empty.tsx
  43. 19 5
      examples/dashboard/components/ui/grid.tsx
  44. 25 7
      examples/dashboard/components/ui/heading.tsx
  45. 34 34
      examples/dashboard/components/ui/index.ts
  46. 5 5
      examples/dashboard/components/ui/list.tsx
  47. 32 15
      examples/dashboard/components/ui/metric.tsx
  48. 15 13
      examples/dashboard/components/ui/select.tsx
  49. 23 9
      examples/dashboard/components/ui/stack.tsx
  50. 33 24
      examples/dashboard/components/ui/table.tsx
  51. 30 26
      examples/dashboard/components/ui/text-field.tsx
  52. 14 9
      examples/dashboard/components/ui/text.tsx
  53. 47 43
      examples/dashboard/lib/catalog.ts
  54. 39 29
      packages/core/src/actions.ts
  55. 83 45
      packages/core/src/catalog.ts
  56. 10 15
      packages/core/src/index.ts
  57. 36 31
      packages/core/src/types.ts
  58. 59 55
      packages/core/src/validation.ts
  59. 119 66
      packages/core/src/visibility.ts
  60. 4 4
      packages/core/tsup.config.ts
  61. 63 46
      packages/react/src/contexts/actions.tsx
  62. 10 13
      packages/react/src/contexts/data.tsx
  63. 31 16
      packages/react/src/contexts/validation.tsx
  64. 16 9
      packages/react/src/contexts/visibility.tsx
  65. 31 27
      packages/react/src/hooks.ts
  66. 6 6
      packages/react/src/index.ts
  67. 34 24
      packages/react/src/renderer.tsx
  68. 4 4
      packages/react/tsup.config.ts

+ 3 - 3
apps/web/app/api/generate/route.ts

@@ -1,4 +1,4 @@
-import { streamText } from 'ai';
+import { streamText } from "ai";
 
 export const maxDuration = 30;
 
@@ -82,10 +82,10 @@ const MAX_PROMPT_LENGTH = 140;
 export async function POST(req: Request) {
   const { prompt } = await req.json();
 
-  const sanitizedPrompt = String(prompt || '').slice(0, MAX_PROMPT_LENGTH);
+  const sanitizedPrompt = String(prompt || "").slice(0, MAX_PROMPT_LENGTH);
 
   const result = streamText({
-    model: 'anthropic/claude-opus-4.5',
+    model: "anthropic/claude-opus-4.5",
     system: SYSTEM_PROMPT,
     prompt: sanitizedPrompt,
     temperature: 0.7,

+ 17 - 5
apps/web/app/docs/actions/page.tsx

@@ -15,8 +15,9 @@ export default function ActionsPage() {
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Why Named Actions?</h2>
       <p className="text-sm text-muted-foreground mb-4">
-        Instead of AI generating arbitrary code, it declares <em>intent</em> by name. 
-        Your application provides the implementation. This is a core guardrail.
+        Instead of AI generating arbitrary code, it declares <em>intent</em> by
+        name. Your application provides the implementation. This is a core
+        guardrail.
       </p>
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Defining Actions</h2>
@@ -81,7 +82,9 @@ function App() {
   );
 }`}</Code>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">Using Actions in Components</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        Using Actions in Components
+      </h2>
       <Code lang="tsx">{`const Button = ({ element, onAction }) => (
   <button onClick={() => onAction(element.props.action, {})}>
     {element.props.label}
@@ -101,7 +104,9 @@ function SubmitButton() {
   );
 }`}</Code>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">Actions with Confirmation</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        Actions with Confirmation
+      </h2>
       <p className="text-sm text-muted-foreground mb-4">
         AI can declare actions that require user confirmation:
       </p>
@@ -144,7 +149,14 @@ function SubmitButton() {
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
       <p className="text-sm text-muted-foreground">
-        Learn about <Link href="/docs/visibility" className="text-foreground hover:underline">conditional visibility</Link>.
+        Learn about{" "}
+        <Link
+          href="/docs/visibility"
+          className="text-foreground hover:underline"
+        >
+          conditional visibility
+        </Link>
+        .
       </p>
     </article>
   );

+ 13 - 3
apps/web/app/docs/ai-sdk/page.tsx

@@ -77,7 +77,8 @@ function GenerativeUI() {
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Prompt Engineering</h2>
       <p className="text-sm text-muted-foreground mb-4">
-        The <code className="text-foreground">generateCatalogPrompt</code> function creates an optimized prompt that:
+        The <code className="text-foreground">generateCatalogPrompt</code>{" "}
+        function creates an optimized prompt that:
       </p>
       <ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mb-4">
         <li>Lists all available components and their props</li>
@@ -86,7 +87,9 @@ function GenerativeUI() {
         <li>Includes examples for better generation</li>
       </ul>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">Custom System Prompts</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        Custom System Prompts
+      </h2>
       <Code lang="typescript">{`const basePrompt = generateCatalogPrompt(catalog);
 
 const customPrompt = \`
@@ -100,7 +103,14 @@ Additional instructions:
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
       <p className="text-sm text-muted-foreground">
-        Learn about <Link href="/docs/streaming" className="text-foreground hover:underline">progressive streaming</Link>.
+        Learn about{" "}
+        <Link
+          href="/docs/streaming"
+          className="text-foreground hover:underline"
+        >
+          progressive streaming
+        </Link>
+        .
       </p>
     </article>
   );

+ 4 - 2
apps/web/app/docs/api/core/page.tsx

@@ -35,7 +35,9 @@ interface ActionDefinition {
   description?: string;
 }`}</Code>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">generateCatalogPrompt</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        generateCatalogPrompt
+      </h2>
       <p className="text-sm text-muted-foreground mb-4">
         Generates a system prompt for AI models.
       </p>
@@ -64,7 +66,7 @@ type VisibilityCondition =
   | { lte: [DynamicValue, DynamicValue] };`}</Code>
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Types</h2>
-      
+
       <h3 className="text-lg font-semibold mt-8 mb-4">UIElement</h3>
       <Code lang="typescript">{`interface UIElement {
   key: string;

+ 25 - 6
apps/web/app/docs/catalog/page.tsx

@@ -18,9 +18,18 @@ export default function CatalogPage() {
         A catalog is a schema that defines:
       </p>
       <ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mb-4">
-        <li><strong className="text-foreground">Components</strong> — UI elements AI can create</li>
-        <li><strong className="text-foreground">Actions</strong> — Operations AI can trigger</li>
-        <li><strong className="text-foreground">Validation Functions</strong> — Custom validators for form inputs</li>
+        <li>
+          <strong className="text-foreground">Components</strong> — UI elements
+          AI can create
+        </li>
+        <li>
+          <strong className="text-foreground">Actions</strong> — Operations AI
+          can trigger
+        </li>
+        <li>
+          <strong className="text-foreground">Validation Functions</strong> —
+          Custom validators for form inputs
+        </li>
       </ul>
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Creating a Catalog</h2>
@@ -83,9 +92,12 @@ const catalog = createCatalog({
   description?: string,     // Help AI understand when to use it
 }`}</Code>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">Generating AI Prompts</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        Generating AI Prompts
+      </h2>
       <p className="text-sm text-muted-foreground mb-4">
-        Use <code className="text-foreground">generateCatalogPrompt</code> to create a system prompt for AI:
+        Use <code className="text-foreground">generateCatalogPrompt</code> to
+        create a system prompt for AI:
       </p>
       <Code lang="typescript">{`import { generateCatalogPrompt } from '@json-render/core';
 
@@ -94,7 +106,14 @@ const systemPrompt = generateCatalogPrompt(catalog);
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
       <p className="text-sm text-muted-foreground">
-        Learn how to <Link href="/docs/components" className="text-foreground hover:underline">register React components</Link> for your catalog.
+        Learn how to{" "}
+        <Link
+          href="/docs/components"
+          className="text-foreground hover:underline"
+        >
+          register React components
+        </Link>{" "}
+        for your catalog.
       </p>
     </article>
   );

+ 8 - 1
apps/web/app/docs/components/page.tsx

@@ -97,7 +97,14 @@ function App() {
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
       <p className="text-sm text-muted-foreground">
-        Learn about <Link href="/docs/data-binding" className="text-foreground hover:underline">data binding</Link> for dynamic values.
+        Learn about{" "}
+        <Link
+          href="/docs/data-binding"
+          className="text-foreground hover:underline"
+        >
+          data binding
+        </Link>{" "}
+        for dynamic values.
       </p>
     </article>
   );

+ 12 - 4
apps/web/app/docs/data-binding/page.tsx

@@ -55,7 +55,8 @@ function App() {
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Reading Data</h2>
       <p className="text-sm text-muted-foreground mb-4">
-        Use <code className="text-foreground">useDataValue</code> for read-only access:
+        Use <code className="text-foreground">useDataValue</code> for read-only
+        access:
       </p>
       <Code lang="tsx">{`import { useDataValue } from '@json-render/react';
 
@@ -66,7 +67,8 @@ function UserGreeting() {
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Two-Way Binding</h2>
       <p className="text-sm text-muted-foreground mb-4">
-        Use <code className="text-foreground">useDataBinding</code> for read-write access:
+        Use <code className="text-foreground">useDataBinding</code> for
+        read-write access:
       </p>
       <Code lang="tsx">{`import { useDataBinding } from '@json-render/react';
 
@@ -82,7 +84,9 @@ function EmailInput() {
   );
 }`}</Code>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">Using the Data Context</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        Using the Data Context
+      </h2>
       <p className="text-sm text-muted-foreground mb-4">
         Access the full data context for advanced use cases:
       </p>
@@ -118,7 +122,11 @@ function DataDebugger() {
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
       <p className="text-sm text-muted-foreground">
-        Learn about <Link href="/docs/actions" className="text-foreground hover:underline">actions</Link> for user interactions.
+        Learn about{" "}
+        <Link href="/docs/actions" className="text-foreground hover:underline">
+          actions
+        </Link>{" "}
+        for user interactions.
       </p>
     </article>
   );

+ 11 - 4
apps/web/app/docs/installation/page.tsx

@@ -17,7 +17,9 @@ export default function InstallationPage() {
       <h2 className="text-xl font-semibold mt-12 mb-4">Install packages</h2>
       <Code lang="bash">npm install @json-render/core @json-render/react</Code>
 
-      <p className="text-sm text-muted-foreground mb-4">Or with other package managers:</p>
+      <p className="text-sm text-muted-foreground mb-4">
+        Or with other package managers:
+      </p>
       <Code lang="bash">{`# pnpm
 pnpm add @json-render/core @json-render/react
 
@@ -32,14 +34,19 @@ bun add @json-render/core @json-render/react`}</Code>
         json-render requires the following peer dependencies:
       </p>
       <ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mb-4">
-        <li><code className="text-foreground">react</code> ^19.0.0</li>
-        <li><code className="text-foreground">zod</code> ^3.0.0</li>
+        <li>
+          <code className="text-foreground">react</code> ^19.0.0
+        </li>
+        <li>
+          <code className="text-foreground">zod</code> ^3.0.0
+        </li>
       </ul>
       <Code lang="bash">npm install react zod</Code>
 
       <h2 className="text-xl font-semibold mt-12 mb-4">For AI Integration</h2>
       <p className="text-sm text-muted-foreground mb-4">
-        To use json-render with AI models, you&apos;ll also need the Vercel AI SDK:
+        To use json-render with AI models, you&apos;ll also need the Vercel AI
+        SDK:
       </p>
       <Code lang="bash">npm install ai</Code>
 

+ 1 - 3
apps/web/app/docs/layout.tsx

@@ -69,9 +69,7 @@ export default function DocsLayout({
       </aside>
 
       {/* Content */}
-      <div className="flex-1 min-w-0 max-w-2xl">
-        {children}
-      </div>
+      <div className="flex-1 min-w-0 max-w-2xl">{children}</div>
     </div>
   );
 }

+ 27 - 12
apps/web/app/docs/page.tsx

@@ -7,15 +7,17 @@ export default function DocsPage() {
     <article>
       <h1 className="text-3xl font-bold mb-4">Introduction</h1>
       <p className="text-muted-foreground mb-8">
-        Predictable. Guardrailed. Fast. Let users generate dashboards, widgets, apps, and data visualizations from prompts.
+        Predictable. Guardrailed. Fast. Let users generate dashboards, widgets,
+        apps, and data visualizations from prompts.
       </p>
 
       <h2 className="text-xl font-semibold mt-12 mb-4">What is json-render?</h2>
       <p className="text-sm text-muted-foreground mb-4 leading-relaxed">
-        json-render lets end users generate UI from natural language prompts — safely constrained to 
-        components you define. You set the guardrails: what components exist, what props they take, 
-        what actions are available. AI generates JSON that matches your schema, and your components 
-        render it natively.
+        json-render lets end users generate UI from natural language prompts —
+        safely constrained to components you define. You set the guardrails:
+        what components exist, what props they take, what actions are available.
+        AI generates JSON that matches your schema, and your components render
+        it natively.
       </p>
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Why json-render?</h2>
@@ -23,29 +25,42 @@ export default function DocsPage() {
         <div>
           <h3 className="font-medium mb-1">Guardrailed</h3>
           <p className="text-sm text-muted-foreground">
-            AI can only use components in your catalog. No arbitrary code generation.
+            AI can only use components in your catalog. No arbitrary code
+            generation.
           </p>
         </div>
         <div>
           <h3 className="font-medium mb-1">Predictable</h3>
           <p className="text-sm text-muted-foreground">
-            JSON output matches your schema, every time. Actions are declared by name, you control what they do.
+            JSON output matches your schema, every time. Actions are declared by
+            name, you control what they do.
           </p>
         </div>
         <div>
           <h3 className="font-medium mb-1">Fast</h3>
           <p className="text-sm text-muted-foreground">
-            Stream and render progressively as the model responds. No waiting for completion.
+            Stream and render progressively as the model responds. No waiting
+            for completion.
           </p>
         </div>
       </div>
 
       <h2 className="text-xl font-semibold mt-12 mb-4">How it works</h2>
       <ol className="list-decimal list-inside space-y-2 text-sm text-muted-foreground">
-        <li>Define the guardrails — what components, actions, and data bindings AI can use</li>
-        <li>Users prompt — end users describe what they want in natural language</li>
-        <li>AI generates JSON — output is always predictable, constrained to your catalog</li>
-        <li>Render fast — stream and render progressively as the model responds</li>
+        <li>
+          Define the guardrails — what components, actions, and data bindings AI
+          can use
+        </li>
+        <li>
+          Users prompt — end users describe what they want in natural language
+        </li>
+        <li>
+          AI generates JSON — output is always predictable, constrained to your
+          catalog
+        </li>
+        <li>
+          Render fast — stream and render progressively as the model responds
+        </li>
       </ol>
     </article>
   );

+ 48 - 7
apps/web/app/docs/quick-start/page.tsx

@@ -14,7 +14,9 @@ export default function QuickStartPage() {
         Get up and running with json-render in 5 minutes.
       </p>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">1. Define your catalog</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        1. Define your catalog
+      </h2>
       <p className="text-sm text-muted-foreground mb-4">
         Create a catalog that defines what components AI can use:
       </p>
@@ -53,7 +55,9 @@ export const catalog = createCatalog({
   },
 });`}</Code>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">2. Create your components</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        2. Create your components
+      </h2>
       <p className="text-sm text-muted-foreground mb-4">
         Register React components that render each catalog type:
       </p>
@@ -81,7 +85,9 @@ export const registry = {
   ),
 };`}</Code>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">3. Create an API route</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        3. Create an API route
+      </h2>
       <p className="text-sm text-muted-foreground mb-4">
         Set up a streaming API route for AI generation:
       </p>
@@ -155,10 +161,45 @@ export default function Page() {
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Next steps</h2>
       <ul className="list-disc list-inside text-sm text-muted-foreground space-y-2">
-        <li>Learn about <Link href="/docs/catalog" className="text-foreground hover:underline">catalogs</Link> in depth</li>
-        <li>Explore <Link href="/docs/data-binding" className="text-foreground hover:underline">data binding</Link> for dynamic values</li>
-        <li>Add <Link href="/docs/actions" className="text-foreground hover:underline">actions</Link> for interactivity</li>
-        <li>Implement <Link href="/docs/visibility" className="text-foreground hover:underline">conditional visibility</Link></li>
+        <li>
+          Learn about{" "}
+          <Link
+            href="/docs/catalog"
+            className="text-foreground hover:underline"
+          >
+            catalogs
+          </Link>{" "}
+          in depth
+        </li>
+        <li>
+          Explore{" "}
+          <Link
+            href="/docs/data-binding"
+            className="text-foreground hover:underline"
+          >
+            data binding
+          </Link>{" "}
+          for dynamic values
+        </li>
+        <li>
+          Add{" "}
+          <Link
+            href="/docs/actions"
+            className="text-foreground hover:underline"
+          >
+            actions
+          </Link>{" "}
+          for interactivity
+        </li>
+        <li>
+          Implement{" "}
+          <Link
+            href="/docs/visibility"
+            className="text-foreground hover:underline"
+          >
+            conditional visibility
+          </Link>
+        </li>
       </ul>
 
       <div className="flex gap-3 mt-12">

+ 21 - 7
apps/web/app/docs/streaming/page.tsx

@@ -14,8 +14,8 @@ export default function StreamingPage() {
 
       <h2 className="text-xl font-semibold mt-12 mb-4">How Streaming Works</h2>
       <p className="text-sm text-muted-foreground mb-4">
-        json-render uses JSONL (JSON Lines) streaming. As AI generates, 
-        each line represents a patch operation:
+        json-render uses JSONL (JSON Lines) streaming. As AI generates, each
+        line represents a patch operation:
       </p>
       <Code lang="json">{`{"op":"set","path":"/root","value":{"key":"root","type":"Card","props":{"title":"Dashboard"}}}
 {"op":"add","path":"/root/children","value":{"key":"metric-1","type":"Metric","props":{"label":"Revenue"}}}
@@ -44,10 +44,22 @@ function App() {
         Supported operations:
       </p>
       <ul className="list-disc list-inside text-sm text-muted-foreground space-y-2 mb-4">
-        <li><code className="text-foreground">set</code> — Set the value at a path (creates if needed)</li>
-        <li><code className="text-foreground">add</code> — Add to an array at a path</li>
-        <li><code className="text-foreground">replace</code> — Replace value at a path</li>
-        <li><code className="text-foreground">remove</code> — Remove value at a path</li>
+        <li>
+          <code className="text-foreground">set</code> — Set the value at a path
+          (creates if needed)
+        </li>
+        <li>
+          <code className="text-foreground">add</code> — Add to an array at a
+          path
+        </li>
+        <li>
+          <code className="text-foreground">replace</code> — Replace value at a
+          path
+        </li>
+        <li>
+          <code className="text-foreground">remove</code> — Remove value at a
+          path
+        </li>
       </ul>
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Path Format</h2>
@@ -82,7 +94,9 @@ function App() {
   });
 }`}</Code>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">Progressive Rendering</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        Progressive Rendering
+      </h2>
       <p className="text-sm text-muted-foreground mb-4">
         The Renderer automatically updates as the tree changes:
       </p>

+ 53 - 15
apps/web/app/docs/validation/page.tsx

@@ -18,16 +18,36 @@ export default function ValidationPage() {
         json-render includes common validation functions:
       </p>
       <ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mb-4">
-        <li><code className="text-foreground">required</code> — Value must be non-empty</li>
-        <li><code className="text-foreground">email</code> — Valid email format</li>
-        <li><code className="text-foreground">minLength</code> — Minimum string length</li>
-        <li><code className="text-foreground">maxLength</code> — Maximum string length</li>
-        <li><code className="text-foreground">pattern</code> — Match a regex pattern</li>
-        <li><code className="text-foreground">min</code> — Minimum numeric value</li>
-        <li><code className="text-foreground">max</code> — Maximum numeric value</li>
+        <li>
+          <code className="text-foreground">required</code> — Value must be
+          non-empty
+        </li>
+        <li>
+          <code className="text-foreground">email</code> — Valid email format
+        </li>
+        <li>
+          <code className="text-foreground">minLength</code> — Minimum string
+          length
+        </li>
+        <li>
+          <code className="text-foreground">maxLength</code> — Maximum string
+          length
+        </li>
+        <li>
+          <code className="text-foreground">pattern</code> — Match a regex
+          pattern
+        </li>
+        <li>
+          <code className="text-foreground">min</code> — Minimum numeric value
+        </li>
+        <li>
+          <code className="text-foreground">max</code> — Maximum numeric value
+        </li>
       </ul>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">Using Validation in JSON</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        Using Validation in JSON
+      </h2>
       <Code lang="json">{`{
   "type": "TextField",
   "props": {
@@ -41,7 +61,9 @@ export default function ValidationPage() {
   }
 }`}</Code>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">Validation with Parameters</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        Validation with Parameters
+      </h2>
       <Code lang="json">{`{
   "type": "TextField",
   "props": {
@@ -63,7 +85,9 @@ export default function ValidationPage() {
   }
 }`}</Code>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">Custom Validation Functions</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        Custom Validation Functions
+      </h2>
       <p className="text-sm text-muted-foreground mb-4">
         Define custom validators in your catalog:
       </p>
@@ -130,17 +154,31 @@ function TextField({ element }) {
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Validation Timing</h2>
       <p className="text-sm text-muted-foreground mb-4">
-        Control when validation runs with <code className="text-foreground">validateOn</code>:
+        Control when validation runs with{" "}
+        <code className="text-foreground">validateOn</code>:
       </p>
       <ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
-        <li><code className="text-foreground">change</code> — Validate on every input change</li>
-        <li><code className="text-foreground">blur</code> — Validate when field loses focus</li>
-        <li><code className="text-foreground">submit</code> — Validate only on form submission</li>
+        <li>
+          <code className="text-foreground">change</code> — Validate on every
+          input change
+        </li>
+        <li>
+          <code className="text-foreground">blur</code> — Validate when field
+          loses focus
+        </li>
+        <li>
+          <code className="text-foreground">submit</code> — Validate only on
+          form submission
+        </li>
       </ul>
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
       <p className="text-sm text-muted-foreground">
-        Learn about <Link href="/docs/ai-sdk" className="text-foreground hover:underline">AI SDK integration</Link>.
+        Learn about{" "}
+        <Link href="/docs/ai-sdk" className="text-foreground hover:underline">
+          AI SDK integration
+        </Link>
+        .
       </p>
     </article>
   );

+ 14 - 3
apps/web/app/docs/visibility/page.tsx

@@ -29,7 +29,9 @@ function App() {
   );
 }`}</Code>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">Path-Based Visibility</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        Path-Based Visibility
+      </h2>
       <p className="text-sm text-muted-foreground mb-4">
         Show/hide based on data values:
       </p>
@@ -41,7 +43,9 @@ function App() {
 
 // Visible when /form/hasErrors is truthy`}</Code>
 
-      <h2 className="text-xl font-semibold mt-12 mb-4">Auth-Based Visibility</h2>
+      <h2 className="text-xl font-semibold mt-12 mb-4">
+        Auth-Based Visibility
+      </h2>
       <p className="text-sm text-muted-foreground mb-4">
         Show/hide based on authentication state:
       </p>
@@ -129,7 +133,14 @@ function ConditionalContent({ element, children }) {
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
       <p className="text-sm text-muted-foreground">
-        Learn about <Link href="/docs/validation" className="text-foreground hover:underline">form validation</Link>.
+        Learn about{" "}
+        <Link
+          href="/docs/validation"
+          className="text-foreground hover:underline"
+        >
+          form validation
+        </Link>
+        .
       </p>
     </article>
   );

+ 54 - 22
apps/web/app/page.tsx

@@ -13,13 +13,16 @@ export default function Home() {
           Predictable. Guardrailed. Fast.
         </h1>
         <p className="text-lg text-muted-foreground max-w-2xl mx-auto mb-12 leading-relaxed">
-          Let users generate dashboards, widgets, apps, and data visualizations from prompts — safely constrained to components you define.
+          Let users generate dashboards, widgets, apps, and data visualizations
+          from prompts — safely constrained to components you define.
         </p>
 
         <Demo />
 
         <div className="flex items-center justify-center gap-2 border border-border rounded px-4 py-3 mt-12 mx-auto w-fit">
-          <code className="text-sm bg-transparent">npm install @json-render/core @json-render/react</code>
+          <code className="text-sm bg-transparent">
+            npm install @json-render/core @json-render/react
+          </code>
           <CopyButton text="npm install @json-render/core @json-render/react" />
         </div>
 
@@ -33,11 +36,7 @@ export default function Home() {
               target="_blank"
               rel="noopener noreferrer"
             >
-              <svg
-                viewBox="0 0 24 24"
-                fill="currentColor"
-                className="w-5 h-5"
-              >
+              <svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
                 <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
               </svg>
               GitHub
@@ -51,24 +50,35 @@ export default function Home() {
         <div className="max-w-5xl mx-auto px-6 py-24">
           <div className="grid md:grid-cols-3 gap-12">
             <div>
-              <div className="text-xs text-muted-foreground font-mono mb-3">01</div>
-              <h3 className="text-lg font-semibold mb-2">Define Your Catalog</h3>
+              <div className="text-xs text-muted-foreground font-mono mb-3">
+                01
+              </div>
+              <h3 className="text-lg font-semibold mb-2">
+                Define Your Catalog
+              </h3>
               <p className="text-sm text-muted-foreground leading-relaxed">
-                Set the guardrails. Define which components, actions, and data bindings AI can use.
+                Set the guardrails. Define which components, actions, and data
+                bindings AI can use.
               </p>
             </div>
             <div>
-              <div className="text-xs text-muted-foreground font-mono mb-3">02</div>
+              <div className="text-xs text-muted-foreground font-mono mb-3">
+                02
+              </div>
               <h3 className="text-lg font-semibold mb-2">Users Prompt</h3>
               <p className="text-sm text-muted-foreground leading-relaxed">
-                End users describe what they want. AI generates JSON constrained to your catalog.
+                End users describe what they want. AI generates JSON constrained
+                to your catalog.
               </p>
             </div>
             <div>
-              <div className="text-xs text-muted-foreground font-mono mb-3">03</div>
+              <div className="text-xs text-muted-foreground font-mono mb-3">
+                03
+              </div>
               <h3 className="text-lg font-semibold mb-2">Render Instantly</h3>
               <p className="text-sm text-muted-foreground leading-relaxed">
-                Stream the response. Your components render progressively as JSON arrives.
+                Stream the response. Your components render progressively as
+                JSON arrives.
               </p>
             </div>
           </div>
@@ -80,7 +90,9 @@ export default function Home() {
         <div className="max-w-5xl mx-auto px-6 py-24">
           <div className="grid lg:grid-cols-2 gap-12">
             <div>
-              <h2 className="text-2xl font-semibold mb-4">Define your catalog</h2>
+              <h2 className="text-2xl font-semibold mb-4">
+                Define your catalog
+              </h2>
               <p className="text-muted-foreground mb-6">
                 Components, actions, and validation functions.
               </p>
@@ -144,12 +156,30 @@ export const catalog = createCatalog({
           <h2 className="text-2xl font-semibold mb-12 text-center">Features</h2>
           <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-8">
             {[
-              { title: "Guardrails", desc: "AI can only use components you define in the catalog" },
-              { title: "Streaming", desc: "Progressive rendering as JSON streams from the model" },
-              { title: "Data Binding", desc: "Two-way binding with JSON Pointer paths" },
-              { title: "Actions", desc: "Named actions handled by your application" },
-              { title: "Visibility", desc: "Conditional show/hide based on data or auth" },
-              { title: "Validation", desc: "Built-in and custom validation functions" },
+              {
+                title: "Guardrails",
+                desc: "AI can only use components you define in the catalog",
+              },
+              {
+                title: "Streaming",
+                desc: "Progressive rendering as JSON streams from the model",
+              },
+              {
+                title: "Data Binding",
+                desc: "Two-way binding with JSON Pointer paths",
+              },
+              {
+                title: "Actions",
+                desc: "Named actions handled by your application",
+              },
+              {
+                title: "Visibility",
+                desc: "Conditional show/hide based on data or auth",
+              },
+              {
+                title: "Validation",
+                desc: "Built-in and custom validation functions",
+              },
             ].map((feature) => (
               <div key={feature.title}>
                 <h3 className="font-semibold mb-2">{feature.title}</h3>
@@ -165,7 +195,9 @@ export const catalog = createCatalog({
         <div className="max-w-4xl mx-auto px-6 py-24 text-center">
           <h2 className="text-2xl font-semibold mb-4">Get started</h2>
           <div className="flex items-center justify-center gap-2 border border-border rounded px-4 py-3 mb-8 mx-auto w-fit">
-            <code className="text-sm bg-transparent">npm install @json-render/core @json-render/react</code>
+            <code className="text-sm bg-transparent">
+              npm install @json-render/core @json-render/react
+            </code>
             <CopyButton text="npm install @json-render/core @json-render/react" />
           </div>
           <div>

+ 30 - 12
apps/web/components/code-block.tsx

@@ -21,7 +21,11 @@ const vercelDarkTheme = {
       settings: { foreground: "#50E3C2" },
     },
     {
-      scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
+      scope: [
+        "constant.numeric",
+        "constant.language.boolean",
+        "constant.language.null",
+      ],
       settings: { foreground: "#50E3C2" },
     },
     {
@@ -49,7 +53,11 @@ const vercelDarkTheme = {
       settings: { foreground: "#888888" },
     },
     {
-      scope: ["support.type.property-name", "entity.name.tag.json", "meta.object-literal.key"],
+      scope: [
+        "support.type.property-name",
+        "entity.name.tag.json",
+        "meta.object-literal.key",
+      ],
       settings: { foreground: "#EDEDED" },
     },
     {
@@ -80,7 +88,11 @@ const vercelLightTheme = {
       settings: { foreground: "#067a6e" },
     },
     {
-      scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
+      scope: [
+        "constant.numeric",
+        "constant.language.boolean",
+        "constant.language.null",
+      ],
       settings: { foreground: "#067a6e" },
     },
     {
@@ -108,7 +120,11 @@ const vercelLightTheme = {
       settings: { foreground: "#6b7280" },
     },
     {
-      scope: ["support.type.property-name", "entity.name.tag.json", "meta.object-literal.key"],
+      scope: [
+        "support.type.property-name",
+        "entity.name.tag.json",
+        "meta.object-literal.key",
+      ],
       settings: { foreground: "#171717" },
     },
     {
@@ -150,14 +166,16 @@ export function CodeBlock({ code, lang }: CodeBlockProps) {
 
   useEffect(() => {
     getHighlighter().then((highlighter) => {
-      setHtml(highlighter.codeToHtml(code, {
-        lang,
-        themes: {
-          light: "vercel-light",
-          dark: "vercel-dark",
-        },
-        defaultColor: false,
-      }));
+      setHtml(
+        highlighter.codeToHtml(code, {
+          lang,
+          themes: {
+            light: "vercel-light",
+            dark: "vercel-dark",
+          },
+          defaultColor: false,
+        }),
+      );
     });
   }, [code, lang]);
 

+ 24 - 5
apps/web/components/code.tsx

@@ -18,7 +18,11 @@ const vercelDarkTheme = {
       settings: { foreground: "#50E3C2" },
     },
     {
-      scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
+      scope: [
+        "constant.numeric",
+        "constant.language.boolean",
+        "constant.language.null",
+      ],
       settings: { foreground: "#50E3C2" },
     },
     {
@@ -46,7 +50,11 @@ const vercelDarkTheme = {
       settings: { foreground: "#888888" },
     },
     {
-      scope: ["support.type.property-name", "entity.name.tag.json", "meta.object-literal.key"],
+      scope: [
+        "support.type.property-name",
+        "entity.name.tag.json",
+        "meta.object-literal.key",
+      ],
       settings: { foreground: "#EDEDED" },
     },
     {
@@ -77,7 +85,11 @@ const vercelLightTheme = {
       settings: { foreground: "#067a6e" },
     },
     {
-      scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
+      scope: [
+        "constant.numeric",
+        "constant.language.boolean",
+        "constant.language.null",
+      ],
       settings: { foreground: "#067a6e" },
     },
     {
@@ -105,7 +117,11 @@ const vercelLightTheme = {
       settings: { foreground: "#6b7280" },
     },
     {
-      scope: ["support.type.property-name", "entity.name.tag.json", "meta.object-literal.key"],
+      scope: [
+        "support.type.property-name",
+        "entity.name.tag.json",
+        "meta.object-literal.key",
+      ],
       settings: { foreground: "#171717" },
     },
     {
@@ -142,7 +158,10 @@ export async function Code({ children, lang = "typescript" }: CodeProps) {
           className="opacity-0 group-hover:opacity-100 text-neutral-500 dark:text-neutral-400 bg-neutral-100 dark:bg-[#0a0a0a]"
         />
       </div>
-      <div className="overflow-x-auto [&_pre]:bg-transparent! [&_pre]:m-0! [&_pre]:p-4! [&_code]:bg-transparent! [&_.shiki]:bg-transparent!" dangerouslySetInnerHTML={{ __html: html }} />
+      <div
+        className="overflow-x-auto [&_pre]:bg-transparent! [&_pre]:m-0! [&_pre]:p-4! [&_code]:bg-transparent! [&_.shiki]:bg-transparent!"
+        dangerouslySetInnerHTML={{ __html: html }}
+      />
     </div>
   );
 }

+ 20 - 2
apps/web/components/copy-button.tsx

@@ -23,11 +23,29 @@ export function CopyButton({ text, className = "" }: CopyButtonProps) {
       aria-label="Copy code"
     >
       {copied ? (
-        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+        <svg
+          width="14"
+          height="14"
+          viewBox="0 0 24 24"
+          fill="none"
+          stroke="currentColor"
+          strokeWidth="2"
+          strokeLinecap="round"
+          strokeLinejoin="round"
+        >
           <polyline points="20 6 9 17 4 12" />
         </svg>
       ) : (
-        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+        <svg
+          width="14"
+          height="14"
+          viewBox="0 0 24 24"
+          fill="none"
+          stroke="currentColor"
+          strokeWidth="2"
+          strokeLinecap="round"
+          strokeLinejoin="round"
+        >
           <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
           <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
         </svg>

+ 178 - 29
apps/web/components/demo.tsx

@@ -6,7 +6,11 @@ import type { UITree } from "@json-render/core";
 import { toast } from "sonner";
 import { CodeBlock } from "./code-block";
 import { Toaster } from "./ui/sonner";
-import { demoRegistry, fallbackComponent, useInteractiveState } from "./demo/index";
+import {
+  demoRegistry,
+  fallbackComponent,
+  useInteractiveState,
+} from "./demo/index";
 
 const SIMULATION_PROMPT = "Create a contact form with name, email, and message";
 
@@ -17,24 +21,128 @@ interface SimulationStage {
 
 const SIMULATION_STAGES: SimulationStage[] = [
   {
-    tree: { root: "card", elements: { card: { key: "card", type: "Card", props: { title: "Contact Us", maxWidth: "md" }, children: [] } } },
+    tree: {
+      root: "card",
+      elements: {
+        card: {
+          key: "card",
+          type: "Card",
+          props: { title: "Contact Us", maxWidth: "md" },
+          children: [],
+        },
+      },
+    },
     stream: '{"op":"set","path":"/root","value":"card"}',
   },
   {
-    tree: { root: "card", elements: { card: { key: "card", type: "Card", props: { title: "Contact Us", maxWidth: "md" }, children: ["name"] }, name: { key: "name", type: "Input", props: { label: "Name", name: "name" } } } },
-    stream: '{"op":"add","path":"/elements/card","value":{"key":"card","type":"Card","props":{"title":"Contact Us","maxWidth":"md"},"children":["name"]}}',
+    tree: {
+      root: "card",
+      elements: {
+        card: {
+          key: "card",
+          type: "Card",
+          props: { title: "Contact Us", maxWidth: "md" },
+          children: ["name"],
+        },
+        name: {
+          key: "name",
+          type: "Input",
+          props: { label: "Name", name: "name" },
+        },
+      },
+    },
+    stream:
+      '{"op":"add","path":"/elements/card","value":{"key":"card","type":"Card","props":{"title":"Contact Us","maxWidth":"md"},"children":["name"]}}',
   },
   {
-    tree: { root: "card", elements: { card: { key: "card", type: "Card", props: { title: "Contact Us", maxWidth: "md" }, children: ["name", "email"] }, name: { key: "name", type: "Input", props: { label: "Name", name: "name" } }, email: { key: "email", type: "Input", props: { label: "Email", name: "email" } } } },
-    stream: '{"op":"add","path":"/elements/email","value":{"key":"email","type":"Input","props":{"label":"Email","name":"email"}}}',
+    tree: {
+      root: "card",
+      elements: {
+        card: {
+          key: "card",
+          type: "Card",
+          props: { title: "Contact Us", maxWidth: "md" },
+          children: ["name", "email"],
+        },
+        name: {
+          key: "name",
+          type: "Input",
+          props: { label: "Name", name: "name" },
+        },
+        email: {
+          key: "email",
+          type: "Input",
+          props: { label: "Email", name: "email" },
+        },
+      },
+    },
+    stream:
+      '{"op":"add","path":"/elements/email","value":{"key":"email","type":"Input","props":{"label":"Email","name":"email"}}}',
   },
   {
-    tree: { root: "card", elements: { card: { key: "card", type: "Card", props: { title: "Contact Us", maxWidth: "md" }, children: ["name", "email", "message"] }, name: { key: "name", type: "Input", props: { label: "Name", name: "name" } }, email: { key: "email", type: "Input", props: { label: "Email", name: "email" } }, message: { key: "message", type: "Textarea", props: { label: "Message", name: "message" } } } },
-    stream: '{"op":"add","path":"/elements/message","value":{"key":"message","type":"Textarea","props":{"label":"Message","name":"message"}}}',
+    tree: {
+      root: "card",
+      elements: {
+        card: {
+          key: "card",
+          type: "Card",
+          props: { title: "Contact Us", maxWidth: "md" },
+          children: ["name", "email", "message"],
+        },
+        name: {
+          key: "name",
+          type: "Input",
+          props: { label: "Name", name: "name" },
+        },
+        email: {
+          key: "email",
+          type: "Input",
+          props: { label: "Email", name: "email" },
+        },
+        message: {
+          key: "message",
+          type: "Textarea",
+          props: { label: "Message", name: "message" },
+        },
+      },
+    },
+    stream:
+      '{"op":"add","path":"/elements/message","value":{"key":"message","type":"Textarea","props":{"label":"Message","name":"message"}}}',
   },
   {
-    tree: { root: "card", elements: { card: { key: "card", type: "Card", props: { title: "Contact Us", maxWidth: "md" }, children: ["name", "email", "message", "submit"] }, name: { key: "name", type: "Input", props: { label: "Name", name: "name" } }, email: { key: "email", type: "Input", props: { label: "Email", name: "email" } }, message: { key: "message", type: "Textarea", props: { label: "Message", name: "message" } }, submit: { key: "submit", type: "Button", props: { label: "Send Message", variant: "primary" } } } },
-    stream: '{"op":"add","path":"/elements/submit","value":{"key":"submit","type":"Button","props":{"label":"Send Message","variant":"primary"}}}',
+    tree: {
+      root: "card",
+      elements: {
+        card: {
+          key: "card",
+          type: "Card",
+          props: { title: "Contact Us", maxWidth: "md" },
+          children: ["name", "email", "message", "submit"],
+        },
+        name: {
+          key: "name",
+          type: "Input",
+          props: { label: "Name", name: "name" },
+        },
+        email: {
+          key: "email",
+          type: "Input",
+          props: { label: "Email", name: "email" },
+        },
+        message: {
+          key: "message",
+          type: "Textarea",
+          props: { label: "Message", name: "message" },
+        },
+        submit: {
+          key: "submit",
+          type: "Button",
+          props: { label: "Send Message", variant: "primary" },
+        },
+      },
+    },
+    stream:
+      '{"op":"add","path":"/elements/submit","value":{"key":"submit","type":"Button","props":{"label":"Send Message","variant":"primary"}}}',
   },
 ];
 
@@ -85,12 +193,14 @@ export function Demo() {
   // Initialize interactive state for Select components
   useInteractiveState();
 
-  const currentSimulationStage = stageIndex >= 0 ? SIMULATION_STAGES[stageIndex] : null;
+  const currentSimulationStage =
+    stageIndex >= 0 ? SIMULATION_STAGES[stageIndex] : null;
 
   // Determine which tree to display - keep simulation tree until new API response
-  const currentTree = mode === "simulation" 
-    ? (currentSimulationStage?.tree || simulationTree) 
-    : (apiTree || simulationTree);
+  const currentTree =
+    mode === "simulation"
+      ? currentSimulationStage?.tree || simulationTree
+      : apiTree || simulationTree;
 
   const stopGeneration = useCallback(() => {
     if (mode === "simulation") {
@@ -152,7 +262,10 @@ export function Demo() {
     if (mode === "interactive" && apiTree) {
       // Convert tree to stream line for display
       const streamLine = JSON.stringify({ tree: apiTree });
-      if (!streamLines.includes(streamLine) && Object.keys(apiTree.elements).length > 0) {
+      if (
+        !streamLines.includes(streamLine) &&
+        Object.keys(apiTree.elements).length > 0
+      ) {
         setStreamLines((prev) => {
           const lastLine = prev[prev.length - 1];
           if (lastLine !== streamLine) {
@@ -172,15 +285,20 @@ export function Demo() {
 
   // Expose action handler for registry components - shows toast with text
   useEffect(() => {
-    (window as unknown as { __demoAction?: (text: string) => void }).__demoAction = (text: string) => {
+    (
+      window as unknown as { __demoAction?: (text: string) => void }
+    ).__demoAction = (text: string) => {
       toast(text);
     };
     return () => {
-      delete (window as unknown as { __demoAction?: (text: string) => void }).__demoAction;
+      delete (window as unknown as { __demoAction?: (text: string) => void })
+        .__demoAction;
     };
   }, []);
 
-  const jsonCode = currentTree ? JSON.stringify(currentTree, null, 2) : "// waiting...";
+  const jsonCode = currentTree
+    ? JSON.stringify(currentTree, null, 2)
+    : "// waiting...";
 
   const isTypingSimulation = mode === "simulation" && phase === "typing";
   const isStreamingSimulation = mode === "simulation" && phase === "streaming";
@@ -205,7 +323,9 @@ export function Demo() {
         >
           {mode === "simulation" ? (
             <div className="flex items-center flex-1">
-              <span className="inline-flex items-center h-5">{typedPrompt}</span>
+              <span className="inline-flex items-center h-5">
+                {typedPrompt}
+              </span>
               {isTypingSimulation && (
                 <span className="inline-block w-2 h-4 bg-foreground ml-0.5 animate-pulse" />
               )}
@@ -230,7 +350,7 @@ export function Demo() {
               />
             </form>
           )}
-          {(mode === "simulation" || isStreaming) ? (
+          {mode === "simulation" || isStreaming ? (
             <button
               onClick={(e) => {
                 e.stopPropagation();
@@ -276,7 +396,8 @@ export function Demo() {
           )}
         </div>
         <div className="mt-2 text-xs text-muted-foreground text-center">
-          Try: &quot;Create a login form&quot; or &quot;Build a feedback form with rating&quot;
+          Try: &quot;Create a login form&quot; or &quot;Build a feedback form
+          with rating&quot;
         </div>
       </div>
 
@@ -289,7 +410,9 @@ export function Demo() {
                 key={tab}
                 onClick={() => setActiveTab(tab)}
                 className={`text-xs font-mono transition-colors ${
-                  activeTab === tab ? "text-foreground" : "text-muted-foreground hover:text-foreground"
+                  activeTab === tab
+                    ? "text-foreground"
+                    : "text-muted-foreground hover:text-foreground"
                 }`}
               >
                 {tab}
@@ -327,7 +450,9 @@ export function Demo() {
         {/* Rendered output using json-render */}
         <div>
           <div className="flex items-center justify-between mb-2 h-6">
-            <div className="text-xs text-muted-foreground font-mono">render</div>
+            <div className="text-xs text-muted-foreground font-mono">
+              render
+            </div>
             <button
               onClick={() => setIsFullscreen(true)}
               className="text-muted-foreground hover:text-foreground transition-colors"
@@ -353,12 +478,24 @@ export function Demo() {
           <div className="border border-border rounded p-3 bg-background h-96 overflow-auto">
             {currentTree && currentTree.root ? (
               <div className="animate-in fade-in duration-200 w-full min-h-full flex items-center justify-center py-4">
-                <JSONUIProvider registry={demoRegistry as Parameters<typeof JSONUIProvider>[0]['registry']}>
+                <JSONUIProvider
+                  registry={
+                    demoRegistry as Parameters<
+                      typeof JSONUIProvider
+                    >[0]["registry"]
+                  }
+                >
                   <Renderer
                     tree={currentTree}
-                    registry={demoRegistry as Parameters<typeof Renderer>[0]['registry']}
+                    registry={
+                      demoRegistry as Parameters<typeof Renderer>[0]["registry"]
+                    }
                     loading={isStreaming || isStreamingSimulation}
-                    fallback={fallbackComponent as Parameters<typeof Renderer>[0]['fallback']}
+                    fallback={
+                      fallbackComponent as Parameters<
+                        typeof Renderer
+                      >[0]["fallback"]
+                    }
                   />
                 </JSONUIProvider>
               </div>
@@ -400,12 +537,24 @@ export function Demo() {
           <div className="flex-1 overflow-auto p-6">
             {currentTree && currentTree.root ? (
               <div className="w-full min-h-full flex items-center justify-center">
-                <JSONUIProvider registry={demoRegistry as Parameters<typeof JSONUIProvider>[0]['registry']}>
+                <JSONUIProvider
+                  registry={
+                    demoRegistry as Parameters<
+                      typeof JSONUIProvider
+                    >[0]["registry"]
+                  }
+                >
                   <Renderer
                     tree={currentTree}
-                    registry={demoRegistry as Parameters<typeof Renderer>[0]['registry']}
+                    registry={
+                      demoRegistry as Parameters<typeof Renderer>[0]["registry"]
+                    }
                     loading={isStreaming || isStreamingSimulation}
-                    fallback={fallbackComponent as Parameters<typeof Renderer>[0]['fallback']}
+                    fallback={
+                      fallbackComponent as Parameters<
+                        typeof Renderer
+                      >[0]["fallback"]
+                    }
                   />
                 </JSONUIProvider>
               </div>

+ 3 - 1
apps/web/components/demo/alert.tsx

@@ -17,7 +17,9 @@ export function Alert({ element }: ComponentRenderProps) {
           : "bg-blue-50 border-blue-200";
 
   return (
-    <div className={`p-2 rounded border ${alertClass} ${baseClass} ${customClass}`}>
+    <div
+      className={`p-2 rounded border ${alertClass} ${baseClass} ${customClass}`}
+    >
       <div className="text-xs font-medium">{props.title as string}</div>
       {props.message ? (
         <div className="text-[10px] mt-0.5">{props.message as string}</div>

+ 5 - 4
apps/web/components/demo/bar-graph.tsx

@@ -23,13 +23,14 @@ export function BarGraph({ element }: ComponentRenderProps) {
       <div className="flex gap-1">
         {data.map((d, i) => (
           <div key={i} className="flex-1 flex flex-col items-center gap-1">
-            <div className="text-[8px] text-muted-foreground">
-              {d.value}
-            </div>
+            <div className="text-[8px] text-muted-foreground">{d.value}</div>
             <div className="w-full h-20 flex items-end">
               <div
                 className="w-full bg-foreground/80 rounded-t transition-all"
-                style={{ height: `${(d.value / maxValue) * 100}%`, minHeight: 2 }}
+                style={{
+                  height: `${(d.value / maxValue) * 100}%`,
+                  minHeight: 2,
+                }}
               />
             </div>
             <div className="text-[8px] text-muted-foreground truncate w-full text-center">

+ 6 - 6
apps/web/components/demo/line-graph.tsx

@@ -26,8 +26,11 @@ export function LineGraph({ element }: ComponentRenderProps) {
 
   // Calculate points for the SVG path
   const points = data.map((d, i) => {
-    const x = padding.left + (data.length > 1 ? (i / (data.length - 1)) * chartWidth : chartWidth / 2);
-    const y = padding.top + chartHeight - ((d.value - minValue) / range) * chartHeight;
+    const x =
+      padding.left +
+      (data.length > 1 ? (i / (data.length - 1)) * chartWidth : chartWidth / 2);
+    const y =
+      padding.top + chartHeight - ((d.value - minValue) / range) * chartHeight;
     return { x, y, ...d };
   });
 
@@ -42,10 +45,7 @@ export function LineGraph({ element }: ComponentRenderProps) {
         <div className="text-xs font-medium mb-2 text-left">{title}</div>
       ) : null}
       <div className="relative h-24">
-        <svg
-          viewBox={`0 0 ${width} ${height}`}
-          className="w-full h-full"
-        >
+        <svg viewBox={`0 0 ${width} ${height}`} className="w-full h-full">
           {/* Grid lines */}
           <line
             x1={padding.left}

+ 2 - 2
apps/web/components/demo/utils.ts

@@ -18,13 +18,13 @@ let openSelect: string | null = null;
 let setOpenSelect: (v: string | null) => void = () => {};
 let selectValues: Record<string, string> = {};
 let setSelectValues: (
-  fn: (prev: Record<string, string>) => Record<string, string>
+  fn: (prev: Record<string, string>) => Record<string, string>,
 ) => void = () => {};
 
 export function useInteractiveState() {
   const [_openSelect, _setOpenSelect] = useState<string | null>(null);
   const [_selectValues, _setSelectValues] = useState<Record<string, string>>(
-    {}
+    {},
   );
 
   openSelect = _openSelect;

+ 4 - 1
apps/web/components/footer.tsx

@@ -6,7 +6,10 @@ export function Footer() {
       <div className="max-w-5xl mx-auto px-6 py-6 flex justify-between items-center text-sm text-muted-foreground">
         <div>json-render</div>
         <div className="flex gap-6">
-          <Link href="/docs" className="hover:text-foreground transition-colors">
+          <Link
+            href="/docs"
+            className="hover:text-foreground transition-colors"
+          >
             Docs
           </Link>
           <a

+ 4 - 1
apps/web/components/header.tsx

@@ -5,7 +5,10 @@ export function Header() {
   return (
     <header className="sticky top-0 z-50 backdrop-blur-sm border-b border-border bg-background/80">
       <div className="max-w-5xl mx-auto px-6 h-14 flex justify-between items-center">
-        <Link href="/" className="font-semibold hover:opacity-70 transition-opacity">
+        <Link
+          href="/"
+          className="font-semibold hover:opacity-70 transition-opacity"
+        >
           json-render
         </Link>
         <nav className="flex gap-4 items-center text-sm">

+ 9 - 9
apps/web/components/ui/badge.tsx

@@ -1,8 +1,8 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
 
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
 
 const badgeVariants = cva(
   "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
@@ -22,8 +22,8 @@ const badgeVariants = cva(
     defaultVariants: {
       variant: "default",
     },
-  }
-)
+  },
+);
 
 function Badge({
   className,
@@ -32,7 +32,7 @@ function Badge({
   ...props
 }: React.ComponentProps<"span"> &
   VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
-  const Comp = asChild ? Slot : "span"
+  const Comp = asChild ? Slot : "span";
 
   return (
     <Comp
@@ -40,7 +40,7 @@ function Badge({
       className={cn(badgeVariants({ variant }), className)}
       {...props}
     />
-  )
+  );
 }
 
-export { Badge, badgeVariants }
+export { Badge, badgeVariants };

+ 10 - 10
apps/web/components/ui/button.tsx

@@ -1,8 +1,8 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
 
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
 
 const buttonVariants = cva(
   "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
@@ -33,8 +33,8 @@ const buttonVariants = cva(
       variant: "default",
       size: "default",
     },
-  }
-)
+  },
+);
 
 function Button({
   className,
@@ -44,9 +44,9 @@ function Button({
   ...props
 }: React.ComponentProps<"button"> &
   VariantProps<typeof buttonVariants> & {
-    asChild?: boolean
+    asChild?: boolean;
   }) {
-  const Comp = asChild ? Slot : "button"
+  const Comp = asChild ? Slot : "button";
 
   return (
     <Comp
@@ -56,7 +56,7 @@ function Button({
       className={cn(buttonVariants({ variant, size, className }))}
       {...props}
     />
-  )
+  );
 }
 
-export { Button, buttonVariants }
+export { Button, buttonVariants };

+ 13 - 13
apps/web/components/ui/card.tsx

@@ -1,6 +1,6 @@
-import * as React from "react"
+import * as React from "react";
 
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
 
 function Card({ className, ...props }: React.ComponentProps<"div">) {
   return (
@@ -8,11 +8,11 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
       data-slot="card"
       className={cn(
         "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
-        className
+        className,
       )}
       {...props}
     />
-  )
+  );
 }
 
 function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -21,11 +21,11 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
       data-slot="card-header"
       className={cn(
         "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
-        className
+        className,
       )}
       {...props}
     />
-  )
+  );
 }
 
 function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -35,7 +35,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
       className={cn("leading-none font-semibold", className)}
       {...props}
     />
-  )
+  );
 }
 
 function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
@@ -45,7 +45,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
       className={cn("text-muted-foreground text-sm", className)}
       {...props}
     />
-  )
+  );
 }
 
 function CardAction({ className, ...props }: React.ComponentProps<"div">) {
@@ -54,11 +54,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
       data-slot="card-action"
       className={cn(
         "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
-        className
+        className,
       )}
       {...props}
     />
-  )
+  );
 }
 
 function CardContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -68,7 +68,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
       className={cn("px-6", className)}
       {...props}
     />
-  )
+  );
 }
 
 function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -78,7 +78,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
       className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
       {...props}
     />
-  )
+  );
 }
 
 export {
@@ -89,4 +89,4 @@ export {
   CardAction,
   CardDescription,
   CardContent,
-}
+};

+ 7 - 7
apps/web/components/ui/sonner.tsx

@@ -1,10 +1,10 @@
-"use client"
+"use client";
 
-import { useTheme } from "next-themes"
-import { Toaster as Sonner, type ToasterProps } from "sonner"
+import { useTheme } from "next-themes";
+import { Toaster as Sonner, type ToasterProps } from "sonner";
 
 const Toaster = ({ ...props }: ToasterProps) => {
-  const { resolvedTheme } = useTheme()
+  const { resolvedTheme } = useTheme();
 
   return (
     <Sonner
@@ -20,7 +20,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
       }
       {...props}
     />
-  )
-}
+  );
+};
 
-export { Toaster }
+export { Toaster };

+ 11 - 11
apps/web/components/ui/tabs.tsx

@@ -1,9 +1,9 @@
-"use client"
+"use client";
 
-import * as React from "react"
-import * as TabsPrimitive from "@radix-ui/react-tabs"
+import * as React from "react";
+import * as TabsPrimitive from "@radix-ui/react-tabs";
 
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
 
 function Tabs({
   className,
@@ -15,7 +15,7 @@ function Tabs({
       className={cn("flex flex-col gap-2", className)}
       {...props}
     />
-  )
+  );
 }
 
 function TabsList({
@@ -27,11 +27,11 @@ function TabsList({
       data-slot="tabs-list"
       className={cn(
         "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
-        className
+        className,
       )}
       {...props}
     />
-  )
+  );
 }
 
 function TabsTrigger({
@@ -43,11 +43,11 @@ function TabsTrigger({
       data-slot="tabs-trigger"
       className={cn(
         "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
-        className
+        className,
       )}
       {...props}
     />
-  )
+  );
 }
 
 function TabsContent({
@@ -60,7 +60,7 @@ function TabsContent({
       className={cn("flex-1 outline-none", className)}
       {...props}
     />
-  )
+  );
 }
 
-export { Tabs, TabsList, TabsTrigger, TabsContent }
+export { Tabs, TabsList, TabsTrigger, TabsContent };

+ 3 - 3
apps/web/lib/utils.ts

@@ -1,6 +1,6 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
 
 export function cn(...inputs: ClassValue[]) {
-  return twMerge(clsx(inputs))
+  return twMerge(clsx(inputs));
 }

+ 4 - 4
examples/dashboard/app/api/generate/route.ts

@@ -1,12 +1,12 @@
-import { streamText } from 'ai';
-import { componentList } from '@/lib/catalog';
+import { streamText } from "ai";
+import { componentList } from "@/lib/catalog";
 
 export const maxDuration = 30;
 
 const SYSTEM_PROMPT = `You are a dashboard widget generator that outputs JSONL (JSON Lines) patches.
 
 AVAILABLE COMPONENTS:
-${componentList.join(', ')}
+${componentList.join(", ")}
 
 COMPONENT DETAILS:
 - Card: { title?: string, description?: string, padding?: "sm"|"md"|"lg" } - Container with optional title
@@ -69,7 +69,7 @@ export async function POST(req: Request) {
   }
 
   const result = streamText({
-    model: 'anthropic/claude-opus-4.5',
+    model: "anthropic/claude-opus-4.5",
     system: SYSTEM_PROMPT,
     prompt: fullPrompt,
     temperature: 0.7,

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

@@ -1,9 +1,9 @@
-import type { Metadata } from 'next';
-import './globals.css';
+import type { Metadata } from "next";
+import "./globals.css";
 
 export const metadata: Metadata = {
-  title: 'Dashboard | json-render',
-  description: 'AI-generated dashboard widgets with guardrails',
+  title: "Dashboard | json-render",
+  description: "AI-generated dashboard widgets with guardrails",
 };
 
 export default function RootLayout({

+ 111 - 67
examples/dashboard/app/page.tsx

@@ -1,14 +1,14 @@
-'use client';
+"use client";
 
-import { useState, useCallback } from 'react';
+import { useState, useCallback } from "react";
 import {
   DataProvider,
   ActionProvider,
   VisibilityProvider,
   useUIStream,
   Renderer,
-} from '@json-render/react';
-import { componentRegistry } from '@/components/ui';
+} from "@json-render/react";
+import { componentRegistry } from "@/components/ui";
 
 const INITIAL_DATA = {
   analytics: {
@@ -17,36 +17,61 @@ const INITIAL_DATA = {
     customers: 1234,
     orders: 567,
     salesByRegion: [
-      { label: 'US', value: 45000 },
-      { label: 'EU', value: 35000 },
-      { label: 'Asia', value: 28000 },
-      { label: 'Other', value: 17000 },
+      { label: "US", value: 45000 },
+      { label: "EU", value: 35000 },
+      { label: "Asia", value: 28000 },
+      { label: "Other", value: 17000 },
     ],
     recentTransactions: [
-      { id: 'TXN001', customer: 'Acme Corp', amount: 1500, status: 'completed', date: '2024-01-15' },
-      { id: 'TXN002', customer: 'Globex Inc', amount: 2300, status: 'pending', date: '2024-01-14' },
-      { id: 'TXN003', customer: 'Initech', amount: 890, status: 'completed', date: '2024-01-13' },
-      { id: 'TXN004', customer: 'Umbrella Co', amount: 4200, status: 'completed', date: '2024-01-12' },
+      {
+        id: "TXN001",
+        customer: "Acme Corp",
+        amount: 1500,
+        status: "completed",
+        date: "2024-01-15",
+      },
+      {
+        id: "TXN002",
+        customer: "Globex Inc",
+        amount: 2300,
+        status: "pending",
+        date: "2024-01-14",
+      },
+      {
+        id: "TXN003",
+        customer: "Initech",
+        amount: 890,
+        status: "completed",
+        date: "2024-01-13",
+      },
+      {
+        id: "TXN004",
+        customer: "Umbrella Co",
+        amount: 4200,
+        status: "completed",
+        date: "2024-01-12",
+      },
     ],
   },
   form: {
-    dateRange: '',
-    region: '',
+    dateRange: "",
+    region: "",
   },
 };
 
 const ACTION_HANDLERS = {
-  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...'),
+  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 [prompt, setPrompt] = useState("");
   const { tree, isStreaming, error, send, clear } = useUIStream({
-    api: '/api/generate',
-    onError: (err) => console.error('Generation error:', err),
+    api: "/api/generate",
+    onError: (err) => console.error("Generation error:", err),
   });
 
   const handleSubmit = useCallback(
@@ -55,30 +80,37 @@ function DashboardContent() {
       if (!prompt.trim()) return;
       await send(prompt, { data: INITIAL_DATA });
     },
-    [prompt, send]
+    [prompt, send],
   );
 
   const examples = [
-    'Revenue dashboard with metrics and chart',
-    'Recent transactions table',
-    'Customer count with trend',
+    "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: 960, margin: '0 auto', padding: '48px 24px' }}>
+    <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' }}>
+        <h1
+          style={{
+            margin: 0,
+            fontSize: 32,
+            fontWeight: 600,
+            letterSpacing: "-0.02em",
+          }}
+        >
           Dashboard
         </h1>
-        <p style={{ margin: '8px 0 0', color: 'var(--muted)', fontSize: 16 }}>
+        <p style={{ margin: "8px 0 0", color: "var(--muted)", fontSize: 16 }}>
           Generate widgets from prompts. Constrained to your catalog.
         </p>
       </header>
 
       <form onSubmit={handleSubmit} style={{ marginBottom: 32 }}>
-        <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
+        <div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
           <input
             type="text"
             value={prompt}
@@ -87,41 +119,41 @@ function DashboardContent() {
             disabled={isStreaming}
             style={{
               flex: 1,
-              padding: '12px 16px',
-              background: 'var(--card)',
-              border: '1px solid var(--border)',
-              borderRadius: 'var(--radius)',
-              color: 'var(--foreground)',
+              padding: "12px 16px",
+              background: "var(--card)",
+              border: "1px solid var(--border)",
+              borderRadius: "var(--radius)",
+              color: "var(--foreground)",
               fontSize: 16,
-              outline: 'none',
+              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)',
+              padding: "12px 24px",
+              background: isStreaming ? "var(--border)" : "var(--foreground)",
+              color: "var(--background)",
+              border: "none",
+              borderRadius: "var(--radius)",
               fontSize: 16,
               fontWeight: 500,
               opacity: isStreaming || !prompt.trim() ? 0.5 : 1,
             }}
           >
-            {isStreaming ? 'Generating...' : 'Generate'}
+            {isStreaming ? "Generating..." : "Generate"}
           </button>
           {hasElements && (
             <button
               type="button"
               onClick={clear}
               style={{
-                padding: '12px 16px',
-                background: 'transparent',
-                color: 'var(--muted)',
-                border: '1px solid var(--border)',
-                borderRadius: 'var(--radius)',
+                padding: "12px 16px",
+                background: "transparent",
+                color: "var(--muted)",
+                border: "1px solid var(--border)",
+                borderRadius: "var(--radius)",
                 fontSize: 16,
               }}
             >
@@ -130,18 +162,18 @@ function DashboardContent() {
           )}
         </div>
 
-        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
+        <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
           {examples.map((ex) => (
             <button
               key={ex}
               type="button"
               onClick={() => setPrompt(ex)}
               style={{
-                padding: '6px 12px',
-                background: 'var(--card)',
-                color: 'var(--muted)',
-                border: '1px solid var(--border)',
-                borderRadius: 'var(--radius)',
+                padding: "6px 12px",
+                background: "var(--card)",
+                color: "var(--muted)",
+                border: "1px solid var(--border)",
+                borderRadius: "var(--radius)",
                 fontSize: 13,
               }}
             >
@@ -156,10 +188,10 @@ function DashboardContent() {
           style={{
             padding: 16,
             marginBottom: 24,
-            background: 'var(--card)',
-            border: '1px solid var(--border)',
-            borderRadius: 'var(--radius)',
-            color: '#ef4444',
+            background: "var(--card)",
+            border: "1px solid var(--border)",
+            borderRadius: "var(--radius)",
+            color: "#ef4444",
             fontSize: 14,
           }}
         >
@@ -171,35 +203,47 @@ function DashboardContent() {
         style={{
           minHeight: 300,
           padding: 24,
-          background: 'var(--card)',
-          border: '1px solid var(--border)',
-          borderRadius: 'var(--radius)',
+          background: "var(--card)",
+          border: "1px solid var(--border)",
+          borderRadius: "var(--radius)",
         }}
       >
         {!hasElements && !isStreaming ? (
-          <div style={{ textAlign: 'center', padding: '60px 20px', color: 'var(--muted)' }}>
+          <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>
 
       {hasElements && (
         <details style={{ marginTop: 24 }}>
-          <summary style={{ cursor: 'pointer', fontSize: 14, color: 'var(--muted)' }}>
+          <summary
+            style={{ cursor: "pointer", fontSize: 14, color: "var(--muted)" }}
+          >
             View JSON
           </summary>
           <pre
             style={{
               marginTop: 8,
               padding: 16,
-              background: 'var(--card)',
-              border: '1px solid var(--border)',
-              borderRadius: 'var(--radius)',
-              overflow: 'auto',
+              background: "var(--card)",
+              border: "1px solid var(--border)",
+              borderRadius: "var(--radius)",
+              overflow: "auto",
               fontSize: 12,
-              color: 'var(--muted)',
+              color: "var(--muted)",
             }}
           >
             {JSON.stringify(tree, null, 2)}

+ 21 - 16
examples/dashboard/components/ui/alert.tsx

@@ -1,38 +1,43 @@
-'use client';
+"use client";
 
-import { type ComponentRenderProps } from '@json-render/react';
-import { useData } from '@json-render/react';
-import { getByPath } from '@json-render/core';
+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 {
+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) {
+  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 { 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',
+    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)',
+        padding: "12px 16px",
+        borderRadius: "var(--radius)",
+        background: "var(--card)",
+        border: "1px solid var(--border)",
         fontSize: 14,
-        color: colors[variant || 'info'],
+        color: colors[variant || "info"],
       }}
     >
       {resolvedMessage}

+ 21 - 16
examples/dashboard/components/ui/badge.tsx

@@ -1,40 +1,45 @@
-'use client';
+"use client";
 
-import { type ComponentRenderProps } from '@json-render/react';
-import { useData } from '@json-render/react';
-import { getByPath } from '@json-render/core';
+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 {
+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) {
+  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 { 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)',
+    default: "var(--foreground)",
+    success: "#22c55e",
+    warning: "#eab308",
+    error: "#ef4444",
+    info: "var(--muted)",
   };
 
   return (
     <span
       style={{
-        display: 'inline-block',
-        padding: '2px 8px',
+        display: "inline-block",
+        padding: "2px 8px",
         borderRadius: 12,
         fontSize: 12,
         fontWeight: 500,
-        background: 'var(--border)',
-        color: colors[variant || 'default'],
+        background: "var(--border)",
+        color: colors[variant || "default"],
       }}
     >
       {resolvedText}

+ 19 - 11
examples/dashboard/components/ui/button.tsx

@@ -1,7 +1,7 @@
-'use client';
+"use client";
 
-import React from 'react';
-import { type ComponentRenderProps } from '@json-render/react';
+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 {
@@ -12,10 +12,18 @@ export function Button({ element, onAction, loading }: ComponentRenderProps) {
   };
 
   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' },
+    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 (
@@ -23,15 +31,15 @@ export function Button({ element, onAction, loading }: ComponentRenderProps) {
       onClick={() => !disabled && action && onAction?.(action)}
       disabled={!!disabled || loading}
       style={{
-        padding: '8px 16px',
-        borderRadius: 'var(--radius)',
+        padding: "8px 16px",
+        borderRadius: "var(--radius)",
         fontSize: 14,
         fontWeight: 500,
         opacity: disabled ? 0.5 : 1,
-        ...variants[variant || 'primary'],
+        ...variants[variant || "primary"],
       }}
     >
-      {loading ? 'Loading...' : label}
+      {loading ? "Loading..." : label}
     </button>
   );
 }

+ 35 - 8
examples/dashboard/components/ui/card.tsx

@@ -1,6 +1,6 @@
-'use client';
+"use client";
 
-import { type ComponentRenderProps } from '@json-render/react';
+import { type ComponentRenderProps } from "@json-render/react";
 
 export function Card({ element, children }: ComponentRenderProps) {
   const { title, description, padding } = element.props as {
@@ -9,17 +9,44 @@ export function Card({ element, children }: ComponentRenderProps) {
     padding?: string | null;
   };
 
-  const paddings: Record<string, string> = { none: '0', sm: '12px', lg: '24px' };
+  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)' }}>
+    <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
+          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 style={{ padding: paddings[padding || ""] || "16px" }}>
+        {children}
+      </div>
     </div>
   );
 }

+ 36 - 14
examples/dashboard/components/ui/chart.tsx

@@ -1,36 +1,58 @@
-'use client';
+"use client";
 
-import { type ComponentRenderProps } from '@json-render/react';
-import { useData } from '@json-render/react';
-import { getByPath } from '@json-render/core';
+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 { 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;
+  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>;
+    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 }}>
+      {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
+            key={i}
+            style={{
+              flex: 1,
+              display: "flex",
+              flexDirection: "column",
+              alignItems: "center",
+              gap: 4,
+            }}
+          >
             <div
               style={{
-                width: '100%',
+                width: "100%",
                 height: `${(d.value / maxValue) * 100}%`,
-                background: 'var(--foreground)',
-                borderRadius: '4px 4px 0 0',
+                background: "var(--foreground)",
+                borderRadius: "4px 4px 0 0",
                 minHeight: 4,
               }}
             />
-            <span style={{ fontSize: 12, color: 'var(--muted)' }}>{d.label}</span>
+            <span style={{ fontSize: 12, color: "var(--muted)" }}>
+              {d.label}
+            </span>
           </div>
         ))}
       </div>

+ 16 - 13
examples/dashboard/components/ui/date-picker.tsx

@@ -1,29 +1,32 @@
-'use client';
+"use client";
 
-import { type ComponentRenderProps } from '@json-render/react';
-import { useData } from '@json-render/react';
-import { getByPath } from '@json-render/core';
+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 { 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 }}>
+    <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
       <label style={{ fontSize: 14, fontWeight: 500 }}>{label}</label>
       <input
         type="date"
-        value={value ?? ''}
+        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)',
+          padding: "8px 12px",
+          borderRadius: "var(--radius)",
+          border: "1px solid var(--border)",
+          background: "var(--card)",
+          color: "var(--foreground)",
           fontSize: 16,
-          outline: 'none',
+          outline: "none",
         }}
       />
     </div>

+ 17 - 5
examples/dashboard/components/ui/divider.tsx

@@ -1,11 +1,23 @@
-'use client';
+"use client";
 
-import { type ComponentRenderProps } from '@json-render/react';
+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' }} />;
+  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' }} />;
+  return (
+    <hr
+      style={{
+        border: "none",
+        borderTop: "1px solid var(--border)",
+        margin: "16px 0",
+      }}
+    />
+  );
 }

+ 15 - 6
examples/dashboard/components/ui/empty.tsx

@@ -1,14 +1,23 @@
-'use client';
+"use client";
 
-import { type ComponentRenderProps } from '@json-render/react';
+import { type ComponentRenderProps } from "@json-render/react";
 
 export function Empty({ element }: ComponentRenderProps) {
-  const { title, description } = element.props as { title: string; description?: string | null };
+  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 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>
   );
 }

+ 19 - 5
examples/dashboard/components/ui/grid.tsx

@@ -1,13 +1,27 @@
-'use client';
+"use client";
 
-import { type ComponentRenderProps } from '@json-render/react';
+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' };
+  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'] }}>
+    <div
+      style={{
+        display: "grid",
+        gridTemplateColumns: `repeat(${columns || 2}, 1fr)`,
+        gap: gaps[gap || "md"],
+      }}
+    >
       {children}
     </div>
   );

+ 25 - 7
examples/dashboard/components/ui/heading.tsx

@@ -1,11 +1,29 @@
-'use client';
+"use client";
 
-import React from 'react';
-import { type ComponentRenderProps } from '@json-render/react';
+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>;
+  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>
+  );
 }

+ 34 - 34
examples/dashboard/components/ui/index.ts

@@ -1,38 +1,38 @@
-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';
+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';
+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,

+ 5 - 5
examples/dashboard/components/ui/list.tsx

@@ -1,8 +1,8 @@
-'use client';
+"use client";
 
-import { type ComponentRenderProps } from '@json-render/react';
-import { useData } from '@json-render/react';
-import { getByPath } from '@json-render/core';
+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 };
@@ -10,7 +10,7 @@ export function List({ element, children }: ComponentRenderProps) {
   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 style={{ color: "var(--muted)" }}>No items</div>;
   }
 
   return <div>{children}</div>;

+ 32 - 15
examples/dashboard/components/ui/metric.tsx

@@ -1,8 +1,8 @@
-'use client';
+"use client";
 
-import { type ComponentRenderProps } from '@json-render/react';
-import { useData } from '@json-render/react';
-import { getByPath } from '@json-render/core';
+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 {
@@ -16,22 +16,39 @@ export function Metric({ element }: ComponentRenderProps) {
   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);
+  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>
+    <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
+          style={{
+            fontSize: 14,
+            color:
+              trend === "up"
+                ? "#22c55e"
+                : trend === "down"
+                  ? "#ef4444"
+                  : "var(--muted)",
+          }}
+        >
+          {trend === "up" ? "+" : trend === "down" ? "-" : ""}
+          {trendValue}
         </span>
       )}
     </div>

+ 15 - 13
examples/dashboard/components/ui/select.tsx

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

+ 23 - 9
examples/dashboard/components/ui/stack.tsx

@@ -1,19 +1,33 @@
-'use client';
+"use client";
 
-import { type ComponentRenderProps } from '@json-render/react';
+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' };
+  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'],
+        display: "flex",
+        flexDirection: direction === "horizontal" ? "row" : "column",
+        gap: gaps[gap || "md"],
+        alignItems: alignments[align || "stretch"],
       }}
     >
       {children}

+ 33 - 24
examples/dashboard/components/ui/table.tsx

@@ -1,8 +1,8 @@
-'use client';
+"use client";
 
-import { type ComponentRenderProps } from '@json-render/react';
-import { useData } from '@json-render/react';
-import { getByPath } from '@json-render/core';
+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 {
@@ -12,30 +12,35 @@ export function Table({ element }: ComponentRenderProps) {
   };
 
   const { data } = useData();
-  const tableData = getByPath(data, dataPath) as Array<Record<string, unknown>> | undefined;
+  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>;
+    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 (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') {
+    if (format === "date" && typeof value === "string") {
       return new Date(value).toLocaleDateString();
     }
-    if (format === 'badge') {
+    if (format === "badge") {
       return (
         <span
           style={{
-            padding: '2px 8px',
+            padding: "2px 8px",
             borderRadius: 12,
             fontSize: 12,
             fontWeight: 500,
-            background: 'var(--border)',
-            color: 'var(--foreground)',
+            background: "var(--border)",
+            color: "var(--foreground)",
           }}
         >
           {String(value)}
@@ -47,22 +52,26 @@ export function Table({ element }: ComponentRenderProps) {
 
   return (
     <div>
-      {title && <h4 style={{ margin: '0 0 16px', fontSize: 14, fontWeight: 600 }}>{title}</h4>}
-      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
+      {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)',
+                  textAlign: "left",
+                  padding: "12px 8px",
+                  borderBottom: "1px solid var(--border)",
                   fontSize: 12,
                   fontWeight: 500,
-                  color: 'var(--muted)',
-                  textTransform: 'uppercase',
-                  letterSpacing: '0.05em',
+                  color: "var(--muted)",
+                  textTransform: "uppercase",
+                  letterSpacing: "0.05em",
                 }}
               >
                 {col.label}
@@ -77,8 +86,8 @@ export function Table({ element }: ComponentRenderProps) {
                 <td
                   key={col.key}
                   style={{
-                    padding: '12px 8px',
-                    borderBottom: '1px solid var(--border)',
+                    padding: "12px 8px",
+                    borderBottom: "1px solid var(--border)",
                     fontSize: 14,
                   }}
                 >

+ 30 - 26
examples/dashboard/components/ui/text-field.tsx

@@ -1,53 +1,57 @@
-'use client';
+"use client";
 
-import { type ComponentRenderProps } from '@json-render/react';
-import { useData, useFieldValidation } from '@json-render/react';
-import { getByPath } from '@json-render/core';
+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 { 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',
+    validateOn: (validateOn as "change" | "blur" | "submit") ?? "blur",
   });
 
   return (
-    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
+    <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
       <label style={{ fontSize: 14, fontWeight: 500 }}>{label}</label>
       <input
-        type={type || 'text'}
-        value={value ?? ''}
+        type={type || "text"}
+        value={value ?? ""}
         onChange={(e) => {
           set(valuePath, e.target.value);
-          if (validateOn === 'change') validate();
+          if (validateOn === "change") validate();
         }}
         onBlur={() => {
           touch();
-          if (validateOn === 'blur' || !validateOn) validate();
+          if (validateOn === "blur" || !validateOn) validate();
         }}
-        placeholder={placeholder ?? ''}
+        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)',
+          padding: "8px 12px",
+          borderRadius: "var(--radius)",
+          border:
+            errors.length > 0 ? "1px solid #ef4444" : "1px solid var(--border)",
+          background: "var(--card)",
+          color: "var(--foreground)",
           fontSize: 16,
-          outline: 'none',
+          outline: "none",
         }}
       />
       {errors.map((error, i) => (
-        <span key={i} style={{ fontSize: 12, color: '#ef4444' }}>{error}</span>
+        <span key={i} style={{ fontSize: 12, color: "#ef4444" }}>
+          {error}
+        </span>
       ))}
     </div>
   );

+ 14 - 9
examples/dashboard/components/ui/text.tsx

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

+ 47 - 43
examples/dashboard/lib/catalog.ts

@@ -1,46 +1,46 @@
-import { createCatalog } from '@json-render/core';
-import { z } from 'zod';
+import { createCatalog } from "@json-render/core";
+import { z } from "zod";
 
 /**
  * Dashboard component catalog
  *
  * This defines the ONLY components that the AI can generate.
  * It acts as a guardrail - the AI cannot create arbitrary HTML/CSS.
- * 
+ *
  * Note: OpenAI structured output requires all fields to be required.
  * Use .nullable() instead of .optional() for optional fields.
  */
 export const dashboardCatalog = createCatalog({
-  name: 'dashboard',
+  name: "dashboard",
   components: {
     // Layout Components
     Card: {
       props: z.object({
         title: z.string().nullable(),
         description: z.string().nullable(),
-        padding: z.enum(['sm', 'md', 'lg']).nullable(),
+        padding: z.enum(["sm", "md", "lg"]).nullable(),
       }),
       hasChildren: true,
-      description: 'A card container with optional title',
+      description: "A card container with optional title",
     },
 
     Grid: {
       props: z.object({
         columns: z.number().min(1).max(4).nullable(),
-        gap: z.enum(['sm', 'md', 'lg']).nullable(),
+        gap: z.enum(["sm", "md", "lg"]).nullable(),
       }),
       hasChildren: true,
-      description: 'Grid layout with configurable columns',
+      description: "Grid layout with configurable columns",
     },
 
     Stack: {
       props: z.object({
-        direction: z.enum(['horizontal', 'vertical']).nullable(),
-        gap: z.enum(['sm', 'md', 'lg']).nullable(),
-        align: z.enum(['start', 'center', 'end', 'stretch']).nullable(),
+        direction: z.enum(["horizontal", "vertical"]).nullable(),
+        gap: z.enum(["sm", "md", "lg"]).nullable(),
+        align: z.enum(["start", "center", "end", "stretch"]).nullable(),
       }),
       hasChildren: true,
-      description: 'Flex stack for horizontal or vertical layouts',
+      description: "Flex stack for horizontal or vertical layouts",
     },
 
     // Data Display Components
@@ -48,21 +48,21 @@ export const dashboardCatalog = createCatalog({
       props: z.object({
         label: z.string(),
         valuePath: z.string(),
-        format: z.enum(['number', 'currency', 'percent']).nullable(),
-        trend: z.enum(['up', 'down', 'neutral']).nullable(),
+        format: z.enum(["number", "currency", "percent"]).nullable(),
+        trend: z.enum(["up", "down", "neutral"]).nullable(),
         trendValue: z.string().nullable(),
       }),
-      description: 'Display a single metric with optional trend indicator',
+      description: "Display a single metric with optional trend indicator",
     },
 
     Chart: {
       props: z.object({
-        type: z.enum(['bar', 'line', 'pie', 'area']),
+        type: z.enum(["bar", "line", "pie", "area"]),
         dataPath: z.string(),
         title: z.string().nullable(),
         height: z.number().nullable(),
       }),
-      description: 'Display a chart from array data',
+      description: "Display a chart from array data",
     },
 
     Table: {
@@ -72,11 +72,11 @@ export const dashboardCatalog = createCatalog({
           z.object({
             key: z.string(),
             label: z.string(),
-            format: z.enum(['text', 'currency', 'date', 'badge']).nullable(),
-          })
+            format: z.enum(["text", "currency", "date", "badge"]).nullable(),
+          }),
         ),
       }),
-      description: 'Display tabular data',
+      description: "Display tabular data",
     },
 
     List: {
@@ -85,19 +85,19 @@ export const dashboardCatalog = createCatalog({
         emptyMessage: z.string().nullable(),
       }),
       hasChildren: true,
-      description: 'Render a list from array data',
+      description: "Render a list from array data",
     },
 
     // Interactive Components
     Button: {
       props: z.object({
         label: z.string(),
-        variant: z.enum(['primary', 'secondary', 'danger', 'ghost']).nullable(),
-        size: z.enum(['sm', 'md', 'lg']).nullable(),
+        variant: z.enum(["primary", "secondary", "danger", "ghost"]).nullable(),
+        size: z.enum(["sm", "md", "lg"]).nullable(),
         action: z.string(),
         disabled: z.boolean().nullable(),
       }),
-      description: 'Clickable button with action',
+      description: "Clickable button with action",
     },
 
     Select: {
@@ -108,11 +108,11 @@ export const dashboardCatalog = createCatalog({
           z.object({
             value: z.string(),
             label: z.string(),
-          })
+          }),
         ),
         placeholder: z.string().nullable(),
       }),
-      description: 'Dropdown select input',
+      description: "Dropdown select input",
     },
 
     DatePicker: {
@@ -121,44 +121,48 @@ export const dashboardCatalog = createCatalog({
         bindPath: z.string(),
         placeholder: z.string().nullable(),
       }),
-      description: 'Date picker input',
+      description: "Date picker input",
     },
 
     // Typography
     Heading: {
       props: z.object({
         text: z.string(),
-        level: z.enum(['h1', 'h2', 'h3', 'h4']).nullable(),
+        level: z.enum(["h1", "h2", "h3", "h4"]).nullable(),
       }),
-      description: 'Section heading',
+      description: "Section heading",
     },
 
     Text: {
       props: z.object({
         content: z.string(),
-        variant: z.enum(['body', 'caption', 'label']).nullable(),
-        color: z.enum(['default', 'muted', 'success', 'warning', 'danger']).nullable(),
+        variant: z.enum(["body", "caption", "label"]).nullable(),
+        color: z
+          .enum(["default", "muted", "success", "warning", "danger"])
+          .nullable(),
       }),
-      description: 'Text paragraph',
+      description: "Text paragraph",
     },
 
     // Status Components
     Badge: {
       props: z.object({
         text: z.string(),
-        variant: z.enum(['default', 'success', 'warning', 'danger', 'info']).nullable(),
+        variant: z
+          .enum(["default", "success", "warning", "danger", "info"])
+          .nullable(),
       }),
-      description: 'Small status badge',
+      description: "Small status badge",
     },
 
     Alert: {
       props: z.object({
-        type: z.enum(['info', 'success', 'warning', 'error']),
+        type: z.enum(["info", "success", "warning", "error"]),
         title: z.string(),
         message: z.string().nullable(),
         dismissible: z.boolean().nullable(),
       }),
-      description: 'Alert/notification banner',
+      description: "Alert/notification banner",
     },
 
     // Special Components
@@ -166,7 +170,7 @@ export const dashboardCatalog = createCatalog({
       props: z.object({
         label: z.string().nullable(),
       }),
-      description: 'Visual divider',
+      description: "Visual divider",
     },
 
     Empty: {
@@ -176,16 +180,16 @@ export const dashboardCatalog = createCatalog({
         action: z.string().nullable(),
         actionLabel: z.string().nullable(),
       }),
-      description: 'Empty state placeholder',
+      description: "Empty state placeholder",
     },
   },
   actions: {
-    export_report: { description: 'Export the current dashboard to PDF' },
-    refresh_data: { description: 'Refresh all metrics and charts' },
-    view_details: { description: 'View detailed information' },
-    apply_filter: { description: 'Apply the current filter settings' },
+    export_report: { description: "Export the current dashboard to PDF" },
+    refresh_data: { description: "Refresh all metrics and charts" },
+    view_details: { description: "View detailed information" },
+    apply_filter: { description: "Apply the current filter settings" },
   },
-  validation: 'strict',
+  validation: "strict",
 });
 
 // Export the component list for the AI prompt

+ 39 - 29
packages/core/src/actions.ts

@@ -1,6 +1,6 @@
-import { z } from 'zod';
-import type { DynamicValue, DataModel } from './types';
-import { DynamicValueSchema, resolveDynamicValue } from './types';
+import { z } from "zod";
+import type { DynamicValue, DataModel } from "./types";
+import { DynamicValueSchema, resolveDynamicValue } from "./types";
 
 /**
  * Confirmation dialog configuration
@@ -10,7 +10,7 @@ export interface ActionConfirm {
   message: string;
   confirmLabel?: string;
   cancelLabel?: string;
-  variant?: 'default' | 'danger';
+  variant?: "default" | "danger";
 }
 
 /**
@@ -52,7 +52,7 @@ export const ActionConfirmSchema = z.object({
   message: z.string(),
   confirmLabel: z.string().optional(),
   cancelLabel: z.string().optional(),
-  variant: z.enum(['default', 'danger']).optional(),
+  variant: z.enum(["default", "danger"]).optional(),
 });
 
 /**
@@ -86,9 +86,10 @@ export const ActionSchema = z.object({
 /**
  * Action handler function signature
  */
-export type ActionHandler<TParams = Record<string, unknown>, TResult = unknown> = (
-  params: TParams
-) => Promise<TResult> | TResult;
+export type ActionHandler<
+  TParams = Record<string, unknown>,
+  TResult = unknown,
+> = (params: TParams) => Promise<TResult> | TResult;
 
 /**
  * Action definition in catalog
@@ -114,15 +115,18 @@ export interface ResolvedAction {
 /**
  * Resolve all dynamic values in an action
  */
-export function resolveAction(action: Action, dataModel: DataModel): ResolvedAction {
+export function resolveAction(
+  action: Action,
+  dataModel: DataModel,
+): ResolvedAction {
   const resolvedParams: Record<string, unknown> = {};
-  
+
   if (action.params) {
     for (const [key, value] of Object.entries(action.params)) {
       resolvedParams[key] = resolveDynamicValue(value, dataModel);
     }
   }
-  
+
   // Interpolate confirmation message if present
   let confirm = action.confirm;
   if (confirm) {
@@ -132,7 +136,7 @@ export function resolveAction(action: Action, dataModel: DataModel): ResolvedAct
       title: interpolateString(confirm.title, dataModel),
     };
   }
-  
+
   return {
     name: action.name,
     params: resolvedParams,
@@ -145,10 +149,13 @@ export function resolveAction(action: Action, dataModel: DataModel): ResolvedAct
 /**
  * Interpolate ${path} expressions in a string
  */
-export function interpolateString(template: string, dataModel: DataModel): string {
+export function interpolateString(
+  template: string,
+  dataModel: DataModel,
+): string {
   return template.replace(/\$\{([^}]+)\}/g, (_, path) => {
     const value = resolveDynamicValue({ path }, dataModel);
-    return String(value ?? '');
+    return String(value ?? "");
   });
 }
 
@@ -171,36 +178,39 @@ export interface ActionExecutionContext {
 /**
  * Execute an action with all callbacks
  */
-export async function executeAction(ctx: ActionExecutionContext): Promise<void> {
+export async function executeAction(
+  ctx: ActionExecutionContext,
+): Promise<void> {
   const { action, handler, setData, navigate, executeAction } = ctx;
-  
+
   try {
     await handler(action.params);
-    
+
     // Handle success
     if (action.onSuccess) {
-      if ('navigate' in action.onSuccess && navigate) {
+      if ("navigate" in action.onSuccess && navigate) {
         navigate(action.onSuccess.navigate);
-      } else if ('set' in action.onSuccess) {
+      } else if ("set" in action.onSuccess) {
         for (const [path, value] of Object.entries(action.onSuccess.set)) {
           setData(path, value);
         }
-      } else if ('action' in action.onSuccess && executeAction) {
+      } else if ("action" in action.onSuccess && executeAction) {
         await executeAction(action.onSuccess.action);
       }
     }
   } catch (error) {
     // Handle error
     if (action.onError) {
-      if ('set' in action.onError) {
+      if ("set" in action.onError) {
         for (const [path, value] of Object.entries(action.onError.set)) {
           // Replace $error.message with actual error
-          const resolvedValue = typeof value === 'string' && value === '$error.message'
-            ? (error as Error).message
-            : value;
+          const resolvedValue =
+            typeof value === "string" && value === "$error.message"
+              ? (error as Error).message
+              : value;
           setData(path, resolvedValue);
         }
-      } else if ('action' in action.onError && executeAction) {
+      } else if ("action" in action.onError && executeAction) {
         await executeAction(action.onError.action);
       }
     } else {
@@ -218,23 +228,23 @@ export const action = {
     name,
     params,
   }),
-  
+
   /** Create an action with confirmation */
   withConfirm: (
     name: string,
     confirm: ActionConfirm,
-    params?: Record<string, DynamicValue>
+    params?: Record<string, DynamicValue>,
   ): Action => ({
     name,
     params,
     confirm,
   }),
-  
+
   /** Create an action with success handler */
   withSuccess: (
     name: string,
     onSuccess: ActionOnSuccess,
-    params?: Record<string, DynamicValue>
+    params?: Record<string, DynamicValue>,
   ): Action => ({
     name,
     params,

+ 83 - 45
packages/core/src/catalog.ts

@@ -1,19 +1,21 @@
-import { z } from 'zod';
+import { z } from "zod";
 import type {
   ComponentSchema,
   ValidationMode,
   UIElement,
   UITree,
   VisibilityCondition,
-} from './types';
-import { VisibilityConditionSchema } from './visibility';
-import { ActionSchema, type ActionDefinition } from './actions';
-import { ValidationConfigSchema, type ValidationFunction } from './validation';
+} from "./types";
+import { VisibilityConditionSchema } from "./visibility";
+import { ActionSchema, type ActionDefinition } from "./actions";
+import { ValidationConfigSchema, type ValidationFunction } from "./validation";
 
 /**
  * Component definition with visibility and validation support
  */
-export interface ComponentDefinition<TProps extends ComponentSchema = ComponentSchema> {
+export interface ComponentDefinition<
+  TProps extends ComponentSchema = ComponentSchema,
+> {
   /** Zod schema for component props */
   props: TProps;
   /** Whether this component can have children */
@@ -26,9 +28,18 @@ export interface ComponentDefinition<TProps extends ComponentSchema = ComponentS
  * Catalog configuration
  */
 export interface CatalogConfig<
-  TComponents extends Record<string, ComponentDefinition> = Record<string, ComponentDefinition>,
-  TActions extends Record<string, ActionDefinition> = Record<string, ActionDefinition>,
-  TFunctions extends Record<string, ValidationFunction> = Record<string, ValidationFunction>
+  TComponents extends Record<string, ComponentDefinition> = Record<
+    string,
+    ComponentDefinition
+  >,
+  TActions extends Record<string, ActionDefinition> = Record<
+    string,
+    ActionDefinition
+  >,
+  TFunctions extends Record<string, ValidationFunction> = Record<
+    string,
+    ValidationFunction
+  >,
 > {
   /** Catalog name */
   name?: string;
@@ -46,9 +57,18 @@ export interface CatalogConfig<
  * Catalog instance
  */
 export interface Catalog<
-  TComponents extends Record<string, ComponentDefinition> = Record<string, ComponentDefinition>,
-  TActions extends Record<string, ActionDefinition> = Record<string, ActionDefinition>,
-  TFunctions extends Record<string, ValidationFunction> = Record<string, ValidationFunction>
+  TComponents extends Record<string, ComponentDefinition> = Record<
+    string,
+    ComponentDefinition
+  >,
+  TActions extends Record<string, ActionDefinition> = Record<
+    string,
+    ActionDefinition
+  >,
+  TFunctions extends Record<string, ValidationFunction> = Record<
+    string,
+    ValidationFunction
+  >,
 > {
   /** Catalog name */
   readonly name: string;
@@ -77,9 +97,17 @@ export interface Catalog<
   /** Check if function exists */
   hasFunction(name: string): boolean;
   /** Validate an element */
-  validateElement(element: unknown): { success: boolean; data?: UIElement; error?: z.ZodError };
+  validateElement(element: unknown): {
+    success: boolean;
+    data?: UIElement;
+    error?: z.ZodError;
+  };
   /** Validate a UI tree */
-  validateTree(tree: unknown): { success: boolean; data?: UITree; error?: z.ZodError };
+  validateTree(tree: unknown): {
+    success: boolean;
+    data?: UITree;
+    error?: z.ZodError;
+  };
 }
 
 /**
@@ -87,17 +115,23 @@ export interface Catalog<
  */
 export function createCatalog<
   TComponents extends Record<string, ComponentDefinition>,
-  TActions extends Record<string, ActionDefinition> = Record<string, ActionDefinition>,
-  TFunctions extends Record<string, ValidationFunction> = Record<string, ValidationFunction>
+  TActions extends Record<string, ActionDefinition> = Record<
+    string,
+    ActionDefinition
+  >,
+  TFunctions extends Record<string, ValidationFunction> = Record<
+    string,
+    ValidationFunction
+  >,
 >(
-  config: CatalogConfig<TComponents, TActions, TFunctions>
+  config: CatalogConfig<TComponents, TActions, TFunctions>,
 ): Catalog<TComponents, TActions, TFunctions> {
   const {
-    name = 'unnamed',
+    name = "unnamed",
     components,
     actions = {} as TActions,
     functions = {} as TFunctions,
-    validation = 'strict',
+    validation = "strict",
   } = config;
 
   const componentNames = Object.keys(components) as (keyof TComponents)[];
@@ -107,7 +141,7 @@ export function createCatalog<
   // Create element schema for each component type
   const componentSchemas = componentNames.map((componentName) => {
     const def = components[componentName]!;
-    
+
     return z.object({
       key: z.string(),
       type: z.literal(componentName as string),
@@ -120,7 +154,7 @@ export function createCatalog<
 
   // Create union schema for all components
   let elementSchema: z.ZodType<UIElement>;
-  
+
   if (componentSchemas.length === 0) {
     elementSchema = z.object({
       key: z.string(),
@@ -133,10 +167,10 @@ export function createCatalog<
   } else if (componentSchemas.length === 1) {
     elementSchema = componentSchemas[0] as unknown as z.ZodType<UIElement>;
   } else {
-    elementSchema = z.discriminatedUnion('type', [
+    elementSchema = z.discriminatedUnion("type", [
       componentSchemas[0] as z.ZodObject<any>,
       componentSchemas[1] as z.ZodObject<any>,
-      ...componentSchemas.slice(2) as z.ZodObject<any>[],
+      ...(componentSchemas.slice(2) as z.ZodObject<any>[]),
     ]) as unknown as z.ZodType<UIElement>;
   }
 
@@ -194,13 +228,13 @@ export function createCatalog<
 export function generateCatalogPrompt<
   TComponents extends Record<string, ComponentDefinition>,
   TActions extends Record<string, ActionDefinition>,
-  TFunctions extends Record<string, ValidationFunction>
+  TFunctions extends Record<string, ValidationFunction>,
 >(catalog: Catalog<TComponents, TActions, TFunctions>): string {
   const lines: string[] = [
     `# ${catalog.name} Component Catalog`,
-    '',
-    '## Available Components',
-    '',
+    "",
+    "## Available Components",
+    "",
   ];
 
   // Components
@@ -210,50 +244,54 @@ export function generateCatalogPrompt<
     if (def.description) {
       lines.push(def.description);
     }
-    lines.push('');
+    lines.push("");
   }
 
   // Actions
   if (catalog.actionNames.length > 0) {
-    lines.push('## Available Actions');
-    lines.push('');
+    lines.push("## Available Actions");
+    lines.push("");
     for (const name of catalog.actionNames) {
       const def = catalog.actions[name]!;
-      lines.push(`- \`${String(name)}\`${def.description ? `: ${def.description}` : ''}`);
+      lines.push(
+        `- \`${String(name)}\`${def.description ? `: ${def.description}` : ""}`,
+      );
     }
-    lines.push('');
+    lines.push("");
   }
 
   // Visibility
-  lines.push('## Visibility Conditions');
-  lines.push('');
-  lines.push('Components can have a `visible` property:');
-  lines.push('- `true` / `false` - Always visible/hidden');
+  lines.push("## Visibility Conditions");
+  lines.push("");
+  lines.push("Components can have a `visible` property:");
+  lines.push("- `true` / `false` - Always visible/hidden");
   lines.push('- `{ "path": "/data/path" }` - Visible when path is truthy');
   lines.push('- `{ "auth": "signedIn" }` - Visible when user is signed in');
   lines.push('- `{ "and": [...] }` - All conditions must be true');
   lines.push('- `{ "or": [...] }` - Any condition must be true');
   lines.push('- `{ "not": {...} }` - Negates a condition');
   lines.push('- `{ "eq": [a, b] }` - Equality check');
-  lines.push('');
+  lines.push("");
 
   // Validation
-  lines.push('## Validation Functions');
-  lines.push('');
-  lines.push('Built-in: `required`, `email`, `minLength`, `maxLength`, `pattern`, `min`, `max`, `url`');
+  lines.push("## Validation Functions");
+  lines.push("");
+  lines.push(
+    "Built-in: `required`, `email`, `minLength`, `maxLength`, `pattern`, `min`, `max`, `url`",
+  );
   if (catalog.functionNames.length > 0) {
-    lines.push(`Custom: ${catalog.functionNames.map(String).join(', ')}`);
+    lines.push(`Custom: ${catalog.functionNames.map(String).join(", ")}`);
   }
-  lines.push('');
+  lines.push("");
 
-  return lines.join('\n');
+  return lines.join("\n");
 }
 
 /**
  * Type helper to infer component props from catalog
  */
 export type InferCatalogComponentProps<
-  C extends Catalog<Record<string, ComponentDefinition>>
+  C extends Catalog<Record<string, ComponentDefinition>>,
 > = {
-  [K in keyof C['components']]: z.infer<C['components'][K]['props']>;
+  [K in keyof C["components"]]: z.infer<C["components"][K]["props"]>;
 };

+ 10 - 15
packages/core/src/index.ts

@@ -14,7 +14,7 @@ export type {
   ValidationMode,
   PatchOp,
   JsonPatch,
-} from './types';
+} from "./types";
 
 export {
   DynamicValueSchema,
@@ -24,12 +24,10 @@ export {
   resolveDynamicValue,
   getByPath,
   setByPath,
-} from './types';
+} from "./types";
 
 // Visibility
-export type {
-  VisibilityContext,
-} from './visibility';
+export type { VisibilityContext } from "./visibility";
 
 export {
   VisibilityConditionSchema,
@@ -37,7 +35,7 @@ export {
   evaluateVisibility,
   evaluateLogicExpression,
   visibility,
-} from './visibility';
+} from "./visibility";
 
 // Actions
 export type {
@@ -49,7 +47,7 @@ export type {
   ActionDefinition,
   ResolvedAction,
   ActionExecutionContext,
-} from './actions';
+} from "./actions";
 
 export {
   ActionSchema,
@@ -60,7 +58,7 @@ export {
   executeAction,
   interpolateString,
   action,
-} from './actions';
+} from "./actions";
 
 // Validation
 export type {
@@ -71,7 +69,7 @@ export type {
   ValidationCheckResult,
   ValidationResult,
   ValidationContext,
-} from './validation';
+} from "./validation";
 
 export {
   ValidationCheckSchema,
@@ -80,7 +78,7 @@ export {
   runValidationCheck,
   runValidation,
   check,
-} from './validation';
+} from "./validation";
 
 // Catalog
 export type {
@@ -88,9 +86,6 @@ export type {
   CatalogConfig,
   Catalog,
   InferCatalogComponentProps,
-} from './catalog';
+} from "./catalog";
 
-export {
-  createCatalog,
-  generateCatalogPrompt,
-} from './catalog';
+export { createCatalog, generateCatalogPrompt } from "./catalog";

+ 36 - 31
packages/core/src/types.ts

@@ -1,11 +1,9 @@
-import { z } from 'zod';
+import { z } from "zod";
 
 /**
  * Dynamic value - can be a literal or a path reference to data model
  */
-export type DynamicValue<T = unknown> =
-  | T
-  | { path: string };
+export type DynamicValue<T = unknown> = T | { path: string };
 
 /**
  * Dynamic string value
@@ -51,7 +49,10 @@ export const DynamicBooleanSchema = z.union([
 /**
  * Base UI element structure for v2
  */
-export interface UIElement<T extends string = string, P = Record<string, unknown>> {
+export interface UIElement<
+  T extends string = string,
+  P = Record<string, unknown>,
+> {
   /** Unique key for reconciliation */
   key: string;
   /** Component type from the catalog */
@@ -72,7 +73,7 @@ export interface UIElement<T extends string = string, P = Record<string, unknown
 export type VisibilityCondition =
   | boolean
   | { path: string }
-  | { auth: 'signedIn' | 'signedOut' }
+  | { auth: "signedIn" | "signedOut" }
   | LogicExpression;
 
 /**
@@ -121,12 +122,12 @@ export type ComponentSchema = z.ZodType<Record<string, unknown>>;
 /**
  * Validation mode for catalog validation
  */
-export type ValidationMode = 'strict' | 'warn' | 'ignore';
+export type ValidationMode = "strict" | "warn" | "ignore";
 
 /**
  * JSON patch operation types
  */
-export type PatchOp = 'add' | 'remove' | 'replace' | 'set';
+export type PatchOp = "add" | "remove" | "replace" | "set";
 
 /**
  * JSON patch operation
@@ -142,16 +143,16 @@ export interface JsonPatch {
  */
 export function resolveDynamicValue<T>(
   value: DynamicValue<T>,
-  dataModel: DataModel
+  dataModel: DataModel,
 ): T | undefined {
   if (value === null || value === undefined) {
     return undefined;
   }
-  
-  if (typeof value === 'object' && 'path' in value) {
+
+  if (typeof value === "object" && "path" in value) {
     return getByPath(dataModel, value.path) as T | undefined;
   }
-  
+
   return value as T;
 }
 
@@ -159,51 +160,55 @@ export function resolveDynamicValue<T>(
  * Get a value from an object by JSON Pointer path
  */
 export function getByPath(obj: unknown, path: string): unknown {
-  if (!path || path === '/') {
+  if (!path || path === "/") {
     return obj;
   }
-  
-  const segments = path.startsWith('/')
-    ? path.slice(1).split('/')
-    : path.split('/');
-  
+
+  const segments = path.startsWith("/")
+    ? path.slice(1).split("/")
+    : path.split("/");
+
   let current: unknown = obj;
-  
+
   for (const segment of segments) {
     if (current === null || current === undefined) {
       return undefined;
     }
-    
-    if (typeof current === 'object') {
+
+    if (typeof current === "object") {
       current = (current as Record<string, unknown>)[segment];
     } else {
       return undefined;
     }
   }
-  
+
   return current;
 }
 
 /**
  * Set a value in an object by JSON Pointer path
  */
-export function setByPath(obj: Record<string, unknown>, path: string, value: unknown): void {
-  const segments = path.startsWith('/')
-    ? path.slice(1).split('/')
-    : path.split('/');
-  
+export function setByPath(
+  obj: Record<string, unknown>,
+  path: string,
+  value: unknown,
+): void {
+  const segments = path.startsWith("/")
+    ? path.slice(1).split("/")
+    : path.split("/");
+
   if (segments.length === 0) return;
-  
+
   let current: Record<string, unknown> = obj;
-  
+
   for (let i = 0; i < segments.length - 1; i++) {
     const segment = segments[i]!;
-    if (!(segment in current) || typeof current[segment] !== 'object') {
+    if (!(segment in current) || typeof current[segment] !== "object") {
       current[segment] = {};
     }
     current = current[segment] as Record<string, unknown>;
   }
-  
+
   const lastSegment = segments[segments.length - 1]!;
   current[lastSegment] = value;
 }

+ 59 - 55
packages/core/src/validation.ts

@@ -1,7 +1,7 @@
-import { z } from 'zod';
-import type { DynamicValue, DataModel, LogicExpression } from './types';
-import { DynamicValueSchema, resolveDynamicValue } from './types';
-import { LogicExpressionSchema, evaluateLogicExpression } from './visibility';
+import { z } from "zod";
+import type { DynamicValue, DataModel, LogicExpression } from "./types";
+import { DynamicValueSchema, resolveDynamicValue } from "./types";
+import { LogicExpressionSchema, evaluateLogicExpression } from "./visibility";
 
 /**
  * Validation check definition
@@ -22,7 +22,7 @@ export interface ValidationConfig {
   /** Array of checks to run */
   checks?: ValidationCheck[];
   /** When to run validation */
-  validateOn?: 'change' | 'blur' | 'submit';
+  validateOn?: "change" | "blur" | "submit";
   /** Condition for when validation is enabled */
   enabled?: LogicExpression;
 }
@@ -41,7 +41,7 @@ export const ValidationCheckSchema = z.object({
  */
 export const ValidationConfigSchema = z.object({
   checks: z.array(ValidationCheckSchema).optional(),
-  validateOn: z.enum(['change', 'blur', 'submit']).optional(),
+  validateOn: z.enum(["change", "blur", "submit"]).optional(),
   enabled: LogicExpressionSchema.optional(),
 });
 
@@ -50,7 +50,7 @@ export const ValidationConfigSchema = z.object({
  */
 export type ValidationFunction = (
   value: unknown,
-  args?: Record<string, unknown>
+  args?: Record<string, unknown>,
 ) => boolean;
 
 /**
@@ -72,7 +72,7 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
    */
   required: (value: unknown) => {
     if (value === null || value === undefined) return false;
-    if (typeof value === 'string') return value.trim().length > 0;
+    if (typeof value === "string") return value.trim().length > 0;
     if (Array.isArray(value)) return value.length > 0;
     return true;
   },
@@ -81,7 +81,7 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
    * Check if value is a valid email address
    */
   email: (value: unknown) => {
-    if (typeof value !== 'string') return false;
+    if (typeof value !== "string") return false;
     return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
   },
 
@@ -89,9 +89,9 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
    * Check minimum string length
    */
   minLength: (value: unknown, args?: Record<string, unknown>) => {
-    if (typeof value !== 'string') return false;
+    if (typeof value !== "string") return false;
     const min = args?.min;
-    if (typeof min !== 'number') return false;
+    if (typeof min !== "number") return false;
     return value.length >= min;
   },
 
@@ -99,9 +99,9 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
    * Check maximum string length
    */
   maxLength: (value: unknown, args?: Record<string, unknown>) => {
-    if (typeof value !== 'string') return false;
+    if (typeof value !== "string") return false;
     const max = args?.max;
-    if (typeof max !== 'number') return false;
+    if (typeof max !== "number") return false;
     return value.length <= max;
   },
 
@@ -109,9 +109,9 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
    * Check if string matches a regex pattern
    */
   pattern: (value: unknown, args?: Record<string, unknown>) => {
-    if (typeof value !== 'string') return false;
+    if (typeof value !== "string") return false;
     const pattern = args?.pattern;
-    if (typeof pattern !== 'string') return false;
+    if (typeof pattern !== "string") return false;
     try {
       return new RegExp(pattern).test(value);
     } catch {
@@ -123,9 +123,9 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
    * Check minimum numeric value
    */
   min: (value: unknown, args?: Record<string, unknown>) => {
-    if (typeof value !== 'number') return false;
+    if (typeof value !== "number") return false;
     const min = args?.min;
-    if (typeof min !== 'number') return false;
+    if (typeof min !== "number") return false;
     return value >= min;
   },
 
@@ -133,9 +133,9 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
    * Check maximum numeric value
    */
   max: (value: unknown, args?: Record<string, unknown>) => {
-    if (typeof value !== 'number') return false;
+    if (typeof value !== "number") return false;
     const max = args?.max;
-    if (typeof max !== 'number') return false;
+    if (typeof max !== "number") return false;
     return value <= max;
   },
 
@@ -143,8 +143,8 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
    * Check if value is a number
    */
   numeric: (value: unknown) => {
-    if (typeof value === 'number') return !isNaN(value);
-    if (typeof value === 'string') return !isNaN(parseFloat(value));
+    if (typeof value === "number") return !isNaN(value);
+    if (typeof value === "string") return !isNaN(parseFloat(value));
     return false;
   },
 
@@ -152,7 +152,7 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
    * Check if value is a valid URL
    */
   url: (value: unknown) => {
-    if (typeof value !== 'string') return false;
+    if (typeof value !== "string") return false;
     try {
       new URL(value);
       return true;
@@ -205,10 +205,10 @@ export interface ValidationContext {
  */
 export function runValidationCheck(
   check: ValidationCheck,
-  ctx: ValidationContext
+  ctx: ValidationContext,
 ): ValidationCheckResult {
   const { value, dataModel, customFunctions } = ctx;
-  
+
   // Resolve args
   const resolvedArgs: Record<string, unknown> = {};
   if (check.args) {
@@ -216,10 +216,11 @@ export function runValidationCheck(
       resolvedArgs[key] = resolveDynamicValue(argValue, dataModel);
     }
   }
-  
+
   // Find the validation function
-  const fn = builtInValidationFunctions[check.fn] ?? customFunctions?.[check.fn];
-  
+  const fn =
+    builtInValidationFunctions[check.fn] ?? customFunctions?.[check.fn];
+
   if (!fn) {
     console.warn(`Unknown validation function: ${check.fn}`);
     return {
@@ -228,9 +229,9 @@ export function runValidationCheck(
       message: check.message,
     };
   }
-  
+
   const valid = fn(value, resolvedArgs);
-  
+
   return {
     fn: check.fn,
     valid,
@@ -243,11 +244,11 @@ export function runValidationCheck(
  */
 export function runValidation(
   config: ValidationConfig,
-  ctx: ValidationContext & { authState?: { isSignedIn: boolean } }
+  ctx: ValidationContext & { authState?: { isSignedIn: boolean } },
 ): ValidationResult {
   const checks: ValidationCheckResult[] = [];
   const errors: string[] = [];
-  
+
   // Check if validation is enabled
   if (config.enabled) {
     const enabled = evaluateLogicExpression(config.enabled, {
@@ -258,7 +259,7 @@ export function runValidation(
       return { valid: true, errors: [], checks: [] };
     }
   }
-  
+
   // Run each check
   if (config.checks) {
     for (const check of config.checks) {
@@ -269,7 +270,7 @@ export function runValidation(
       }
     }
   }
-  
+
   return {
     valid: errors.length === 0,
     errors,
@@ -281,53 +282,56 @@ export function runValidation(
  * Helper to create validation checks
  */
 export const check = {
-  required: (message = 'This field is required'): ValidationCheck => ({
-    fn: 'required',
+  required: (message = "This field is required"): ValidationCheck => ({
+    fn: "required",
     message,
   }),
-  
-  email: (message = 'Invalid email address'): ValidationCheck => ({
-    fn: 'email',
+
+  email: (message = "Invalid email address"): ValidationCheck => ({
+    fn: "email",
     message,
   }),
-  
+
   minLength: (min: number, message?: string): ValidationCheck => ({
-    fn: 'minLength',
+    fn: "minLength",
     args: { min },
     message: message ?? `Must be at least ${min} characters`,
   }),
-  
+
   maxLength: (max: number, message?: string): ValidationCheck => ({
-    fn: 'maxLength',
+    fn: "maxLength",
     args: { max },
     message: message ?? `Must be at most ${max} characters`,
   }),
-  
-  pattern: (pattern: string, message = 'Invalid format'): ValidationCheck => ({
-    fn: 'pattern',
+
+  pattern: (pattern: string, message = "Invalid format"): ValidationCheck => ({
+    fn: "pattern",
     args: { pattern },
     message,
   }),
-  
+
   min: (min: number, message?: string): ValidationCheck => ({
-    fn: 'min',
+    fn: "min",
     args: { min },
     message: message ?? `Must be at least ${min}`,
   }),
-  
+
   max: (max: number, message?: string): ValidationCheck => ({
-    fn: 'max',
+    fn: "max",
     args: { max },
     message: message ?? `Must be at most ${max}`,
   }),
-  
-  url: (message = 'Invalid URL'): ValidationCheck => ({
-    fn: 'url',
+
+  url: (message = "Invalid URL"): ValidationCheck => ({
+    fn: "url",
     message,
   }),
-  
-  matches: (otherPath: string, message = 'Fields must match'): ValidationCheck => ({
-    fn: 'matches',
+
+  matches: (
+    otherPath: string,
+    message = "Fields must match",
+  ): ValidationCheck => ({
+    fn: "matches",
     args: { other: { path: otherPath } },
     message,
   }),

+ 119 - 66
packages/core/src/visibility.ts

@@ -1,12 +1,12 @@
-import { z } from 'zod';
+import { z } from "zod";
 import type {
   VisibilityCondition,
   LogicExpression,
   DataModel,
   AuthState,
   DynamicValue,
-} from './types';
-import { resolveDynamicValue, DynamicValueSchema } from './types';
+} from "./types";
+import { resolveDynamicValue, DynamicValueSchema } from "./types";
 
 // Dynamic value schema for comparisons (number-focused)
 const DynamicNumberValueSchema = z.union([
@@ -26,22 +26,31 @@ export const LogicExpressionSchema: z.ZodType<LogicExpression> = z.lazy(() =>
     z.object({ path: z.string() }),
     z.object({ eq: z.tuple([DynamicValueSchema, DynamicValueSchema]) }),
     z.object({ neq: z.tuple([DynamicValueSchema, DynamicValueSchema]) }),
-    z.object({ gt: z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema]) }),
-    z.object({ gte: z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema]) }),
-    z.object({ lt: z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema]) }),
-    z.object({ lte: z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema]) }),
-  ])
+    z.object({
+      gt: z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema]),
+    }),
+    z.object({
+      gte: z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema]),
+    }),
+    z.object({
+      lt: z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema]),
+    }),
+    z.object({
+      lte: z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema]),
+    }),
+  ]),
 ) as z.ZodType<LogicExpression>;
 
 /**
  * Visibility condition schema
  */
-export const VisibilityConditionSchema: z.ZodType<VisibilityCondition> = z.union([
-  z.boolean(),
-  z.object({ path: z.string() }),
-  z.object({ auth: z.enum(['signedIn', 'signedOut']) }),
-  LogicExpressionSchema,
-]);
+export const VisibilityConditionSchema: z.ZodType<VisibilityCondition> =
+  z.union([
+    z.boolean(),
+    z.object({ path: z.string() }),
+    z.object({ auth: z.enum(["signedIn", "signedOut"]) }),
+    LogicExpressionSchema,
+  ]);
 
 /**
  * Context for evaluating visibility
@@ -56,33 +65,33 @@ export interface VisibilityContext {
  */
 export function evaluateLogicExpression(
   expr: LogicExpression,
-  ctx: VisibilityContext
+  ctx: VisibilityContext,
 ): boolean {
   const { dataModel } = ctx;
 
   // AND expression
-  if ('and' in expr) {
+  if ("and" in expr) {
     return expr.and.every((subExpr) => evaluateLogicExpression(subExpr, ctx));
   }
 
   // OR expression
-  if ('or' in expr) {
+  if ("or" in expr) {
     return expr.or.some((subExpr) => evaluateLogicExpression(subExpr, ctx));
   }
 
   // NOT expression
-  if ('not' in expr) {
+  if ("not" in expr) {
     return !evaluateLogicExpression(expr.not, ctx);
   }
 
   // Path expression (resolve to boolean)
-  if ('path' in expr) {
+  if ("path" in expr) {
     const value = resolveDynamicValue({ path: expr.path }, dataModel);
     return Boolean(value);
   }
 
   // Equality comparison
-  if ('eq' in expr) {
+  if ("eq" in expr) {
     const [left, right] = expr.eq;
     const leftValue = resolveDynamicValue(left, dataModel);
     const rightValue = resolveDynamicValue(right, dataModel);
@@ -90,7 +99,7 @@ export function evaluateLogicExpression(
   }
 
   // Not equal comparison
-  if ('neq' in expr) {
+  if ("neq" in expr) {
     const [left, right] = expr.neq;
     const leftValue = resolveDynamicValue(left, dataModel);
     const rightValue = resolveDynamicValue(right, dataModel);
@@ -98,44 +107,68 @@ export function evaluateLogicExpression(
   }
 
   // Greater than
-  if ('gt' in expr) {
+  if ("gt" in expr) {
     const [left, right] = expr.gt;
-    const leftValue = resolveDynamicValue(left as DynamicValue<number>, dataModel);
-    const rightValue = resolveDynamicValue(right as DynamicValue<number>, dataModel);
-    if (typeof leftValue === 'number' && typeof rightValue === 'number') {
+    const leftValue = resolveDynamicValue(
+      left as DynamicValue<number>,
+      dataModel,
+    );
+    const rightValue = resolveDynamicValue(
+      right as DynamicValue<number>,
+      dataModel,
+    );
+    if (typeof leftValue === "number" && typeof rightValue === "number") {
       return leftValue > rightValue;
     }
     return false;
   }
 
   // Greater than or equal
-  if ('gte' in expr) {
+  if ("gte" in expr) {
     const [left, right] = expr.gte;
-    const leftValue = resolveDynamicValue(left as DynamicValue<number>, dataModel);
-    const rightValue = resolveDynamicValue(right as DynamicValue<number>, dataModel);
-    if (typeof leftValue === 'number' && typeof rightValue === 'number') {
+    const leftValue = resolveDynamicValue(
+      left as DynamicValue<number>,
+      dataModel,
+    );
+    const rightValue = resolveDynamicValue(
+      right as DynamicValue<number>,
+      dataModel,
+    );
+    if (typeof leftValue === "number" && typeof rightValue === "number") {
       return leftValue >= rightValue;
     }
     return false;
   }
 
   // Less than
-  if ('lt' in expr) {
+  if ("lt" in expr) {
     const [left, right] = expr.lt;
-    const leftValue = resolveDynamicValue(left as DynamicValue<number>, dataModel);
-    const rightValue = resolveDynamicValue(right as DynamicValue<number>, dataModel);
-    if (typeof leftValue === 'number' && typeof rightValue === 'number') {
+    const leftValue = resolveDynamicValue(
+      left as DynamicValue<number>,
+      dataModel,
+    );
+    const rightValue = resolveDynamicValue(
+      right as DynamicValue<number>,
+      dataModel,
+    );
+    if (typeof leftValue === "number" && typeof rightValue === "number") {
       return leftValue < rightValue;
     }
     return false;
   }
 
   // Less than or equal
-  if ('lte' in expr) {
+  if ("lte" in expr) {
     const [left, right] = expr.lte;
-    const leftValue = resolveDynamicValue(left as DynamicValue<number>, dataModel);
-    const rightValue = resolveDynamicValue(right as DynamicValue<number>, dataModel);
-    if (typeof leftValue === 'number' && typeof rightValue === 'number') {
+    const leftValue = resolveDynamicValue(
+      left as DynamicValue<number>,
+      dataModel,
+    );
+    const rightValue = resolveDynamicValue(
+      right as DynamicValue<number>,
+      dataModel,
+    );
+    if (typeof leftValue === "number" && typeof rightValue === "number") {
       return leftValue <= rightValue;
     }
     return false;
@@ -149,7 +182,7 @@ export function evaluateLogicExpression(
  */
 export function evaluateVisibility(
   condition: VisibilityCondition | undefined,
-  ctx: VisibilityContext
+  ctx: VisibilityContext,
 ): boolean {
   // No condition = visible
   if (condition === undefined) {
@@ -157,23 +190,23 @@ export function evaluateVisibility(
   }
 
   // Boolean literal
-  if (typeof condition === 'boolean') {
+  if (typeof condition === "boolean") {
     return condition;
   }
 
   // Path reference
-  if ('path' in condition && !('and' in condition) && !('or' in condition)) {
+  if ("path" in condition && !("and" in condition) && !("or" in condition)) {
     const value = resolveDynamicValue({ path: condition.path }, ctx.dataModel);
     return Boolean(value);
   }
 
   // Auth condition
-  if ('auth' in condition) {
+  if ("auth" in condition) {
     const isSignedIn = ctx.authState?.isSignedIn ?? false;
-    if (condition.auth === 'signedIn') {
+    if (condition.auth === "signedIn") {
       return isSignedIn;
     }
-    if (condition.auth === 'signedOut') {
+    if (condition.auth === "signedOut") {
       return !isSignedIn;
     }
     return false;
@@ -189,43 +222,63 @@ export function evaluateVisibility(
 export const visibility = {
   /** Always visible */
   always: true as const,
-  
+
   /** Never visible */
   never: false as const,
-  
+
   /** Visible when path is truthy */
   when: (path: string): VisibilityCondition => ({ path }),
-  
+
   /** Visible when signed in */
-  signedIn: { auth: 'signedIn' } as const,
-  
+  signedIn: { auth: "signedIn" } as const,
+
   /** Visible when signed out */
-  signedOut: { auth: 'signedOut' } as const,
-  
+  signedOut: { auth: "signedOut" } as const,
+
   /** AND multiple conditions */
-  and: (...conditions: LogicExpression[]): LogicExpression => ({ and: conditions }),
-  
+  and: (...conditions: LogicExpression[]): LogicExpression => ({
+    and: conditions,
+  }),
+
   /** OR multiple conditions */
-  or: (...conditions: LogicExpression[]): LogicExpression => ({ or: conditions }),
-  
+  or: (...conditions: LogicExpression[]): LogicExpression => ({
+    or: conditions,
+  }),
+
   /** NOT a condition */
   not: (condition: LogicExpression): LogicExpression => ({ not: condition }),
-  
+
   /** Equality check */
-  eq: (left: DynamicValue, right: DynamicValue): LogicExpression => ({ eq: [left, right] }),
-  
+  eq: (left: DynamicValue, right: DynamicValue): LogicExpression => ({
+    eq: [left, right],
+  }),
+
   /** Not equal check */
-  neq: (left: DynamicValue, right: DynamicValue): LogicExpression => ({ neq: [left, right] }),
-  
+  neq: (left: DynamicValue, right: DynamicValue): LogicExpression => ({
+    neq: [left, right],
+  }),
+
   /** Greater than */
-  gt: (left: DynamicValue<number>, right: DynamicValue<number>): LogicExpression => ({ gt: [left, right] }),
-  
+  gt: (
+    left: DynamicValue<number>,
+    right: DynamicValue<number>,
+  ): LogicExpression => ({ gt: [left, right] }),
+
   /** Greater than or equal */
-  gte: (left: DynamicValue<number>, right: DynamicValue<number>): LogicExpression => ({ gte: [left, right] }),
-  
+  gte: (
+    left: DynamicValue<number>,
+    right: DynamicValue<number>,
+  ): LogicExpression => ({ gte: [left, right] }),
+
   /** Less than */
-  lt: (left: DynamicValue<number>, right: DynamicValue<number>): LogicExpression => ({ lt: [left, right] }),
-  
+  lt: (
+    left: DynamicValue<number>,
+    right: DynamicValue<number>,
+  ): LogicExpression => ({ lt: [left, right] }),
+
   /** Less than or equal */
-  lte: (left: DynamicValue<number>, right: DynamicValue<number>): LogicExpression => ({ lte: [left, right] }),
+  lte: (
+    left: DynamicValue<number>,
+    right: DynamicValue<number>,
+  ): LogicExpression => ({ lte: [left, right] }),
 };

+ 4 - 4
packages/core/tsup.config.ts

@@ -1,10 +1,10 @@
-import { defineConfig } from 'tsup';
+import { defineConfig } from "tsup";
 
 export default defineConfig({
-  entry: ['src/index.ts'],
-  format: ['cjs', 'esm'],
+  entry: ["src/index.ts"],
+  format: ["cjs", "esm"],
   dts: true,
   sourcemap: true,
   clean: true,
-  external: ['zod'],
+  external: ["zod"],
 });

+ 63 - 46
packages/react/src/contexts/actions.tsx

@@ -1,4 +1,4 @@
-'use client';
+"use client";
 
 import React, {
   createContext,
@@ -7,7 +7,7 @@ import React, {
   useCallback,
   useMemo,
   type ReactNode,
-} from 'react';
+} from "react";
 import {
   resolveAction,
   executeAction,
@@ -15,8 +15,8 @@ import {
   type ActionHandler,
   type ActionConfirm,
   type ResolvedAction,
-} from '@json-render/core';
-import { useData } from './data';
+} from "@json-render/core";
+import { useData } from "./data";
 
 /**
  * Pending confirmation state
@@ -74,13 +74,18 @@ export function ActionProvider({
   children,
 }: ActionProviderProps) {
   const { data, set } = useData();
-  const [handlers, setHandlers] = useState<Record<string, ActionHandler>>(initialHandlers);
+  const [handlers, setHandlers] =
+    useState<Record<string, ActionHandler>>(initialHandlers);
   const [loadingActions, setLoadingActions] = useState<Set<string>>(new Set());
-  const [pendingConfirmation, setPendingConfirmation] = useState<PendingConfirmation | null>(null);
+  const [pendingConfirmation, setPendingConfirmation] =
+    useState<PendingConfirmation | null>(null);
 
-  const registerHandler = useCallback((name: string, handler: ActionHandler) => {
-    setHandlers((prev) => ({ ...prev, [name]: handler }));
-  }, []);
+  const registerHandler = useCallback(
+    (name: string, handler: ActionHandler) => {
+      setHandlers((prev) => ({ ...prev, [name]: handler }));
+    },
+    [],
+  );
 
   const execute = useCallback(
     async (action: Action) => {
@@ -104,7 +109,7 @@ export function ActionProvider({
             },
             reject: () => {
               setPendingConfirmation(null);
-              reject(new Error('Action cancelled'));
+              reject(new Error("Action cancelled"));
             },
           });
         }).then(async () => {
@@ -151,7 +156,7 @@ export function ActionProvider({
         });
       }
     },
-    [data, handlers, set, navigate]
+    [data, handlers, set, navigate],
   );
 
   const confirm = useCallback(() => {
@@ -172,7 +177,15 @@ export function ActionProvider({
       cancel,
       registerHandler,
     }),
-    [handlers, loadingActions, pendingConfirmation, execute, confirm, cancel, registerHandler]
+    [
+      handlers,
+      loadingActions,
+      pendingConfirmation,
+      execute,
+      confirm,
+      cancel,
+      registerHandler,
+    ],
   );
 
   return (
@@ -186,7 +199,7 @@ export function ActionProvider({
 export function useActions(): ActionContextValue {
   const ctx = useContext(ActionContext);
   if (!ctx) {
-    throw new Error('useActions must be used within an ActionProvider');
+    throw new Error("useActions must be used within an ActionProvider");
   }
   return ctx;
 }
@@ -221,37 +234,41 @@ export interface ConfirmDialogProps {
 /**
  * Default confirmation dialog component
  */
-export function ConfirmDialog({ confirm, onConfirm, onCancel }: ConfirmDialogProps) {
-  const isDanger = confirm.variant === 'danger';
+export function ConfirmDialog({
+  confirm,
+  onConfirm,
+  onCancel,
+}: ConfirmDialogProps) {
+  const isDanger = confirm.variant === "danger";
 
   return (
     <div
       style={{
-        position: 'fixed',
+        position: "fixed",
         inset: 0,
-        backgroundColor: 'rgba(0, 0, 0, 0.5)',
-        display: 'flex',
-        alignItems: 'center',
-        justifyContent: 'center',
+        backgroundColor: "rgba(0, 0, 0, 0.5)",
+        display: "flex",
+        alignItems: "center",
+        justifyContent: "center",
         zIndex: 50,
       }}
       onClick={onCancel}
     >
       <div
         style={{
-          backgroundColor: 'white',
-          borderRadius: '8px',
-          padding: '24px',
-          maxWidth: '400px',
-          width: '100%',
-          boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1)',
+          backgroundColor: "white",
+          borderRadius: "8px",
+          padding: "24px",
+          maxWidth: "400px",
+          width: "100%",
+          boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1)",
         }}
         onClick={(e) => e.stopPropagation()}
       >
         <h3
           style={{
-            margin: '0 0 8px 0',
-            fontSize: '18px',
+            margin: "0 0 8px 0",
+            fontSize: "18px",
             fontWeight: 600,
           }}
         >
@@ -259,43 +276,43 @@ export function ConfirmDialog({ confirm, onConfirm, onCancel }: ConfirmDialogPro
         </h3>
         <p
           style={{
-            margin: '0 0 24px 0',
-            color: '#6b7280',
+            margin: "0 0 24px 0",
+            color: "#6b7280",
           }}
         >
           {confirm.message}
         </p>
         <div
           style={{
-            display: 'flex',
-            gap: '12px',
-            justifyContent: 'flex-end',
+            display: "flex",
+            gap: "12px",
+            justifyContent: "flex-end",
           }}
         >
           <button
             onClick={onCancel}
             style={{
-              padding: '8px 16px',
-              borderRadius: '6px',
-              border: '1px solid #d1d5db',
-              backgroundColor: 'white',
-              cursor: 'pointer',
+              padding: "8px 16px",
+              borderRadius: "6px",
+              border: "1px solid #d1d5db",
+              backgroundColor: "white",
+              cursor: "pointer",
             }}
           >
-            {confirm.cancelLabel ?? 'Cancel'}
+            {confirm.cancelLabel ?? "Cancel"}
           </button>
           <button
             onClick={onConfirm}
             style={{
-              padding: '8px 16px',
-              borderRadius: '6px',
-              border: 'none',
-              backgroundColor: isDanger ? '#dc2626' : '#3b82f6',
-              color: 'white',
-              cursor: 'pointer',
+              padding: "8px 16px",
+              borderRadius: "6px",
+              border: "none",
+              backgroundColor: isDanger ? "#dc2626" : "#3b82f6",
+              color: "white",
+              cursor: "pointer",
             }}
           >
-            {confirm.confirmLabel ?? 'Confirm'}
+            {confirm.confirmLabel ?? "Confirm"}
           </button>
         </div>
       </div>

+ 10 - 13
packages/react/src/contexts/data.tsx

@@ -1,4 +1,4 @@
-'use client';
+"use client";
 
 import React, {
   createContext,
@@ -7,13 +7,13 @@ import React, {
   useCallback,
   useMemo,
   type ReactNode,
-} from 'react';
+} from "react";
 import {
   getByPath,
   setByPath,
   type DataModel,
   type AuthState,
-} from '@json-render/core';
+} from "@json-render/core";
 
 /**
  * Data context value
@@ -57,10 +57,7 @@ export function DataProvider({
 }: DataProviderProps) {
   const [data, setData] = useState<DataModel>(initialData);
 
-  const get = useCallback(
-    (path: string) => getByPath(data, path),
-    [data]
-  );
+  const get = useCallback((path: string) => getByPath(data, path), [data]);
 
   const set = useCallback(
     (path: string, value: unknown) => {
@@ -71,7 +68,7 @@ export function DataProvider({
       });
       onDataChange?.(path, value);
     },
-    [onDataChange]
+    [onDataChange],
   );
 
   const update = useCallback(
@@ -85,7 +82,7 @@ export function DataProvider({
         return next;
       });
     },
-    [onDataChange]
+    [onDataChange],
   );
 
   const value = useMemo<DataContextValue>(
@@ -96,7 +93,7 @@ export function DataProvider({
       set,
       update,
     }),
-    [data, authState, get, set, update]
+    [data, authState, get, set, update],
   );
 
   return <DataContext.Provider value={value}>{children}</DataContext.Provider>;
@@ -108,7 +105,7 @@ export function DataProvider({
 export function useData(): DataContextValue {
   const ctx = useContext(DataContext);
   if (!ctx) {
-    throw new Error('useData must be used within a DataProvider');
+    throw new Error("useData must be used within a DataProvider");
   }
   return ctx;
 }
@@ -125,13 +122,13 @@ export function useDataValue<T>(path: string): T | undefined {
  * Hook to get and set a value from the data model (like useState)
  */
 export function useDataBinding<T>(
-  path: string
+  path: string,
 ): [T | undefined, (value: T) => void] {
   const { get, set } = useData();
   const value = get(path) as T | undefined;
   const setValue = useCallback(
     (newValue: T) => set(path, newValue),
-    [path, set]
+    [path, set],
   );
   return [value, setValue];
 }

+ 31 - 16
packages/react/src/contexts/validation.tsx

@@ -1,4 +1,4 @@
-'use client';
+"use client";
 
 import React, {
   createContext,
@@ -7,14 +7,14 @@ import React, {
   useCallback,
   useMemo,
   type ReactNode,
-} from 'react';
+} from "react";
 import {
   runValidation,
   type ValidationConfig,
   type ValidationFunction,
   type ValidationResult,
-} from '@json-render/core';
-import { useData } from './data';
+} from "@json-render/core";
+import { useData } from "./data";
 
 /**
  * Field validation state
@@ -67,16 +67,23 @@ export function ValidationProvider({
   children,
 }: ValidationProviderProps) {
   const { data, authState } = useData();
-  const [fieldStates, setFieldStates] = useState<Record<string, FieldValidationState>>({});
-  const [fieldConfigs, setFieldConfigs] = useState<Record<string, ValidationConfig>>({});
-
-  const registerField = useCallback((path: string, config: ValidationConfig) => {
-    setFieldConfigs((prev) => ({ ...prev, [path]: config }));
-  }, []);
+  const [fieldStates, setFieldStates] = useState<
+    Record<string, FieldValidationState>
+  >({});
+  const [fieldConfigs, setFieldConfigs] = useState<
+    Record<string, ValidationConfig>
+  >({});
+
+  const registerField = useCallback(
+    (path: string, config: ValidationConfig) => {
+      setFieldConfigs((prev) => ({ ...prev, [path]: config }));
+    },
+    [],
+  );
 
   const validate = useCallback(
     (path: string, config: ValidationConfig): ValidationResult => {
-      const value = data[path.split('/').filter(Boolean).join('.')];
+      const value = data[path.split("/").filter(Boolean).join(".")];
       const result = runValidation(config, {
         value,
         dataModel: data,
@@ -95,7 +102,7 @@ export function ValidationProvider({
 
       return result;
     },
-    [data, customFunctions, authState]
+    [data, customFunctions, authState],
   );
 
   const touch = useCallback((path: string) => {
@@ -140,7 +147,15 @@ export function ValidationProvider({
       validateAll,
       registerField,
     }),
-    [customFunctions, fieldStates, validate, touch, clear, validateAll, registerField]
+    [
+      customFunctions,
+      fieldStates,
+      validate,
+      touch,
+      clear,
+      validateAll,
+      registerField,
+    ],
   );
 
   return (
@@ -156,7 +171,7 @@ export function ValidationProvider({
 export function useValidation(): ValidationContextValue {
   const ctx = useContext(ValidationContext);
   if (!ctx) {
-    throw new Error('useValidation must be used within a ValidationProvider');
+    throw new Error("useValidation must be used within a ValidationProvider");
   }
   return ctx;
 }
@@ -166,7 +181,7 @@ export function useValidation(): ValidationContextValue {
  */
 export function useFieldValidation(
   path: string,
-  config?: ValidationConfig
+  config?: ValidationConfig,
 ): {
   state: FieldValidationState;
   validate: () => ValidationResult;
@@ -198,7 +213,7 @@ export function useFieldValidation(
 
   const validate = useCallback(
     () => validateField(path, config ?? { checks: [] }),
-    [path, config, validateField]
+    [path, config, validateField],
   );
 
   const touch = useCallback(() => touchField(path), [path, touchField]);

+ 16 - 9
packages/react/src/contexts/visibility.tsx

@@ -1,12 +1,17 @@
-'use client';
+"use client";
 
-import React, { createContext, useContext, useMemo, type ReactNode } from 'react';
+import React, {
+  createContext,
+  useContext,
+  useMemo,
+  type ReactNode,
+} from "react";
 import {
   evaluateVisibility,
   type VisibilityCondition,
   type VisibilityContext as CoreVisibilityContext,
-} from '@json-render/core';
-import { useData } from './data';
+} from "@json-render/core";
+import { useData } from "./data";
 
 /**
  * Visibility context value
@@ -38,18 +43,18 @@ export function VisibilityProvider({ children }: VisibilityProviderProps) {
       dataModel: data,
       authState,
     }),
-    [data, authState]
+    [data, authState],
   );
 
   const isVisible = useMemo(
     () => (condition: VisibilityCondition | undefined) =>
       evaluateVisibility(condition, ctx),
-    [ctx]
+    [ctx],
   );
 
   const value = useMemo<VisibilityContextValue>(
     () => ({ isVisible, ctx }),
-    [isVisible, ctx]
+    [isVisible, ctx],
   );
 
   return (
@@ -65,7 +70,7 @@ export function VisibilityProvider({ children }: VisibilityProviderProps) {
 export function useVisibility(): VisibilityContextValue {
   const ctx = useContext(VisibilityContext);
   if (!ctx) {
-    throw new Error('useVisibility must be used within a VisibilityProvider');
+    throw new Error("useVisibility must be used within a VisibilityProvider");
   }
   return ctx;
 }
@@ -73,7 +78,9 @@ export function useVisibility(): VisibilityContextValue {
 /**
  * Hook to check if a condition is visible
  */
-export function useIsVisible(condition: VisibilityCondition | undefined): boolean {
+export function useIsVisible(
+  condition: VisibilityCondition | undefined,
+): boolean {
   const { isVisible } = useVisibility();
   return isVisible(condition);
 }

+ 31 - 27
packages/react/src/hooks.ts

@@ -1,8 +1,8 @@
-'use client';
+"use client";
 
-import { useState, useCallback, useRef, useEffect } from 'react';
-import type { UITree, UIElement, JsonPatch } from '@json-render/core';
-import { setByPath } from '@json-render/core';
+import { useState, useCallback, useRef, useEffect } from "react";
+import type { UITree, UIElement, JsonPatch } from "@json-render/core";
+import { setByPath } from "@json-render/core";
 
 /**
  * Parse a single JSON patch line
@@ -10,7 +10,7 @@ import { setByPath } from '@json-render/core';
 function parsePatchLine(line: string): JsonPatch | null {
   try {
     const trimmed = line.trim();
-    if (!trimmed || trimmed.startsWith('//')) {
+    if (!trimmed || trimmed.startsWith("//")) {
       return null;
     }
     return JSON.parse(trimmed) as JsonPatch;
@@ -26,18 +26,18 @@ function applyPatch(tree: UITree, patch: JsonPatch): UITree {
   const newTree = { ...tree, elements: { ...tree.elements } };
 
   switch (patch.op) {
-    case 'set':
-    case 'add':
-    case 'replace': {
+    case "set":
+    case "add":
+    case "replace": {
       // Handle root path
-      if (patch.path === '/root') {
+      if (patch.path === "/root") {
         newTree.root = patch.value as string;
         return newTree;
       }
 
       // Handle elements paths
-      if (patch.path.startsWith('/elements/')) {
-        const pathParts = patch.path.slice('/elements/'.length).split('/');
+      if (patch.path.startsWith("/elements/")) {
+        const pathParts = patch.path.slice("/elements/".length).split("/");
         const elementKey = pathParts[0];
 
         if (!elementKey) return newTree;
@@ -49,18 +49,22 @@ function applyPatch(tree: UITree, patch: JsonPatch): UITree {
           // Setting property of element
           const element = newTree.elements[elementKey];
           if (element) {
-            const propPath = '/' + pathParts.slice(1).join('/');
+            const propPath = "/" + pathParts.slice(1).join("/");
             const newElement = { ...element };
-            setByPath(newElement as unknown as Record<string, unknown>, propPath, patch.value);
+            setByPath(
+              newElement as unknown as Record<string, unknown>,
+              propPath,
+              patch.value,
+            );
             newTree.elements[elementKey] = newElement;
           }
         }
       }
       break;
     }
-    case 'remove': {
-      if (patch.path.startsWith('/elements/')) {
-        const elementKey = patch.path.slice('/elements/'.length).split('/')[0];
+    case "remove": {
+      if (patch.path.startsWith("/elements/")) {
+        const elementKey = patch.path.slice("/elements/".length).split("/")[0];
         if (elementKey) {
           const { [elementKey]: _, ...rest } = newTree.elements;
           newTree.elements = rest;
@@ -129,13 +133,13 @@ export function useUIStream({
       setError(null);
 
       // Start with an empty tree
-      let currentTree: UITree = { root: '', elements: {} };
+      let currentTree: UITree = { root: "", elements: {} };
       setTree(currentTree);
 
       try {
         const response = await fetch(api, {
-          method: 'POST',
-          headers: { 'Content-Type': 'application/json' },
+          method: "POST",
+          headers: { "Content-Type": "application/json" },
           body: JSON.stringify({
             prompt,
             context,
@@ -150,11 +154,11 @@ export function useUIStream({
 
         const reader = response.body?.getReader();
         if (!reader) {
-          throw new Error('No response body');
+          throw new Error("No response body");
         }
 
         const decoder = new TextDecoder();
-        let buffer = '';
+        let buffer = "";
 
         while (true) {
           const { done, value } = await reader.read();
@@ -163,8 +167,8 @@ export function useUIStream({
           buffer += decoder.decode(value, { stream: true });
 
           // Process complete lines
-          const lines = buffer.split('\n');
-          buffer = lines.pop() ?? '';
+          const lines = buffer.split("\n");
+          buffer = lines.pop() ?? "";
 
           for (const line of lines) {
             const patch = parsePatchLine(line);
@@ -186,7 +190,7 @@ export function useUIStream({
 
         onComplete?.(currentTree);
       } catch (err) {
-        if ((err as Error).name === 'AbortError') {
+        if ((err as Error).name === "AbortError") {
           return;
         }
         const error = err instanceof Error ? err : new Error(String(err));
@@ -196,7 +200,7 @@ export function useUIStream({
         setIsStreaming(false);
       }
     },
-    [api, onComplete, onError]
+    [api, onComplete, onError],
   );
 
   // Cleanup on unmount
@@ -219,10 +223,10 @@ export function useUIStream({
  * Convert a flat element list to a UITree
  */
 export function flatToTree(
-  elements: Array<UIElement & { parentKey?: string | null }>
+  elements: Array<UIElement & { parentKey?: string | null }>,
 ): UITree {
   const elementMap: Record<string, UIElement> = {};
-  let root = '';
+  let root = "";
 
   // First pass: add all elements to map
   for (const element of elements) {

+ 6 - 6
packages/react/src/index.ts

@@ -6,7 +6,7 @@ export {
   useDataBinding,
   type DataContextValue,
   type DataProviderProps,
-} from './contexts/data';
+} from "./contexts/data";
 
 export {
   VisibilityProvider,
@@ -14,7 +14,7 @@ export {
   useIsVisible,
   type VisibilityContextValue,
   type VisibilityProviderProps,
-} from './contexts/visibility';
+} from "./contexts/visibility";
 
 export {
   ActionProvider,
@@ -25,7 +25,7 @@ export {
   type ActionProviderProps,
   type PendingConfirmation,
   type ConfirmDialogProps,
-} from './contexts/actions';
+} from "./contexts/actions";
 
 export {
   ValidationProvider,
@@ -34,7 +34,7 @@ export {
   type ValidationContextValue,
   type ValidationProviderProps,
   type FieldValidationState,
-} from './contexts/validation';
+} from "./contexts/validation";
 
 // Renderer
 export {
@@ -46,7 +46,7 @@ export {
   type ComponentRegistry,
   type RendererProps,
   type JSONUIProviderProps,
-} from './renderer';
+} from "./renderer";
 
 // Hooks
 export {
@@ -54,4 +54,4 @@ export {
   flatToTree,
   type UseUIStreamOptions,
   type UseUIStreamReturn,
-} from './hooks';
+} from "./hooks";

+ 34 - 24
packages/react/src/renderer.tsx

@@ -1,10 +1,16 @@
-'use client';
-
-import React, { type ComponentType, type ReactNode, useMemo } from 'react';
-import type { UIElement, UITree, Action, Catalog, ComponentDefinition } from '@json-render/core';
-import { useIsVisible } from './contexts/visibility';
-import { useActions } from './contexts/actions';
-import { useData } from './contexts/data';
+"use client";
+
+import React, { type ComponentType, type ReactNode, useMemo } from "react";
+import type {
+  UIElement,
+  UITree,
+  Action,
+  Catalog,
+  ComponentDefinition,
+} from "@json-render/core";
+import { useIsVisible } from "./contexts/visibility";
+import { useActions } from "./contexts/actions";
+import { useData } from "./contexts/data";
 
 /**
  * Props passed to component renderers
@@ -23,7 +29,9 @@ export interface ComponentRenderProps<P = Record<string, unknown>> {
 /**
  * Component renderer type
  */
-export type ComponentRenderer<P = Record<string, unknown>> = ComponentType<ComponentRenderProps<P>>;
+export type ComponentRenderer<P = Record<string, unknown>> = ComponentType<
+  ComponentRenderProps<P>
+>;
 
 /**
  * Registry of component renderers
@@ -95,11 +103,7 @@ function ElementRenderer({
   });
 
   return (
-    <Component
-      element={element}
-      onAction={execute}
-      loading={loading}
-    >
+    <Component element={element} onAction={execute} loading={loading}>
       {children}
     </Component>
   );
@@ -140,22 +144,28 @@ export interface JSONUIProviderProps {
   /** Auth state */
   authState?: { isSignedIn: boolean; user?: Record<string, unknown> };
   /** Action handlers */
-  actionHandlers?: Record<string, (params: Record<string, unknown>) => Promise<unknown> | unknown>;
+  actionHandlers?: Record<
+    string,
+    (params: Record<string, unknown>) => Promise<unknown> | unknown
+  >;
   /** Navigation function */
   navigate?: (path: string) => void;
   /** Custom validation functions */
-  validationFunctions?: Record<string, (value: unknown, args?: Record<string, unknown>) => boolean>;
+  validationFunctions?: Record<
+    string,
+    (value: unknown, args?: Record<string, unknown>) => boolean
+  >;
   /** Callback when data changes */
   onDataChange?: (path: string, value: unknown) => void;
   children: ReactNode;
 }
 
 // Import the providers
-import { DataProvider } from './contexts/data';
-import { VisibilityProvider } from './contexts/visibility';
-import { ActionProvider } from './contexts/actions';
-import { ValidationProvider } from './contexts/validation';
-import { ConfirmDialog } from './contexts/actions';
+import { DataProvider } from "./contexts/data";
+import { VisibilityProvider } from "./contexts/visibility";
+import { ActionProvider } from "./contexts/actions";
+import { ValidationProvider } from "./contexts/validation";
+import { ConfirmDialog } from "./contexts/actions";
 
 /**
  * Combined provider for all JSONUI contexts
@@ -211,12 +221,12 @@ function ConfirmationDialogManager() {
  * Helper to create a renderer component from a catalog
  */
 export function createRendererFromCatalog<
-  C extends Catalog<Record<string, ComponentDefinition>>
+  C extends Catalog<Record<string, ComponentDefinition>>,
 >(
   _catalog: C,
-  registry: ComponentRegistry
-): ComponentType<Omit<RendererProps, 'registry'>> {
-  return function CatalogRenderer(props: Omit<RendererProps, 'registry'>) {
+  registry: ComponentRegistry,
+): ComponentType<Omit<RendererProps, "registry">> {
+  return function CatalogRenderer(props: Omit<RendererProps, "registry">) {
     return <Renderer {...props} registry={registry} />;
   };
 }

+ 4 - 4
packages/react/tsup.config.ts

@@ -1,10 +1,10 @@
-import { defineConfig } from 'tsup';
+import { defineConfig } from "tsup";
 
 export default defineConfig({
-  entry: ['src/index.ts'],
-  format: ['cjs', 'esm'],
+  entry: ["src/index.ts"],
+  format: ["cjs", "esm"],
   dts: true,
   sourcemap: true,
   clean: true,
-  external: ['react', 'react-dom', '@json-render/core'],
+  external: ["react", "react-dom", "@json-render/core"],
 });