🍍 PineUI Documentation

About PineUI

PineUI is a Server-Driven UI framework developed by David Ruiz (wupsbr) in Rio de Janeiro and SΓ£o Paulo, Brazil.

Try it out: See PineUI in action with our interactive demos

Why "PineUI"? 🍍

In Brazilian Portuguese, we have an expression: "descascar esse abacaxi" (literally "peeling this pineapple"), which means solving a tough problem. Any developer who has worked on real-world projects knows the challenges:

  • Building new systems from scratch while maintaining legacy ones
  • Keeping multiple platforms (Web, iOS, Android) in sync
  • Iterating quickly without breaking production
  • Modernizing old codebases without complete rewrites
  • Enabling non-technical teams to experiment and innovate

These are the "pineapples" (tough problems) that PineUI helps you solve. The name is a playful reminder that software development is challenging β€” but with the right tools, you can peel through those challenges smoothly.

The Vision

PineUI was born from the need to simplify the development of dynamic, cross-platform projects in a world increasingly driven by Artificial Intelligence. In an era where Large Language Models (LLMs) are becoming capable of generating and modifying user interfaces, we needed a solution that enables:

  • Full Declarative: 100% JSON-defined interfaces with zero imperative code
  • Cross-Platform: Single schema works on Web (React) and Mobile (Flutter)
  • AI-Friendly: LLMs can easily generate and modify PineUI schemas without deep framework knowledge
  • Server-Driven: Update UIs without redeployment, perfect for A/B testing and dynamic content
  • Productivity: Faster development with ready-to-use, reusable components

Why PineUI?

In a world where applications need to be increasingly dynamic and personalized, PineUI offers an elegant solution: separate presentation logic (UI) from business logic (backend), allowing your server to completely control how the interface is rendered.

This is especially powerful in AI-native applications, where different users can have completely personalized experiences based on context, preferences, and even generated in real-time by language models.

Impact & Innovation Acceleration

PineUI is more than a framework β€” it's a paradigm shift in how we build software. In an era where companies need to innovate faster than ever, PineUI enables:

πŸš€ Accelerating New Solutions

Build entirely new products and experiences 5-10x faster by letting LLMs generate UI schemas. Instead of spending weeks designing and implementing UI components across multiple platforms, describe what you want in natural language, and let AI generate the PineUI schema. The same schema instantly works on Web, Mobile, and any future platform.

πŸ”„ Modernizing Legacy Systems

One of PineUI's most powerful use cases is accelerating innovation within legacy systems. Instead of rewriting your entire backend, expose your legacy APIs and let PineUI provide a modern, dynamic frontend. This hybrid approach allows you to:

  • Keep your battle-tested business logic intact
  • Deliver modern, responsive UIs without full rewrites
  • Iterate on user experience independently from backend changes
  • Gradually modernize systems without big-bang migrations
  • Test new features with A/B testing before committing to backend changes

πŸ€– LLM-Powered Development

PineUI is designed to be the perfect bridge between human intent and machine execution. LLMs can:

  • Generate complete UI schemas from natural language descriptions
  • Modify existing schemas based on user feedback
  • Create personalized experiences for each user based on context
  • A/B test variations without developer intervention
  • Adapt interfaces in real-time based on user behavior

πŸ’Ό Real Business Value

Companies using Server-Driven UI approaches have reported:

  • 80% reduction in time-to-market for new features
  • 90% decrease in mobile app deployment cycles
  • 5-10x increase in A/B testing velocity
  • Zero downtime for UI updates and experiments
  • Unified codebase across all platforms

πŸŒ‰ Bridging Old and New

The real innovation isn't just building new things β€” it's making existing systems better, faster. PineUI enables companies to:

  • Add modern mobile apps to legacy web systems without backend changes
  • Create dynamic dashboards on top of old APIs
  • Modernize customer-facing experiences while preserving core business logic
  • Experiment with new UX patterns without engineering bottlenecks
  • Enable product teams to iterate independently from engineering cycles

πŸ’‘ Real-World Example

Imagine a 20-year-old banking system with stable, audited business logic but outdated UI. Instead of a risky multi-year rewrite, you can:

  1. Keep the core banking APIs untouched
  2. Build a PineUI schema that consumes those APIs
  3. Deploy a modern React web app + Flutter mobile app in weeks
  4. Let LLMs generate personalized dashboards for different user segments
  5. A/B test new features by just changing JSON on the server
  6. Roll out updates instantly without app store deployments

This is the power of Server-Driven UI combined with AI: innovation without disruption.

About the Creator

David Ruiz (wupsbr) is a seasoned technology leader with over 20 years of experience building and scaling high-performance teams across engineering, product, data, design, infrastructure, and security.

Current Role: Chief Product and Technology Officer (CPTO) at Ingresse, one of Brazil's largest ticketing and event platforms, leading Product, Engineering, Data, Security, Infrastructure, and Design teams. Currently building Ingresse's new global platform from scratch using generative AI, while managing operational stability, cost efficiency, cybersecurity maturity, and international expansion.

Previous Experience:

  • iFood: Director of Engineering, created and scaled iFood BenefΓ­cios and iFood Pago, ensuring financial stability for operations exceeding R$70 billion in GMV annually
  • ParanΓ‘ Banco: CTO, led large-scale digital transformation and platform modernization
  • Elo (CartΓ£o Elo): Superintendent, conducted enterprise-wide digital transformations and risk mitigation strategies

Entrepreneurship: Co-founder of ONOVOLAB, one of Brazil's leading innovation hubs. Transformed a 21,000mΒ² former textile factory in SΓ£o Carlos into a vibrant technology and entrepreneurship hub. ONOVOLAB has hosted visitors from 23 countries, was featured in Forbes, appeared on Nasdaq's billboard, and attracted companies like Santander/F1RST, iFood, and LuizaLabs, while housing dozens of growing startups.

πŸš€ Built for Real Innovation

"I believe the most powerful technology is that which brings people together, accelerates ideas, and promotes meaningful transformation. I've been using generative AI (OpenAI, Claude, Bolt.new, V0.dev, lovable.dev) to scale products, decisions, and teams β€” accelerating software development by up to 5x and unlocking new levels of creativity, efficiency, and strategic impact."

β€” David Ruiz

PineUI solves real problems faced by companies that innovate constantly: rapid iteration, multi-platform deployment, AI-generated interfaces, and the ability to update user experiences without code deployments. It's built by someone who has scaled billion-dollar platforms and understands the challenges of modern software development at enterprise scale.

Key Features

  • Reactive state management with automatic UI updates
  • Intent-based action system for clean separation of concerns
  • Built-in HTTP client for API integration
  • Collection rendering with virtualization support
  • Modal and overlay system
  • Custom component library support
  • Data binding with {{}} syntax
  • Material Design 3 components out of the box

Roadmap

PineUI is constantly evolving, with focus on:

  • Expanding the component library
  • Performance improvements and optimizations
  • Native LLM integration for automatic UI generation
  • Visual tools for schema building
  • Support for more platforms (iOS native, Android native, Desktop)
  • Developer experience improvements

🀝 Contribute

PineUI is an open-source project licensed under Apache 2.0 + Commons Clause. Contributions are welcome! Visit our GitHub repository to report bugs, suggest features, or contribute code.

πŸ“§ Contact

For questions, suggestions, or commercial licensing:
Email: wupsbr@gmail.com
Creator: David Ruiz (wupsbr)
LinkedIn: linkedin.com/in/davidruiz
Company: Luma Ventures Ltda (CNPJ: 21.951.820/0001-39)

Installation

React

{
  "dependencies": {
    "@pineui/react": "^0.1.0"
  }
}

CDN (Standalone)

<!-- CSS -->
<link rel="stylesheet" href="https://unpkg.com/@pineui/react@latest/dist/style.css">

<!-- JavaScript (includes React) -->
<script src="https://unpkg.com/@pineui/react@latest/dist/pineui.standalone.js"></script>

Quick Start

Basic Example

{
  "schemaVersion": "1.0.0",
  "screen": {
    "type": "layout.column",
    "padding": 16,
    "spacing": 16,
    "children": [
      {
        "type": "text",
        "content": "Hello PineUI!",
        "style": "titleLarge"
      },
      {
        "type": "button.filled",
        "label": "Click Me",
        "onPress": {
          "type": "action.snackbar.show",
          "message": "Button clicked!"
        }
      }
    ]
  }
}

With State Management

{
  "schemaVersion": "1.0.0",
  "state": {
    "counter": 0
  },
  "intents": {
    "increment": {
      "handler": {
        "type": "action.state.patch",
        "path": "counter",
        "value": "{{state.counter + 1}}"
      }
    }
  },
  "screen": {
    "type": "layout.column",
    "padding": 16,
    "spacing": 16,
    "children": [
      {
        "type": "text",
        "content": "Count: {{state.counter}}",
        "style": "titleLarge"
      },
      {
        "type": "button.filled",
        "label": "Increment",
        "onPress": {
          "intent": "increment"
        }
      }
    ]
  }
}

πŸ€– Using with LLMs

PineUI is designed to be AI-friendly. LLMs like ChatGPT, Claude, and others can generate complete UIs using PineUI's declarative JSON schema - no specific training required!

Why LLMs Love PineUI

  • Declarative - Describe what you want, not how to build it
  • JSON-based - Native format for LLMs to generate
  • Material Design - Built-in components with consistent naming
  • Self-contained - Complete apps in a single schema

Complete LLM Context Guide

Copy and paste this complete guide when using ChatGPT, Claude, or any LLM to build PineUI applications:

πŸ“„ PROMPT.md - Complete Guide
All components, actions, patterns & examples for LLMs
⬇️ Download πŸ”— GitHub
πŸ“– View complete PROMPT.md guide

Click to load the guide...

Quick Example Prompt

Copy and paste this prompt into ChatGPT, Claude, or any LLM:

Create a complete HTML page with a PineUI application that displays a gallery of online courses with category filtering.

Requirements:
1. Use PineUI from CDN:
   - JS: https://unpkg.com/@pineui/react@latest/dist/pineui.standalone.js
   - CSS: https://unpkg.com/@pineui/react@latest/dist/style.css

2. The app should have:
   - Header with app bar showing "Course Gallery" title
   - Row of filter chips for categories: All, Design, Development, Business, Marketing
   - Grid layout (3 columns on desktop) showing course cards
   - Each card should show: thumbnail image, category badge, title, instructor name, rating, and price

3. State management:
   - selectedCategory state (default "All")
   - When a chip is clicked, update selectedCategory
   - Use conditional rendering to filter courses by category

4. Sample courses data (at least 9 courses across different categories):
   - Include realistic course titles, instructors, ratings, and prices
   - Use placeholder images from https://picsum.photos/400/225

5. Styling:
   - Use Material Design 3 colors
   - Responsive grid (3 columns on desktop, 2 on tablet, 1 on mobile)
   - Selected chip should be filled style, others outlined

Use PineUI.render() to mount the application to a div with id="app".

What the LLM Will Generate

The LLM will create a complete, working application with:

  • Proper HTML structure with PineUI CDN links
  • Complete JSON schema with state management
  • Interactive category filtering
  • Responsive grid layout
  • Material Design 3 styling

Tips for Better Results

  • Be specific - Describe exact components and layouts you want
  • Reference CDN - Always include the unpkg CDN links for PineUI
  • Mention state - If you need interactivity, specify state variables and actions
  • Include examples - Reference component types like "button.filled", "layout.column", etc.
  • Ask for variations - Request the LLM to iterate or add features

Common Use Cases

  • πŸ“± Mobile-first landing pages
  • 🎨 Product galleries and catalogs
  • πŸ“Š Data dashboards with charts
  • πŸ“ Forms and surveys
  • πŸ›’ E-commerce product listings
  • πŸ“° News feeds and blogs

πŸ’‘ Pro Tip: After the LLM generates the initial UI, you can ask it to add features, change styling, or refactor components. PineUI's declarative nature makes iterations easy!

Schema Structure

A PineUI schema is a JSON object that defines your entire application structure.

Root Properties

Property Type Description
schemaVersion string Schema version (currently "1.0.0")
imports object External component imports
state object Initial application state
intents object Intent definitions (actions handlers)
screen ComponentNode Root UI component
components object Custom component definitions
overlays object Modal and overlay definitions

Components

PineUI provides a comprehensive set of Material Design 3 components that can be composed to build complex user interfaces. All components support data binding and can be nested to create rich layouts.

Component Categories

  • Layout: Structure your UI with rows, columns, and spacing
  • Display: Show text, images, avatars, and cards
  • Input: Text fields and form controls
  • Action: Buttons, FABs, and interactive elements
  • Data: Collections and conditional rendering

πŸ’‘ Component Composition

All PineUI components can be composed together. Use layout components (row, column) to arrange other components, and nest them as deeply as needed.

State Management

PineUI includes a reactive state management system. When state changes, the UI automatically updates to reflect the new values. State can be accessed using {{state.propertyName}} syntax.

Initial State

Define your initial state in the schema root:

{
  "state": {
    "username": "",
    "counter": 0,
    "items": [],
    "settings": {
      "darkMode": false,
      "notifications": true
    }
  }
}

Updating State

Use the action.state.patch action to update state:

{
  "type": "action.state.patch",
  "path": "counter",
  "value": "{{state.counter + 1}}"
}

Nested State

Access and update nested state using dot notation:

{
  "type": "action.state.patch",
  "path": "settings.darkMode",
  "value": true
}

πŸ’‘ Reactivity

State updates are automatically reactive. Any component that references a state value will re-render when that value changes.

Intents

Intents are named action handlers that encapsulate business logic. They provide a clean separation between UI components and actions, making your schemas more maintainable.

Defining Intents

{
  "intents": {
    "loadUsers": {
      "handler": {
        "type": "action.http",
        "method": "GET",
        "url": "https://api.example.com/users",
        "onSuccess": {
          "type": "action.state.patch",
          "path": "users",
          "value": "{{response}}"
        }
      }
    },
    "deleteUser": {
      "handler": [
        {
          "type": "action.http",
          "method": "DELETE",
          "url": "https://api.example.com/users/{{props.userId}}"
        },
        {
          "intent": "loadUsers"
        }
      ]
    }
  }
}

Calling Intents

Trigger intents from component actions:

{
  "type": "button.filled",
  "label": "Load Data",
  "onPress": {
    "intent": "loadUsers"
  }
}

Intent Props

Pass parameters to intents:

{
  "onPress": {
    "intent": "deleteUser",
    "props": {
      "userId": "{{item.id}}"
    }
  }
}

πŸ’‘ Action Chains

Intents can execute multiple actions in sequence by using an array. This is perfect for workflows like: make API call β†’ update state β†’ show success message.

Actions

Actions are the building blocks of interactivity in PineUI. They handle HTTP requests, state updates, navigation, and more. Actions can be triggered from component events or executed within intents.

Action Types

  • action.http - Make HTTP requests to APIs
  • action.state.patch - Update application state
  • action.overlay.open/close - Control modals and overlays
  • action.snackbar.show - Display toast notifications
  • action.delay - Add timing delays
  • action.collection.refresh - Refresh collection data

Action Chaining

Execute multiple actions in sequence:

{
  "onPress": [
    {
      "type": "action.state.patch",
      "path": "loading",
      "value": true
    },
    {
      "type": "action.http",
      "method": "POST",
      "url": "/api/save"
    },
    {
      "type": "action.state.patch",
      "path": "loading",
      "value": false
    },
    {
      "type": "action.snackbar.show",
      "message": "Saved successfully!"
    }
  ]
}

Text Component

Display text with Material Design 3 typography styles.

Properties

Property Type Description
type required "text" Component type identifier
content required string Text content to display. Supports {{bindings}}
style string Typography style: "titleLarge", "titleMedium", "titleSmall", "bodyLarge", "bodyMedium", "bodySmall", "headlineSmall"
color string Text color (hex, Material color token, or CSS color)
fontWeight string "normal", "bold", or numeric (400, 600, 700, etc.)
fontSize number Font size in pixels
lineHeight number Line height multiplier

Example

{
  "type": "text",
  "content": "Hello World",
  "style": "titleLarge",
  "color": "#6750A4",
  "fontWeight": "bold"
}

πŸ’‘ Tip

Use Material Design color tokens like onSurface, primary, onSurfaceVariant for consistent theming across platforms.

Button Component

Buttons trigger actions when pressed. PineUI provides multiple button types following Material Design 3 guidelines: filled, outlined, text, icon, and floating action buttons (FAB).

Button Types

  • button.filled - High emphasis, primary actions
  • button.outlined - Medium emphasis, secondary actions
  • button.text - Low emphasis, tertiary actions
  • button.icon - Icon-only button for toolbars
  • button.fab - Floating action button for primary screen actions

Properties

Property Type Description
type required string Button type: "button.filled", "button.outlined", "button.text", "button.icon", "button.fab"
label string Button text label. Supports {{bindings}}. Required for filled/outlined/text buttons
icon string Material icon name (e.g., "add", "delete", "edit"). Required for icon and fab buttons
onPress Action | Action[] Action(s) to execute when button is pressed
disabled boolean Disable button interaction. Can use {{bindings}}
fullWidth boolean Make button expand to full width of container
color string Custom button color (hex or Material color token)
size string For FAB: "small", "medium", "large". Default: "medium"

Examples

Filled Button

{
  "type": "button.filled",
  "label": "Submit",
  "onPress": {
    "intent": "submitForm"
  }
}

Outlined Button

{
  "type": "button.outlined",
  "label": "Cancel",
  "onPress": {
    "type": "action.overlay.close"
  }
}

Text Button

{
  "type": "button.text",
  "label": "Learn More",
  "onPress": {
    "type": "action.http",
    "method": "GET",
    "url": "/api/details"
  }
}

Icon Button

{
  "type": "button.icon",
  "icon": "favorite",
  "onPress": {
    "type": "action.state.patch",
    "path": "isFavorite",
    "value": true
  }
}

Floating Action Button

{
  "type": "button.fab",
  "icon": "add",
  "size": "large",
  "onPress": {
    "type": "action.overlay.open",
    "overlayId": "createItemModal"
  }
}

Button with Icon and Label

{
  "type": "button.filled",
  "label": "Download",
  "icon": "download",
  "onPress": {
    "intent": "downloadFile"
  }
}

πŸ’‘ Button Hierarchy

Use filled buttons for primary actions, outlined for secondary actions, and text buttons for tertiary actions. This creates a clear visual hierarchy that guides users.

πŸ’‘ Disabled State

You can dynamically disable buttons based on state: "disabled": "{{state.formInvalid}}"

Layout Components

Layout components organize other components in rows and columns. They provide flexible spacing, alignment, and responsive behavior to structure your UI.

Layout Types

  • layout.column - Vertical stack of components
  • layout.row - Horizontal arrangement of components

Properties

Property Type Description
type required string "layout.column" or "layout.row"
children required ComponentNode[] Array of child components to render
spacing number Gap between children in pixels. Default: 0
padding number | object Padding around the layout. Number for all sides, or object: {top, right, bottom, left}
mainAxisAlignment string "start", "center", "end", "spaceBetween", "spaceAround", "spaceEvenly". Default: "start"
crossAxisAlignment string "start", "center", "end", "stretch". Default: "start"
width number | string Width in pixels or percentage (e.g., "100%")
height number | string Height in pixels or percentage
backgroundColor string Background color (hex, Material color token, or CSS color)
borderRadius number Corner radius in pixels
scrollable boolean Enable scrolling when content overflows. Default: false

Examples

Basic Column

{
  "type": "layout.column",
  "padding": 16,
  "spacing": 12,
  "children": [
    {
      "type": "text",
      "content": "Title",
      "style": "titleLarge"
    },
    {
      "type": "text",
      "content": "Subtitle",
      "style": "bodyMedium"
    }
  ]
}

Row with Alignment

{
  "type": "layout.row",
  "spacing": 8,
  "mainAxisAlignment": "spaceBetween",
  "crossAxisAlignment": "center",
  "children": [
    {
      "type": "text",
      "content": "Total",
      "style": "bodyLarge"
    },
    {
      "type": "text",
      "content": "$99.99",
      "style": "titleMedium",
      "fontWeight": "bold"
    }
  ]
}

Nested Layouts

{
  "type": "layout.column",
  "padding": 16,
  "spacing": 16,
  "children": [
    {
      "type": "layout.row",
      "spacing": 8,
      "crossAxisAlignment": "center",
      "children": [
        {
          "type": "avatar",
          "url": "{{user.avatar}}",
          "size": 40
        },
        {
          "type": "layout.column",
          "spacing": 4,
          "children": [
            {
              "type": "text",
              "content": "{{user.name}}",
              "style": "titleMedium"
            },
            {
              "type": "text",
              "content": "{{user.email}}",
              "style": "bodySmall"
            }
          ]
        }
      ]
    }
  ]
}

Padding Object Syntax

{
  "type": "layout.column",
  "padding": {
    "top": 24,
    "right": 16,
    "bottom": 24,
    "left": 16
  },
  "children": [...]
}

πŸ’‘ Alignment Guide

mainAxisAlignment controls alignment along the primary direction (vertical for column, horizontal for row).

crossAxisAlignment controls alignment perpendicular to the primary direction.

πŸ’‘ Responsive Spacing

Use consistent spacing values (8, 12, 16, 24) to create a harmonious layout that follows Material Design spacing guidelines.

Card Component

Cards are surfaces that display content and actions on a single topic. They provide elevation, borders, and can be made interactive with tap handlers.

Properties

Property Type Description
type required "card" Component type identifier
child required ComponentNode Single child component to render inside the card
elevation number Shadow depth: 0-5. Default: 1
padding number | object Internal padding. Number for all sides, or object: {top, right, bottom, left}
backgroundColor string Card background color. Default: "surface"
borderRadius number Corner radius in pixels. Default: 12
onPress Action | Action[] Action(s) to execute when card is tapped
width number | string Card width in pixels or percentage
height number | string Card height in pixels or percentage

Examples

Basic Card

{
  "type": "card",
  "elevation": 2,
  "padding": 16,
  "child": {
    "type": "layout.column",
    "spacing": 8,
    "children": [
      {
        "type": "text",
        "content": "Card Title",
        "style": "titleMedium"
      },
      {
        "type": "text",
        "content": "Card content goes here",
        "style": "bodyMedium"
      }
    ]
  }
}

Interactive Card

{
  "type": "card",
  "elevation": 1,
  "padding": 16,
  "onPress": {
    "intent": "openDetails",
    "props": {
      "id": "{{item.id}}"
    }
  },
  "child": {
    "type": "layout.row",
    "spacing": 12,
    "crossAxisAlignment": "center",
    "children": [
      {
        "type": "avatar",
        "url": "{{item.imageUrl}}",
        "size": 48
      },
      {
        "type": "layout.column",
        "spacing": 4,
        "children": [
          {
            "type": "text",
            "content": "{{item.title}}",
            "style": "titleMedium"
          },
          {
            "type": "text",
            "content": "{{item.subtitle}}",
            "style": "bodySmall"
          }
        ]
      }
    ]
  }
}

Card with Image

{
  "type": "card",
  "elevation": 2,
  "padding": 0,
  "child": {
    "type": "layout.column",
    "spacing": 0,
    "children": [
      {
        "type": "image",
        "url": "{{product.imageUrl}}",
        "height": 200,
        "width": "100%",
        "fit": "cover"
      },
      {
        "type": "layout.column",
        "padding": 16,
        "spacing": 8,
        "children": [
          {
            "type": "text",
            "content": "{{product.name}}",
            "style": "titleMedium"
          },
          {
            "type": "text",
            "content": "{{product.price}}",
            "style": "bodyLarge",
            "fontWeight": "bold"
          }
        ]
      }
    ]
  }
}

πŸ’‘ Elevation Levels

Use elevation 0 for flat cards, 1 for resting state, 2-3 for hover/focus, and 4-5 for dragged states. Higher elevation creates more visual separation.

πŸ’‘ Interactive Cards

When adding onPress to a card, it automatically becomes interactive with hover and press states. Perfect for list items and navigation.

Input Component

Text input fields allow users to enter and edit text. They follow Material Design 3 text field specifications with support for labels, hints, validation, and various input types.

Properties

Property Type Description
type required "input" Component type identifier
statePath required string Path in state where value is stored (e.g., "username")
label string Floating label text
placeholder string Hint text shown when input is empty
helperText string Supporting text below input field
errorText string Error message to display. Supports {{bindings}}
inputType string "text", "email", "password", "number", "tel", "url". Default: "text"
maxLength number Maximum character length
multiline boolean Enable multi-line text area. Default: false
rows number Number of visible rows (for multiline). Default: 3
disabled boolean Disable input interaction. Can use {{bindings}}
required boolean Mark field as required (shows * in label)
onChange Action | Action[] Action(s) to execute when value changes

Examples

Basic Text Input

{
  "type": "input",
  "statePath": "username",
  "label": "Username",
  "placeholder": "Enter your username"
}

Email Input with Validation

{
  "type": "input",
  "statePath": "email",
  "label": "Email Address",
  "inputType": "email",
  "required": true,
  "errorText": "{{state.emailError}}",
  "helperText": "We'll never share your email"
}

Password Input

{
  "type": "input",
  "statePath": "password",
  "label": "Password",
  "inputType": "password",
  "required": true,
  "minLength": 8,
  "helperText": "At least 8 characters"
}

Multi-line Text Area

{
  "type": "input",
  "statePath": "description",
  "label": "Description",
  "multiline": true,
  "rows": 5,
  "maxLength": 500,
  "helperText": "{{state.description.length || 0}}/500 characters"
}

Input with Change Handler

{
  "type": "input",
  "statePath": "searchQuery",
  "label": "Search",
  "placeholder": "Search items...",
  "onChange": {
    "intent": "searchItems",
    "props": {
      "query": "{{state.searchQuery}}"
    }
  }
}

πŸ’‘ State Binding

The input automatically updates the state at statePath as the user types. Access the value elsewhere with {{state.username}}.

πŸ’‘ Validation

Use errorText with state bindings to show validation errors: "errorText": "{{state.errors.email}}"

Image Component

Display images from URLs with support for sizing, object fit, and loading states. Perfect for photos, illustrations, and icons.

Properties

Property Type Description
type required "image" Component type identifier
url required string Image URL. Supports {{bindings}}
width number | string Width in pixels or percentage (e.g., "100%")
height number | string Height in pixels or percentage
fit string "cover", "contain", "fill", "none", "scaleDown". Default: "cover"
borderRadius number Corner radius in pixels
alt string Alternative text for accessibility
onPress Action | Action[] Action(s) to execute when image is tapped

Examples

Basic Image

{
  "type": "image",
  "url": "https://example.com/photo.jpg",
  "width": "100%",
  "height": 200,
  "fit": "cover",
  "borderRadius": 8
}

Profile Image

{
  "type": "image",
  "url": "{{user.profilePhoto}}",
  "width": 120,
  "height": 120,
  "fit": "cover",
  "borderRadius": 60,
  "alt": "{{user.name}} profile photo"
}

Product Image with Click

{
  "type": "image",
  "url": "{{product.imageUrl}}",
  "width": "100%",
  "height": 300,
  "fit": "contain",
  "onPress": {
    "type": "action.overlay.open",
    "overlayId": "imageGallery",
    "props": {
      "images": "{{product.images}}"
    }
  }
}

Thumbnail Image

{
  "type": "image",
  "url": "{{item.thumbnail}}",
  "width": 80,
  "height": 80,
  "fit": "cover",
  "borderRadius": 4
}

πŸ’‘ Object Fit Guide

  • cover - Fills entire area, may crop image (best for backgrounds)
  • contain - Shows entire image, may have empty space (best for logos)
  • fill - Stretches to fill area, may distort image

πŸ’‘ Performance

Always specify width and height to prevent layout shift when images load. Use appropriate image sizes to reduce bandwidth.

Avatar Component

Avatars display user profile images or initials in a circular format. They're commonly used in lists, headers, and user profiles.

Properties

Property Type Description
type required "avatar" Component type identifier
url string Avatar image URL. Supports {{bindings}}
initials string Initials to display if no image URL. Supports {{bindings}}
size number Avatar diameter in pixels. Default: 40
backgroundColor string Background color for initials (hex or Material color token)
textColor string Color for initials text
onPress Action | Action[] Action(s) to execute when avatar is tapped

Examples

Avatar with Image

{
  "type": "avatar",
  "url": "{{user.avatarUrl}}",
  "size": 48
}

Avatar with Initials

{
  "type": "avatar",
  "initials": "{{user.name.substring(0, 2).toUpperCase()}}",
  "size": 40,
  "backgroundColor": "#6750A4",
  "textColor": "#FFFFFF"
}

Large Profile Avatar

{
  "type": "avatar",
  "url": "{{state.currentUser.photo}}",
  "size": 120,
  "onPress": {
    "type": "action.overlay.open",
    "overlayId": "editProfile"
  }
}

Small Avatar in List

{
  "type": "layout.row",
  "spacing": 12,
  "crossAxisAlignment": "center",
  "children": [
    {
      "type": "avatar",
      "url": "{{item.userAvatar}}",
      "initials": "{{item.userInitials}}",
      "size": 32
    },
    {
      "type": "text",
      "content": "{{item.userName}}",
      "style": "bodyMedium"
    }
  ]
}

πŸ’‘ Fallback Behavior

If url fails to load or is empty, the avatar will display initials instead. Always provide initials as a fallback.

πŸ’‘ Size Guidelines

  • 24-32px: Tiny avatars in dense lists or chips
  • 40-48px: Standard list items and comments
  • 64-80px: User profiles and cards
  • 120px+: Large profile headers

Chip Component

Chips are compact elements used for filtering, selection, and toggling. They show selected state visually.

Properties

PropertyTypeDescription
type required"chip"Component type
label requiredstringText displayed on the chip
selectedbooleanWhether the chip is active/selected. Supports bindings: "{{state.filter == 'Design'}}"
onPressActionAction triggered when chip is clicked

Example

{
  "type": "chip",
  "label": "Design",
  "selected": "{{state.filter == 'Design'}}",
  "onPress": {
    "type": "action.state.patch",
    "path": "filter",
    "value": "Design"
  }
}

Badge Component

Small colored labels used for status, categories, or counts.

Properties

PropertyTypeDescription
type required"badge"Component type
label requiredstringBadge text
colorstring"default" | "success" | "warning" | "error" | "info"

Example

{ "type": "badge", "label": "New", "color": "success" }
{ "type": "badge", "label": "Error", "color": "error" }
{ "type": "badge", "label": "{{item.status}}", "color": "info" }

Progress Component

Linear progress bar. Note: progress.circular and progress.linear do NOT exist β€” use progress.

Properties

PropertyTypeDescription
type required"progress"Component type
valuenumber0–100 (percentage)
labelstringOptional text shown next to bar
colorstring"primary" | "success" | "error"

Example

{
  "type": "progress",
  "value": "{{item.completionPercent}}",
  "label": "{{item.completionPercent}}% complete",
  "color": "primary"
}

Tabs Component

Horizontal tab bar that switches between content panels. Bind activeTab to state for control.

Properties

PropertyTypeDescription
type required"tabs"Component type
tabs requiredarrayArray of { id, label, content }
activeTabstringID of the currently active tab. Use binding: "{{state.tab}}"
onTabChangeActionAction triggered when a tab is clicked. Use {{tab}} for the clicked tab's id

Example

{
  "type": "tabs",
  "activeTab": "{{state.tab}}",
  "onTabChange": {
    "type": "action.state.patch",
    "path": "tab",
    "value": "{{tab}}"
  },
  "tabs": [
    { "id": "home", "label": "Home", "content": { "type": "text", "content": "Home" } },
    { "id": "settings", "label": "Settings", "content": { "type": "text", "content": "Settings" } }
  ]
}

Grid Component

Static grid with fixed children. For API-driven grids, use collection with layout: "grid".

Note: layout.grid does NOT exist β€” use grid.

Properties

PropertyTypeDescription
type required"grid"Component type
columnsnumberNumber of columns (default 3)
spacingnumberGap between items (px)
childrenarrayChild components

Example

{
  "type": "grid",
  "columns": 3,
  "spacing": 16,
  "children": [
    { "type": "card", "child": { "type": "text", "content": "Item 1" } },
    { "type": "card", "child": { "type": "text", "content": "Item 2" } },
    { "type": "card", "child": { "type": "text", "content": "Item 3" } }
  ]
}

Collection Component

Collections render lists of data by repeating a template for each item. They support data binding, item actions, and automatic updates when the underlying data changes.

Properties

Property Type Description
type required "collection" Component type identifier
data required string State path to array of items (e.g., "items" or "users")
template required ComponentNode Component template to render for each item
spacing number Gap between items in pixels. Default: 0
emptyState ComponentNode Component to show when data array is empty
loadingState ComponentNode Component to show while data is loading

Item Context

Within the template, you can access:

  • {{item.propertyName}} - Properties of the current item
  • {{index}} - Zero-based index of the current item

Examples

Basic List

{
  "type": "collection",
  "data": "users",
  "spacing": 8,
  "template": {
    "type": "card",
    "padding": 16,
    "child": {
      "type": "text",
      "content": "{{item.name}}",
      "style": "bodyLarge"
    }
  }
}

User List with Avatars

{
  "type": "collection",
  "data": "users",
  "spacing": 12,
  "template": {
    "type": "card",
    "padding": 16,
    "onPress": {
      "intent": "viewProfile",
      "props": {
        "userId": "{{item.id}}"
      }
    },
    "child": {
      "type": "layout.row",
      "spacing": 12,
      "crossAxisAlignment": "center",
      "children": [
        {
          "type": "avatar",
          "url": "{{item.avatar}}",
          "initials": "{{item.initials}}",
          "size": 48
        },
        {
          "type": "layout.column",
          "spacing": 4,
          "children": [
            {
              "type": "text",
              "content": "{{item.name}}",
              "style": "titleMedium"
            },
            {
              "type": "text",
              "content": "{{item.email}}",
              "style": "bodySmall",
              "color": "#79747E"
            }
          ]
        }
      ]
    }
  }
}

Product Grid with Empty State

{
  "type": "collection",
  "data": "products",
  "spacing": 16,
  "emptyState": {
    "type": "layout.column",
    "padding": 32,
    "spacing": 16,
    "mainAxisAlignment": "center",
    "crossAxisAlignment": "center",
    "children": [
      {
        "type": "text",
        "content": "No products found",
        "style": "titleMedium"
      },
      {
        "type": "text",
        "content": "Try adjusting your filters",
        "style": "bodyMedium",
        "color": "#79747E"
      }
    ]
  },
  "template": {
    "type": "card",
    "elevation": 2,
    "child": {
      "type": "layout.column",
      "spacing": 12,
      "children": [
        {
          "type": "image",
          "url": "{{item.imageUrl}}",
          "width": "100%",
          "height": 200,
          "fit": "cover"
        },
        {
          "type": "layout.column",
          "padding": 16,
          "spacing": 8,
          "children": [
            {
              "type": "text",
              "content": "{{item.name}}",
              "style": "titleMedium"
            },
            {
              "type": "text",
              "content": "${{item.price}}",
              "style": "bodyLarge",
              "fontWeight": "bold",
              "color": "#6750A4"
            }
          ]
        }
      ]
    }
  }
}

List with Index

{
  "type": "collection",
  "data": "tasks",
  "spacing": 4,
  "template": {
    "type": "layout.row",
    "spacing": 8,
    "padding": 12,
    "children": [
      {
        "type": "text",
        "content": "{{index + 1}}.",
        "style": "bodyMedium",
        "color": "#79747E"
      },
      {
        "type": "text",
        "content": "{{item.title}}",
        "style": "bodyMedium"
      }
    ]
  }
}

πŸ’‘ Data Reactivity

Collections automatically update when the data array changes. Add, remove, or modify items using action.state.patch and the UI updates instantly.

πŸ’‘ Performance

Collections use virtualization for large lists, rendering only visible items. This ensures smooth scrolling even with thousands of items.

Collection Map Component

Renders an array already in state β€” no HTTP call needed. Use when data is already loaded.

Use collection when data comes from an API, use collection.map when data is already in state.

Properties

PropertyTypeDescription
type required"collection.map"Component type
data requiredstring (binding)Array expression. Full JS supported: "{{state.items.filter(...)}}"
template or itemTemplateComponentTemplate rendered for each item. {{item}} and {{index}} available
layoutstring"list" (default) | "grid"
columnsnumberGrid columns (default 3)
spacingnumberGap between items (px)

Filtering & Sorting

The data expression re-evaluates on every state change. Put all filtering/sorting inline in the expression:

"data": "{{state.items.filter(i => i.active)}}"
"data": "{{state.filter == 'all' ? state.items : state.items.filter(i => i.type == state.filter)}}"
"data": "{{state.items.sort((a, b) => a.name.localeCompare(b.name))}}"
"data": "{{state.items.slice(0, state.limit)}}"

⚠️ Common Mistake

If you patch state.filter and want the list to update, the filter must be in the data expression. Simply patching state won't re-filter a static data array.

Example

{
  "state": { "filter": "all", "items": [...] },
  "screen": {
    "type": "layout.column",
    "children": [
      {
        "type": "layout.row",
        "spacing": 8,
        "children": [
          { "type": "chip", "label": "All", "selected": "{{state.filter == 'all'}}",
            "onPress": { "type": "action.state.patch", "path": "filter", "value": "all" } },
          { "type": "chip", "label": "Active", "selected": "{{state.filter == 'active'}}",
            "onPress": { "type": "action.state.patch", "path": "filter", "value": "active" } }
        ]
      },
      {
        "type": "collection.map",
        "data": "{{state.filter == 'all' ? state.items : state.items.filter(i => i.status == state.filter)}}",
        "spacing": 8,
        "template": {
          "type": "card",
          "child": { "type": "text", "content": "{{item.title}}" }
        }
      }
    ]
  }
}

Conditional Render

Conditionally show or hide components based on state values. Perfect for showing loading states, error messages, or content that depends on user permissions.

Properties

Property Type Description
type required "conditional" Component type identifier
condition required string Expression to evaluate (e.g., "{{state.isLoggedIn}}")
child required ComponentNode Component to render when condition is true
fallback ComponentNode Component to render when condition is false

Examples

Show/Hide Based on State

{
  "type": "conditional",
  "condition": "{{state.isLoggedIn}}",
  "child": {
    "type": "text",
    "content": "Welcome, {{state.user.name}}!",
    "style": "titleMedium"
  },
  "fallback": {
    "type": "button.filled",
    "label": "Login",
    "onPress": {
      "type": "action.overlay.open",
      "overlayId": "loginModal"
    }
  }
}

Loading State

{
  "type": "conditional",
  "condition": "{{state.loading}}",
  "child": {
    "type": "text",
    "content": "Loading...",
    "style": "bodyMedium"
  },
  "fallback": {
    "type": "collection",
    "data": "items",
    "template": {...}
  }
}

Error Handling

{
  "type": "conditional",
  "condition": "{{state.error}}",
  "child": {
    "type": "card",
    "backgroundColor": "#FEE2E2",
    "padding": 16,
    "child": {
      "type": "text",
      "content": "Error: {{state.error}}",
      "color": "#BA1A1A"
    }
  },
  "fallback": {
    "type": "text",
    "content": "{{state.data}}",
    "style": "bodyMedium"
  }
}

Comparison Condition

{
  "type": "conditional",
  "condition": "{{state.cart.items.length > 0}}",
  "child": {
    "type": "button.filled",
    "label": "Checkout ({{state.cart.items.length}} items)",
    "onPress": {
      "intent": "checkout"
    }
  },
  "fallback": {
    "type": "text",
    "content": "Your cart is empty",
    "style": "bodyMedium",
    "color": "#79747E"
  }
}

Multiple Conditions

{
  "type": "conditional",
  "condition": "{{state.user.role === 'admin'}}",
  "child": {
    "type": "button.filled",
    "label": "Admin Panel",
    "icon": "settings",
    "onPress": {
      "intent": "openAdminPanel"
    }
  }
}

πŸ’‘ Condition Expressions

Conditions support JavaScript expressions:

  • Boolean: {{state.isActive}}
  • Comparison: {{state.count > 10}}
  • Equality: {{state.status === 'complete'}}
  • Logical: {{state.a && state.b}}
  • Negation: {{!state.isEmpty}}

πŸ’‘ Nested Conditionals

You can nest conditional components to handle complex scenarios with multiple states.

HTTP Request Action

Make HTTP requests to REST APIs. Supports GET, POST, PUT, DELETE methods with headers, request bodies, and automatic state mapping of responses.

Properties

Property Type Description
type required "action.http" Action type identifier
method required string "GET", "POST", "PUT", "PATCH", "DELETE"
url required string API endpoint URL. Supports {{bindings}}
headers object HTTP headers as key-value pairs
body object Request body (for POST, PUT, PATCH). Supports {{bindings}}
onSuccess Action Action executed after a successful response. Use {{response}} to access the full response body
onError Action | Action[] Actions to execute on error response (non-2xx status)

Examples

GET Request

{
  "type": "action.http",
  "method": "GET",
  "url": "https://api.example.com/users",
  "onSuccess": {
    "type": "action.state.patch",
    "path": "users",
    "value": "{{response}}"
  }
}

POST Request with Body

{
  "type": "action.http",
  "method": "POST",
  "url": "https://api.example.com/users",
  "headers": {
    "Content-Type": "application/json",
    "Authorization": "Bearer {{state.authToken}}"
  },
  "body": {
    "name": "{{state.form.name}}",
    "email": "{{state.form.email}}",
    "role": "user"
  },
  "onSuccess": [
    {
      "type": "action.snackbar.show",
      "message": "User created successfully!"
    },
    {
      "intent": "loadUsers"
    }
  ],
  "onError": {
    "type": "action.snackbar.show",
    "message": "Error creating user",
    "severity": "error"
  }
}

DELETE Request

{
  "type": "action.http",
  "method": "DELETE",
  "url": "https://api.example.com/users/{{props.userId}}",
  "headers": {
    "Authorization": "Bearer {{state.authToken}}"
  },
  "onSuccess": {
    "intent": "refreshUserList"
  }
}

PUT Request with onSuccess

{
  "type": "action.http",
  "method": "PUT",
  "url": "https://api.example.com/profile",
  "body": {
    "name": "{{state.profile.name}}",
    "bio": "{{state.profile.bio}}"
  },
  "onSuccess": {
    "type": "action.sequence",
    "actions": [
      { "type": "action.state.patch", "path": "profile", "value": "{{response}}" },
      { "type": "action.snackbar.show", "message": "Profile updated!" }
    ]
  }
}

Search with Dynamic URL

{
  "type": "action.http",
  "method": "GET",
  "url": "https://api.example.com/search?q={{state.searchQuery}}&limit=20",
  "onSuccess": {
    "type": "action.state.patch",
    "path": "searchResults",
    "value": "{{response}}"
  }
}

πŸ’‘ onSuccess & {{response}}

Use onSuccess to handle the API response. {{response}} is the full response body and is only available inside onSuccess.

πŸ’‘ Error Handling

Always provide onError handlers to gracefully handle API failures and show user-friendly error messages.

State Patch Action

Update application state reactively. When state changes, all components that reference the updated values automatically re-render.

Properties

Property Type Description
type required "action.state.patch" Action type identifier
path required string State path to update (e.g., "counter", "user.name", "items[0].status")
value required any New value. Supports {{bindings}} and expressions

Examples

Update Simple Value

{
  "type": "action.state.patch",
  "path": "counter",
  "value": "{{state.counter + 1}}"
}

Update Nested Property

{
  "type": "action.state.patch",
  "path": "user.settings.darkMode",
  "value": true
}

Update from Input

{
  "type": "action.state.patch",
  "path": "form.email",
  "value": "{{state.email}}"
}

Toggle Boolean

{
  "type": "action.state.patch",
  "path": "isExpanded",
  "value": "{{!state.isExpanded}}"
}

Update Array Element

{
  "type": "action.state.patch",
  "path": "todos[{{index}}].completed",
  "value": true
}

Set Multiple Values

[
  {
    "type": "action.state.patch",
    "path": "loading",
    "value": false
  },
  {
    "type": "action.state.patch",
    "path": "error",
    "value": null
  },
  {
    "type": "action.state.patch",
    "path": "data",
    "value": "{{response}}"
  }
]

Update with Calculation

{
  "type": "action.state.patch",
  "path": "cart.total",
  "value": "{{state.cart.items.reduce((sum, item) => sum + item.price, 0)}}"
}

πŸ’‘ Dot Notation

Use dot notation to update nested properties: "user.profile.name". PineUI will create missing intermediate objects automatically.

πŸ’‘ Expressions

The value field supports JavaScript expressions. You can perform calculations, concatenations, and transformations.

Overlay Control Actions

Open and close modals, dialogs, and bottom sheets. Overlays are defined in the schema's overlays section and controlled with these actions.

action.overlay.open

Property Type Description
type required "action.overlay.open" Action type identifier
overlayId required string ID of the overlay to open (defined in overlays section)
props object Data to pass to the overlay. Accessible via {{props.key}}

action.overlay.close

Property Type Description
type required "action.overlay.close" Action type identifier
overlayId string Specific overlay to close. If omitted, closes the topmost overlay

Examples

Open Modal

{
  "type": "action.overlay.open",
  "overlayId": "confirmDialog",
  "props": {
    "title": "Delete Item",
    "message": "Are you sure you want to delete this item?",
    "itemId": "{{item.id}}"
  }
}

Close Current Overlay

{
  "type": "action.overlay.close"
}

Close Specific Overlay

{
  "type": "action.overlay.close",
  "overlayId": "editModal"
}

Open Then Close After Action

[
  {
    "type": "action.http",
    "method": "POST",
    "url": "/api/save",
    "body": "{{state.formData}}"
  },
  {
    "type": "action.overlay.close"
  },
  {
    "type": "action.snackbar.show",
    "message": "Saved successfully!"
  }
]

Overlay Definition Example

{
  "overlays": {
    "confirmDialog": {
      "type": "dialog",
      "content": {
        "type": "layout.column",
        "padding": 24,
        "spacing": 16,
        "children": [
          {
            "type": "text",
            "content": "{{props.title}}",
            "style": "titleLarge"
          },
          {
            "type": "text",
            "content": "{{props.message}}",
            "style": "bodyMedium"
          },
          {
            "type": "layout.row",
            "spacing": 8,
            "mainAxisAlignment": "end",
            "children": [
              {
                "type": "button.text",
                "label": "Cancel",
                "onPress": {
                  "type": "action.overlay.close"
                }
              },
              {
                "type": "button.filled",
                "label": "Delete",
                "onPress": [
                  {
                    "intent": "deleteItem",
                    "props": {
                      "id": "{{props.itemId}}"
                    }
                  },
                  {
                    "type": "action.overlay.close"
                  }
                ]
              }
            ]
          }
        ]
      }
    }
  }
}

πŸ’‘ Props Passing

Use props to pass data to overlays. Access inside the overlay with {{props.propertyName}}.

πŸ’‘ Auto Close

Calling action.overlay.close without an ID closes the most recently opened overlay, perfect for "Cancel" buttons.

Snackbar Action

Display brief messages at the bottom of the screen. Snackbars (also called toasts) are perfect for confirmation messages, errors, and notifications.

Properties

Property Type Description
type required "action.snackbar.show" Action type identifier
message required string Message to display. Supports {{bindings}}
duration number Duration in milliseconds. Default: 3000
severity string "success", "error", "warning", "info". Default: "info"
action object Optional action button with label and onPress

Examples

Simple Snackbar

{
  "type": "action.snackbar.show",
  "message": "Item added to cart"
}

Success Message

{
  "type": "action.snackbar.show",
  "message": "Profile updated successfully!",
  "severity": "success",
  "duration": 2000
}

Error Message

{
  "type": "action.snackbar.show",
  "message": "Failed to save changes",
  "severity": "error",
  "duration": 5000
}

With Action Button

{
  "type": "action.snackbar.show",
  "message": "Item deleted",
  "severity": "info",
  "action": {
    "label": "Undo",
    "onPress": {
      "intent": "undoDelete"
    }
  }
}

Dynamic Message

{
  "type": "action.snackbar.show",
  "message": "{{state.items.length}} items in your cart",
  "duration": 2000
}

Warning Message

{
  "type": "action.snackbar.show",
  "message": "You have unsaved changes",
  "severity": "warning",
  "duration": 4000
}

πŸ’‘ Severity Colors

  • success - Green background, for completed actions
  • error - Red background, for failures
  • warning - Orange background, for cautions
  • info - Blue/gray background, for neutral messages

πŸ’‘ Duration Guidelines

Keep messages brief and readable. Use 2-3 seconds for success, 4-5 seconds for errors users need to read carefully.

Sequence Action

Run multiple actions in order, one after another. Each action awaits completion before the next runs. Essential for multi-step workflows.

Properties

PropertyTypeDescription
type required"action.sequence"Action type
actions requiredAction[]Array of actions to run in order

Example

{
  "type": "action.sequence",
  "actions": [
    { "type": "action.state.patch", "path": "loading", "value": true },
    { "type": "action.http", "method": "POST", "url": "/api/save",
      "body": { "text": "{{state.text}}" } },
    { "type": "action.state.patch", "path": "loading", "value": false },
    { "type": "action.overlay.close", "overlayId": "editModal" },
    { "type": "action.snackbar.show", "message": "Saved!" }
  ]
}

Delay Action

Add a time delay before executing the next action in a sequence. Useful for creating animations, debouncing, or timing-based workflows.

Properties

Property Type Description
type required "action.delay" Action type identifier
duration required number Delay duration in milliseconds

Examples

Simple Delay

[
  {
    "type": "action.snackbar.show",
    "message": "Processing..."
  },
  {
    "type": "action.delay",
    "duration": 1000
  },
  {
    "type": "action.snackbar.show",
    "message": "Complete!",
    "severity": "success"
  }
]

Loading State with Delay

[
  {
    "type": "action.state.patch",
    "path": "loading",
    "value": true
  },
  {
    "type": "action.http",
    "method": "GET",
    "url": "/api/data"
  },
  {
    "type": "action.delay",
    "duration": 500
  },
  {
    "type": "action.state.patch",
    "path": "loading",
    "value": false
  }
]

Debounced Search

{
  "onChange": [
    {
      "type": "action.state.patch",
      "path": "searchQuery",
      "value": "{{state.input}}"
    },
    {
      "type": "action.delay",
      "duration": 300
    },
    {
      "intent": "performSearch"
    }
  ]
}

πŸ’‘ Action Sequences

Delays are most useful in action arrays where you need to pace operations. Without delay, all actions execute immediately.

πŸ’‘ User Experience

Use delays sparingly. Long delays can make your app feel sluggish. Most delays should be 100-500ms.

Custom Components

Create reusable component templates that can be instantiated multiple times with different properties. Custom components help you build a consistent design system and reduce duplication in your schemas.

Defining Custom Components

Custom components are defined in the schema's components section. The key must start with component. and the structure uses "definition":

{
  "components": {
    "component.userRow": {
      "definition": {
        "type": "layout.row",
        "spacing": 12,
        "children": [
          {
            "type": "avatar",
            "src": "{{props.user.avatar}}",
            "size": 40
          },
          {
            "type": "layout.column",
            "spacing": 2,
            "children": [
              { "type": "text", "content": "{{props.user.name}}", "style": "titleSmall" },
              { "type": "text", "content": "{{props.user.email}}", "style": "bodySmall" }
            ]
          }
        ]
      }
    }
  }
}

Using Custom Components

Use the component name as the type, and pass data via props:

{
  "type": "component.userRow",
  "props": {
    "user": "{{item}}"
  }
}

⚠️ Important Rules

  • Component names must start with component. (e.g. component.postCard)
  • The structure key is "definition", not "template"
  • Inside the definition, access passed props via {{props.fieldName}}
  • Props can be nested: {{props.user.name}}

Examples

Product Card Component

{
  "components": {
    "component.productCard": {
      "definition": {
        "type": "card",
        "elevation": 1,
        "padding": 0,
        "onPress": { "intent": "product.open", "productId": "{{item.id}}" },
        "child": {
          "type": "layout.column",
          "spacing": 0,
          "children": [
            {
              "type": "image",
              "src": "{{props.product.image}}",
              "aspectRatio": 1.5,
              "borderRadius": 12
            },
            {
              "type": "layout.column",
              "padding": 12,
              "spacing": 4,
              "children": [
                { "type": "text", "content": "{{props.product.name}}", "style": "titleSmall" },
                { "type": "text", "content": "${{props.product.price}}", "style": "bodyMedium", "color": "#6750A4" }
              ]
            }
          ]
        }
      }
    }
  }
}

Using Product Card in a Collection

{
  "type": "collection",
  "layout": "grid",
  "columns": 3,
  "data": { "type": "action.http", "method": "GET", "url": "/api/products" },
  "itemTemplate": {
    "type": "component.productCard",
    "props": { "product": "{{item}}" }
  }
}

πŸ’‘ Props Access

Inside custom component templates, access passed properties with {{props.propertyName}}. This keeps components reusable and flexible.

πŸ’‘ Component Library

Build a library of custom components for your app: buttons, cards, headers, etc. This ensures visual consistency and speeds up development.

πŸ’‘ Default Values

Use the || operator for default prop values: "{{props.color || '#6750A4'}}"

Overlays & Modals

Overlays display content above the main screen. PineUI supports three overlay types: dialogs (centered modals), bottom sheets (slide-up panels), and modals (full-screen overlays).

Overlay Types

  • dialog - Centered modal for confirmations and forms
  • bottomSheet - Slide-up panel from the bottom
  • modal - Full-screen overlay for complex workflows

Overlay Properties

Property Type Description
type required string "dialog", "bottomSheet", or "modal"
content required ComponentNode Component to render inside the overlay
dismissible boolean Allow closing by tapping outside or back button. Default: true
maxWidth number Maximum width in pixels (dialog only)

Examples

Confirm Dialog

{
  "overlays": {
    "deleteConfirm": {
      "type": "dialog",
      "maxWidth": 400,
      "content": {
        "type": "layout.column",
        "padding": 24,
        "spacing": 20,
        "children": [
          {
            "type": "text",
            "content": "Delete Item?",
            "style": "titleLarge"
          },
          {
            "type": "text",
            "content": "This action cannot be undone.",
            "style": "bodyMedium",
            "color": "#49454F"
          },
          {
            "type": "layout.row",
            "spacing": 12,
            "mainAxisAlignment": "end",
            "children": [
              {
                "type": "button.text",
                "label": "Cancel",
                "onPress": {
                  "type": "action.overlay.close"
                }
              },
              {
                "type": "button.filled",
                "label": "Delete",
                "color": "#BA1A1A",
                "onPress": [
                  {
                    "intent": "deleteItem"
                  },
                  {
                    "type": "action.overlay.close"
                  }
                ]
              }
            ]
          }
        ]
      }
    }
  }
}

Bottom Sheet Menu

{
  "overlays": {
    "optionsMenu": {
      "type": "bottomSheet",
      "content": {
        "type": "layout.column",
        "padding": 16,
        "spacing": 8,
        "children": [
          {
            "type": "text",
            "content": "Options",
            "style": "titleMedium",
            "padding": 8
          },
          {
            "type": "button.text",
            "label": "Share",
            "icon": "share",
            "fullWidth": true,
            "onPress": {
              "intent": "shareItem"
            }
          },
          {
            "type": "button.text",
            "label": "Edit",
            "icon": "edit",
            "fullWidth": true,
            "onPress": {
              "intent": "editItem"
            }
          },
          {
            "type": "button.text",
            "label": "Delete",
            "icon": "delete",
            "fullWidth": true,
            "color": "#BA1A1A",
            "onPress": {
              "type": "action.overlay.open",
              "overlayId": "deleteConfirm"
            }
          }
        ]
      }
    }
  }
}

Form Modal

{
  "overlays": {
    "createUserModal": {
      "type": "modal",
      "dismissible": false,
      "content": {
        "type": "layout.column",
        "padding": 24,
        "spacing": 20,
        "height": "100%",
        "children": [
          {
            "type": "layout.row",
            "mainAxisAlignment": "spaceBetween",
            "crossAxisAlignment": "center",
            "children": [
              {
                "type": "text",
                "content": "Create User",
                "style": "titleLarge"
              },
              {
                "type": "button.icon",
                "icon": "close",
                "onPress": {
                  "type": "action.overlay.close"
                }
              }
            ]
          },
          {
            "type": "layout.column",
            "spacing": 16,
            "children": [
              {
                "type": "input",
                "statePath": "newUser.name",
                "label": "Full Name",
                "required": true
              },
              {
                "type": "input",
                "statePath": "newUser.email",
                "label": "Email",
                "inputType": "email",
                "required": true
              },
              {
                "type": "input",
                "statePath": "newUser.role",
                "label": "Role"
              }
            ]
          },
          {
            "type": "layout.row",
            "spacing": 12,
            "mainAxisAlignment": "end",
            "children": [
              {
                "type": "button.outlined",
                "label": "Cancel",
                "onPress": {
                  "type": "action.overlay.close"
                }
              },
              {
                "type": "button.filled",
                "label": "Create",
                "onPress": [
                  {
                    "intent": "createUser"
                  },
                  {
                    "type": "action.overlay.close"
                  }
                ]
              }
            ]
          }
        ]
      }
    }
  }
}

Opening an Overlay

{
  "type": "button.filled",
  "label": "New User",
  "icon": "add",
  "onPress": {
    "type": "action.overlay.open",
    "overlayId": "createUserModal"
  }
}

πŸ’‘ When to Use Each Type

  • Dialog: Quick confirmations, alerts, simple forms
  • Bottom Sheet: Action menus, filters, settings
  • Modal: Multi-step forms, detailed views, complex workflows

πŸ’‘ Passing Data

Use the props parameter in action.overlay.open to pass data to overlays. Access it in the overlay with {{props.propertyName}}.

Design System

PineUI uses Material Design 3 as its foundation, providing a comprehensive design system with customizable colors, typography, spacing, and components.

Color System

PineUI supports Material Design 3 color tokens. You can customize colors at the component level using standard color values (hex, rgb, named colors) or Material Design 3 semantic tokens.

Common Color Properties

{
  "type": "text",
  "content": "Custom colored text",
  "color": "#6750A4",           // Hex color
  "backgroundColor": "#F7F2FA"   // Background color
}

Material Design 3 Semantic Colors

Primary Colors

  • #6750A4 - Primary
  • #E8DEF8 - Primary Container
  • #FFFFFF - On Primary
  • #21005D - On Primary Container

Secondary Colors

  • #625B71 - Secondary
  • #E8DEF8 - Secondary Container
  • #FFFFFF - On Secondary
  • #1D192B - On Secondary Container

Surface & Background

  • #FFFBFE - Surface / Background
  • #1C1B1F - On Surface
  • #E7E0EC - Surface Variant
  • #49454F - On Surface Variant
  • #CAC4D0 - Outline

Error & Status

  • #BA1A1A - Error
  • #F9DEDC - Error Container
  • #FFFFFF - On Error
  • #410E0B - On Error Container

Example: Custom Button Colors

{
  "type": "button.filled",
  "label": "Custom Button",
  "backgroundColor": "#6750A4",
  "color": "#FFFFFF"
}

Typography

PineUI implements Material Design 3 type scale with predefined text styles. Each style has specific size, weight, and line height optimized for readability.

Display Styles (Large Headlines)

{
  "type": "text",
  "content": "Display Text",
  "style": "displayLarge"    // 57sp, Regular
  // or "displayMedium"      // 45sp, Regular
  // or "displaySmall"       // 36sp, Regular
}

Headline Styles (Section Headers)

{
  "type": "text",
  "content": "Headline",
  "style": "headlineLarge"   // 32sp, Regular
  // or "headlineMedium"     // 28sp, Regular
  // or "headlineSmall"      // 24sp, Regular
}

Title Styles (Emphasis)

{
  "type": "text",
  "content": "Title",
  "style": "titleLarge"      // 22sp, Regular
  // or "titleMedium"        // 16sp, Medium (500)
  // or "titleSmall"         // 14sp, Medium (500)
}

Body Styles (Content)

{
  "type": "text",
  "content": "Body text for reading",
  "style": "bodyLarge"       // 16sp, Regular
  // or "bodyMedium"         // 14sp, Regular (default)
  // or "bodySmall"          // 12sp, Regular
}

Label Styles (UI Elements)

{
  "type": "text",
  "content": "Label",
  "style": "labelLarge"      // 14sp, Medium (500)
  // or "labelMedium"        // 12sp, Medium (500)
  // or "labelSmall"         // 11sp, Medium (500)
}

πŸ’‘ Typography Best Practices

  • Use displayLarge for hero sections and main titles
  • Use headlineMedium for page titles
  • Use titleMedium for card titles and list headers
  • Use bodyMedium for main content (default)
  • Use labelMedium for buttons and small UI elements

Spacing & Layout

PineUI uses an 8-point grid system for consistent spacing. All spacing values should be multiples of 4 (4, 8, 12, 16, 20, 24, 32, 40, 48, etc.) for visual harmony.

Common Spacing Values

  • 4px - Extra small (tight spacing)
  • 8px - Small (compact elements)
  • 12px - Medium-small (list items)
  • 16px - Medium (default padding)
  • 24px - Large (section spacing)
  • 32px - Extra large (major sections)
  • 48px - XXL (hero spacing)

Layout Spacing Example

{
  "type": "layout.column",
  "padding": 16,           // Internal padding (all sides)
  "spacing": 12,           // Space between children
  "children": [
    {
      "type": "text",
      "content": "Item 1"
    },
    {
      "type": "text",
      "content": "Item 2"
    }
  ]
}

Card Spacing Example

{
  "type": "card",
  "padding": 16,           // Internal padding
  "child": {
    "type": "layout.column",
    "spacing": 8,          // Tight spacing for card content
    "children": [...]
  }
}

Customizing Theme

You can customize the entire theme by overriding CSS variables or by setting colors directly on components.

Method 1: CSS Variables (Global Theme)

<style>
  :root {
    --pineui-primary: #6750A4;
    --pineui-primary-container: #E8DEF8;
    --pineui-on-primary: #FFFFFF;
    --pineui-secondary: #625B71;
    --pineui-surface: #FFFBFE;
    --pineui-on-surface: #1C1B1F;
    --pineui-outline: #CAC4D0;
    --pineui-error: #BA1A1A;
  }
</style>

Method 2: Component-Level Customization

{
  "type": "button.filled",
  "label": "Custom Button",
  "backgroundColor": "#1976D2",  // Custom blue
  "color": "#FFFFFF"
}

Method 3: Custom CSS Classes

You can add custom CSS classes to any component using the className property:

// In your schema:
{
  "type": "card",
  "className": "my-custom-card",
  "child": {...}
}

// In your CSS:
.my-custom-card {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border: 2px solid #6750A4;
  box-shadow: 0 8px 16px rgba(0,0,0,0.1);
}

Dark Mode Support

PineUI components automatically adapt to dark mode when you define dark theme colors. Use CSS media queries or theme switching logic:

<style>
  /* Light theme (default) */
  :root {
    --pineui-surface: #FFFBFE;
    --pineui-on-surface: #1C1B1F;
  }

  /* Dark theme */
  @media (prefers-color-scheme: dark) {
    :root {
      --pineui-surface: #1C1B1F;
      --pineui-on-surface: #E6E1E5;
      --pineui-primary: #D0BCFF;
      --pineui-outline: #938F99;
    }
  }
</style>

Responsive Design

PineUI components are responsive by default. Use layout components to create adaptive designs:

Flexible Layouts

{
  "type": "layout.row",
  "mainAxisAlignment": "spaceBetween",  // space-between, center, start, end
  "crossAxisAlignment": "center",       // center, start, end, stretch
  "wrap": true,                         // Wrap to next line on small screens
  "children": [...]
}

Responsive Card Grid

{
  "type": "layout.row",
  "wrap": true,
  "spacing": 16,
  "children": [
    {
      "type": "card",
      "padding": 16,
      "child": {...}
    },
    {
      "type": "card",
      "padding": 16,
      "child": {...}
    }
  ]
}

πŸ’‘ Design Tips

  • Consistency: Stick to the 8-point grid for spacing
  • Hierarchy: Use typography scale to establish visual hierarchy
  • Contrast: Ensure sufficient contrast between text and background (WCAG AA minimum)
  • Touch Targets: Minimum 48px height for buttons on mobile
  • Whitespace: Don't be afraid of empty space - it improves readability

Material Design Resources

For more information about Material Design 3, visit:

Data Bindings

Data bindings connect your UI to dynamic values using the {{}} syntax. Bindings automatically update when the underlying data changes, creating a reactive UI.

Binding Types

  • {{state.propertyName}} - Access application state
  • {{props.propertyName}} - Access component/overlay props
  • {{item.propertyName}} - Access collection item data
  • {{index}} - Access collection item index
  • {{response}} - Access HTTP response (only available inside collection.data.onSuccess)

State Bindings

{
  "state": {
    "username": "John",
    "count": 42,
    "user": {
      "name": "Jane",
      "email": "jane@example.com"
    }
  }
}

// Usage:
{
  "type": "text",
  "content": "Hello, {{state.username}}!"
}

{
  "type": "text",
  "content": "Count: {{state.count}}"
}

{
  "type": "text",
  "content": "{{state.user.name}} ({{state.user.email}})"
}

Props Bindings

In custom components (prefix must be component.):

{
  "type": "component.userCard",
  "props": {
    "user": "{{item}}"
  }
}

// Inside component definition:
{
  "type": "text",
  "content": "{{props.user.name}} - {{props.user.role}}"
}

Collection Item Bindings

{
  "type": "collection",
  "data": { "type": "action.http", "method": "GET", "url": "/api/products" },
  "itemTemplate": {
    "type": "card",
    "child": {
      "type": "layout.column",
      "spacing": 8,
      "children": [
        {
          "type": "text",
          "content": "#{{index + 1}}: {{item.name}}"
        },
        {
          "type": "text",
          "content": "Price: ${{item.price}}"
        }
      ]
    }
  }
}

Expressions

Bindings support JavaScript expressions:

// Math
"{{state.count + 10}}"
"{{state.price * 1.1}}"

// String operations
"{{state.firstName + ' ' + state.lastName}}"
"{{state.text.toUpperCase()}}"

// Conditionals
"{{state.count > 10 ? 'High' : 'Low'}}"
"{{state.user.role === 'admin' ? 'Administrator' : 'User'}}"

// Boolean logic
"{{state.isActive && state.isVerified}}"
"{{!state.isEmpty}}"

// Array operations
"{{state.items.length}}"
"{{state.users.map(u => u.name).join(', ')}}"

// Object access
"{{state.settings.theme || 'light'}}"

Examples

Dynamic Text

{
  "type": "text",
  "content": "You have {{state.notifications.length}} new notifications",
  "style": "bodyMedium"
}

Conditional Styling

{
  "type": "text",
  "content": "{{state.status}}",
  "color": "{{state.status === 'active' ? '#00695C' : '#BA1A1A'}}"
}

Dynamic Button State

{
  "type": "button.filled",
  "label": "{{state.loading ? 'Loading...' : 'Submit'}}",
  "disabled": "{{state.loading || !state.formValid}}"
}

URL Construction

{
  "type": "action.http",
  "method": "GET",
  "url": "https://api.example.com/users/{{state.selectedUserId}}/profile"
}

Complex Expression

{
  "type": "text",
  "content": "Total: ${{state.cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0).toFixed(2)}}"
}

πŸ’‘ Reactivity

When state changes, all components using bindings to that state automatically update. No manual refresh needed!

πŸ’‘ Type Safety

Bindings are evaluated at runtime. Use safe navigation: {{state.user?.name || 'Guest'}} to avoid errors when data is missing.

πŸ’‘ Performance

Keep expressions simple. Complex calculations in bindings that appear many times (like in collections) can impact performance.

License

PineUI is licensed under Apache License 2.0 with Commons Clause.

πŸ“œ What does this mean?

You CAN:

  • βœ… Use PineUI for free in your projects (commercial or non-commercial)
  • βœ… Modify the source code
  • βœ… Distribute copies of PineUI
  • βœ… Use it in proprietary software

You CANNOT:

  • ❌ Sell PineUI itself as a product or service
  • ❌ Offer hosting or consulting services where the primary value comes from PineUI

The Commons Clause prevents companies from taking this open-source project and selling it as a competing commercial product, while keeping it free for everyone to use in their own applications.

Full License Text

Apache License 2.0 with Commons Clause

License Agreement

PineUI - Server-Driven UI for AI-Native Applications
Copyright (c) 2026 Luma Ventures Ltda (CNPJ: 21.951.820/0001-39)

Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.

---

"Commons Clause" License Condition v1.0

The Software is provided to you by the Licensor under the License, as defined
below, subject to the following condition.

Without limiting other conditions in the License, the grant of rights under the
License will not include, and the License does not grant to you, the right to
Sell the Software.

For purposes of the foregoing, "Sell" means practicing any or all of the rights
granted to you under the License to provide to third parties, for a fee or
other consideration (including without limitation fees for hosting or
consulting/support services related to the Software), a product or service
whose value derives, entirely or substantially, from the functionality of the
Software. Any license notice or attribution required by the License must also
include this Commons Clause License Condition notice.

Software: PineUI

License: Apache 2.0

Licensor: Luma Ventures Ltda
CNPJ: 21.951.820/0001-39

---

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

   "License" shall mean the terms and conditions for use, reproduction, and
   distribution as defined by Sections 1 through 9 of this document.

   "Licensor" shall mean the copyright owner or entity authorized by the
   copyright owner that is granting the License.

   "Legal Entity" shall mean the union of the acting entity and all other
   entities that control, are controlled by, or are under common control with
   that entity. For the purposes of this definition, "control" means (i) the
   power, direct or indirect, to cause the direction or management of such
   entity, whether by contract or otherwise, or (ii) ownership of fifty
   percent (50%) or more of the outstanding shares, or (iii) beneficial
   ownership of such entity.

   "You" (or "Your") shall mean an individual or Legal Entity exercising
   permissions granted by this License.

   "Source" form shall mean the preferred form for making modifications,
   including but not limited to software source code, documentation source,
   and configuration files.

   "Object" form shall mean any form resulting from mechanical transformation
   or translation of a Source form, including but not limited to compiled
   object code, generated documentation, and conversions to other media types.

   "Work" shall mean the work of authorship, whether in Source or Object form,
   made available under the License, as indicated by a copyright notice that
   is included in or attached to the work.

   "Derivative Works" shall mean any work, whether in Source or Object form,
   that is based on (or derived from) the Work and for which the editorial
   revisions, annotations, elaborations, or other modifications represent, as
   a whole, an original work of authorship. For the purposes of this License,
   Derivative Works shall not include works that remain separable from, or
   merely link (or bind by name) to the interfaces of, the Work and Derivative
   Works thereof.

   "Contribution" shall mean any work of authorship, including the original
   version of the Work and any modifications or additions to that Work or
   Derivative Works thereof, that is intentionally submitted to Licensor for
   inclusion in the Work by the copyright owner or by an individual or Legal
   Entity authorized to submit on behalf of the copyright owner.

2. Grant of Copyright License.

   Subject to the terms and conditions of this License, each Contributor hereby
   grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
   irrevocable copyright license to reproduce, prepare Derivative Works of,
   publicly display, publicly perform, sublicense, and distribute the Work and
   such Derivative Works in Source or Object form.

3. Grant of Patent License.

   Subject to the terms and conditions of this License, each Contributor hereby
   grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
   irrevocable patent license to make, have made, use, offer to sell, sell,
   import, and otherwise transfer the Work.

4. Redistribution.

   You may reproduce and distribute copies of the Work or Derivative Works
   thereof in any medium, with or without modifications, and in Source or
   Object form, provided that You meet the following conditions:

   (a) You must give any other recipients of the Work or Derivative Works a
       copy of this License; and

   (b) You must cause any modified files to carry prominent notices stating
       that You changed the files; and

   (c) You must retain, in the Source form of any Derivative Works that You
       distribute, all copyright, patent, trademark, and attribution notices
       from the Source form of the Work; and

   (d) You must include a copy of this License and the Commons Clause restriction.

5. Submission of Contributions.

   Unless You explicitly state otherwise, any Contribution intentionally
   submitted for inclusion in the Work by You to the Licensor shall be under
   the terms and conditions of this License, without any additional terms or
   conditions.

6. Trademarks.

   This License does not grant permission to use the trade names, trademarks,
   service marks, or product names of the Licensor, except as required for
   reasonable and customary use in describing the origin of the Work.

7. Disclaimer of Warranty.

   Unless required by applicable law or agreed to in writing, Licensor provides
   the Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
   either express or implied, including, without limitation, any warranties or
   conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
   PARTICULAR PURPOSE.

8. Limitation of Liability.

   In no event and under no legal theory shall any Contributor be liable to You
   for damages, including any direct, indirect, special, incidental, or
   consequential damages of any character arising as a result of this License or
   out of the use or inability to use the Work.

9. Accepting Warranty or Additional Liability.

   While redistributing the Work or Derivative Works thereof, You may choose to
   offer, and charge a fee for, acceptance of support, warranty, indemnity, or
   other liability obligations and/or rights consistent with this License.
   However, in accepting such obligations, You may act only on Your own behalf
   and on Your sole responsibility.

---

For commercial licensing inquiries, please contact:
Luma Ventures Ltda
CNPJ: 21.951.820/0001-39
Email: wupsbr@gmail.com