React component library for rapid mobile app prototyping with iOS/Android styles
npm install protomobilekitReact component library for rapid mobile app prototyping. Build iOS and Android-style interfaces with a unified API, complete with navigation, authentication, state management, and 50+ UI components.
- Installation
- Quick Start
- Best Practices
- Project Structure
- Entity Registration
- Frame Registration
- User & Role Registration
- Flow Registration
- Core Concepts
- Canvas & Apps
- Canvas SDK
- Preview Mode
- Navigation
- Screen Architecture (defineScreen)
- State Management
- Authentication
- Events
- Forms
- Frames & Flows
- UI Components
- Theming
- Utilities
- DevTools
- Bypassing Guards (useDevToolsMode)
- Automation (MCP/Puppeteer)
- TypeScript
---
``bash`
npm install protomobilekit
`bash`
npm install react react-dom zustand
ProtoMobileKit uses Tailwind CSS 4. Configure your CSS file:
`css
/ src/index.css /
@import "tailwindcss";
/ Include protomobilekit classes /
@source "../node_modules/protomobilekit/dist/*/.js";
`
---
`tsx
import { Canvas, defineApp, Navigator, Screen, Header, Button, Text, ThemeProvider, DevTools } from 'protomobilekit'
function App() {
return (
apps={[
defineApp({
id: 'myapp',
name: 'My App',
device: 'iphone-14',
component: () =>
}),
]}
layout="row"
showLabels
/>
)
}
function MyApp() {
return (
)
}
function HomeScreen() {
const { navigate } = useNavigate()
return (
---
Best Practices
Follow these conventions for a well-organized prototype that's easy to maintain, document, and automate.
$3
Recommended folder structure for multi-app prototypes:
`
src/
├── apps/ # Each app in separate folder
│ ├── customer/
│ │ ├── index.tsx # App component with Navigator
│ │ ├── screens/ # Screen components
│ │ │ ├── HomeScreen.tsx
│ │ │ ├── OrdersScreen.tsx
│ │ │ └── ProfileScreen.tsx
│ │ ├── frames.ts # Frame definitions for this app
│ │ └── users.ts # Test users for this app
│ ├── admin/
│ │ ├── index.tsx
│ │ ├── screens/
│ │ ├── frames.ts
│ │ └── users.ts
│ └── courier/
│ └── ...
├── entities/ # Shared entity definitions
│ ├── index.ts # Export all + seed function
│ ├── Order.ts
│ ├── Restaurant.ts
│ ├── User.ts
│ └── Courier.ts
├── flows/ # User flow definitions
│ └── index.ts
├── App.tsx # Main app with Canvas
└── main.tsx # Entry point
`$3
Rule: Define all entities BEFORE app renders. Entities are shared across all apps.
`tsx
// src/entities/Order.ts
import { entity } from 'protomobilekit'
import type { InferEntity } from 'protomobilekit'export const Order = entity({
name: 'Order',
fields: {
status: { type: 'enum', values: ['pending', 'preparing', 'delivering', 'delivered'] as const },
customerId: 'string',
courierId: { type: 'string', default: null },
restaurantId: 'string',
items: 'string', // JSON string
total: 'number',
address: 'string',
createdAt: 'date',
},
})
export type Order = InferEntity
``tsx
// src/entities/index.ts
import { useStore, resetStore } from 'protomobilekit'// Import all entities (this registers them)
export * from './Order'
export * from './Restaurant'
export * from './User'
export * from './Courier'
// Seed initial data
export function seedData() {
const store = useStore.getState()
const silent = { silent: true } // No events during seeding
// Check if already seeded
if (store.getAll('Restaurant').length > 0) return
// Restaurants
store.create('Restaurant', { id: 'r1', name: 'Sushi Master', rating: 4.8 }, silent)
store.create('Restaurant', { id: 'r2', name: 'Pizza Place', rating: 4.5 }, silent)
// Orders with different statuses
store.create('Order', {
id: 'o1',
status: 'pending',
customerId: 'alice',
restaurantId: 'r1',
items: JSON.stringify([{ name: 'Dragon Roll', qty: 2, price: 450 }]),
total: 900,
address: '123 Main St',
}, silent)
// ... more seed data
}
``tsx
// src/main.tsx
import { seedData } from './entities'// Seed data BEFORE rendering
seedData()
ReactDOM.createRoot(document.getElementById('root')!).render(
)
`$3
Use
defineScreen to create screens. This:
- Creates a React component for use in Navigator
- Automatically registers screen for DevTools
- Enables direct URL access to screens
- Separates View (UI) from useCase (logic)`tsx
// src/apps/customer/screens/restaurant/index.ts
import { defineScreen } from 'protomobilekit'
import { RestaurantView } from './RestaurantView'
import { useRestaurantCase } from './useRestaurantCase'
import { resolveRestaurantParams } from './resolve'
import { restaurantParamsCodec } from './params'export const RestaurantScreen = defineScreen({
appId: 'customer',
name: 'restaurant',
View: RestaurantView,
useCase: useRestaurantCase,
resolveParams: resolveRestaurantParams,
paramsCodec: restaurantParamsCodec,
tags: ['detail', 'restaurant'],
description: 'Restaurant menu with dishes',
})
`Then use in Navigator:
`tsx
// src/apps/customer/index.tsx
import { RestaurantScreen } from './screens/restaurant'
`$3
For additional DevTools organization (flows, custom navigation), use frames:
`tsx
import { defineFrames, createFrame } from 'protomobilekit'
import { RestaurantScreen } from './screens/restaurant'const restaurantFrame = createFrame({
id: 'restaurant',
name: '1.2 Restaurant',
description: 'Restaurant menu with dishes',
// Can use defineScreen component
component: RestaurantScreen,
tags: ['detail'],
// Custom navigation with default params
onNavigate: (nav) => nav.navigate('restaurant', { id: 'r1' }),
})
defineFrames({
appId: 'customer',
appName: 'Customer App',
initial: 'home',
frames: [homeFrame, restaurantFrame, ordersFrame],
})
`Note:
defineScreen already registers screens for DevTools. Frames are only needed for:
- Custom display names (e.g., "1.2 Restaurant")
- Custom navigation handlers with default params
- Organizing screens into flows$3
Rule: Define test users for EACH app. This enables:
- Quick user switching in DevTools Auth Panel
- Testing different roles/permissions
- Realistic prototype demos
`tsx
// src/apps/customer/users.ts
import { defineUsers, defineRoles } from 'protomobilekit'// Define roles first
defineRoles({
appId: 'customer',
roles: [
{ value: 'regular', label: 'Regular' },
{ value: 'premium', label: 'Premium', color: '#f59e0b' },
{ value: 'vip', label: 'VIP', color: '#8b5cf6' },
],
})
// Define test users
defineUsers({
appId: 'customer',
users: [
{
id: 'alice',
name: 'Alice Johnson',
phone: '+1 234 567 8901',
role: 'premium',
avatar: 'https://i.pravatar.cc/150?u=alice',
// Custom fields for this app
address: '123 Main Street, New York',
defaultPayment: 'card',
},
{
id: 'bob',
name: 'Bob Smith',
phone: '+1 234 567 8902',
role: 'regular',
avatar: 'https://i.pravatar.cc/150?u=bob',
},
{
id: 'charlie',
name: 'Charlie Brown',
phone: '+1 234 567 8903',
role: 'vip',
avatar: 'https://i.pravatar.cc/150?u=charlie',
},
],
})
``tsx
// src/apps/admin/users.ts
import { defineUsers, defineRoles } from 'protomobilekit'defineRoles({
appId: 'admin',
roles: [
{ value: 'manager', label: 'Manager' },
{ value: 'superadmin', label: 'Super Admin', color: '#ef4444' },
],
})
defineUsers({
appId: 'admin',
users: [
{ id: 'admin1', name: 'Admin User', phone: '+1 999 000 0001', role: 'manager' },
{ id: 'super', name: 'Super Admin', phone: '+1 999 000 0000', role: 'superadmin' },
],
})
`Tip: User IDs should match entity foreign keys (e.g.,
order.customerId = 'alice').$3
Rule: Define user flows for key journeys. This enables:
- Task tracking in DevTools Flows Panel
- Acceptance criteria documentation
- QA testing checklists
`tsx
// src/flows/index.ts
import { defineFlow } from 'protomobilekit'
import { homeFrame, ordersFrame, orderDetailsFrame } from '../apps/customer/frames'
import { checkoutFrame } from '../apps/customer/frames'// Order placement flow
defineFlow({
id: 'place-order',
name: 'Place Order',
description: 'Complete flow from browsing to order confirmation',
appId: 'customer',
steps: [
{
frame: homeFrame,
tasks: [
'Browse restaurant list',
'Use search to find restaurant',
'Apply cuisine filter',
'Select a restaurant',
],
},
{
frame: menuFrame,
tasks: [
'View menu categories',
'Add items to cart',
'Customize item (if available)',
'View cart summary',
],
},
{
frame: checkoutFrame,
tasks: [
'Confirm delivery address',
'Select payment method',
'Apply promo code (optional)',
'Place order',
],
},
{
frame: orderDetailsFrame,
tasks: [
'View order confirmation',
'See estimated delivery time',
'Track order status',
],
},
],
})
// Order tracking flow
defineFlow({
id: 'track-order',
name: 'Track Order',
description: 'Monitor order from preparation to delivery',
appId: 'customer',
steps: [
{
frame: ordersFrame,
tasks: ['View active orders', 'Select order to track'],
},
{
frame: orderDetailsFrame,
tasks: [
'View order timeline',
'See courier information',
'Contact courier (if available)',
'Confirm delivery',
],
},
],
})
`$3
`tsx
// src/main.tsx
import ReactDOM from 'react-dom/client'
import { ThemeProvider, DevTools } from 'protomobilekit'// 1. Import entities (registers them)
import { seedData } from './entities'
// 2. Import user definitions (registers them)
import './apps/customer/users'
import './apps/admin/users'
import './apps/courier/users'
// 3. Import frame definitions (registers them)
import './apps/customer/frames'
import './apps/admin/frames'
import './apps/courier/frames'
// 4. Import flow definitions (registers them)
import './flows'
// 5. Seed data
seedData()
// 6. Import main app
import App from './App'
ReactDOM.createRoot(document.getElementById('root')!).render(
)
`$3
Before considering your prototype complete:
- [ ] Entities: All data types defined with proper fields and types
- [ ] Seed Data: Realistic test data for all entities
- [ ] Frames: Every screen registered with description and tags
- [ ] Users: Test users defined for each app with appropriate roles
- [ ] Flows: Key user journeys documented as flows
- [ ] Hash Routing: Navigator uses
useHash for URL-based navigation
- [ ] DevTools: Enabled for easy debugging and demonstrationThis structure ensures your prototype is:
- Discoverable - All screens accessible via Frame Browser
- Testable - Quick user switching and flow tracking
- Automatable - MCP/Puppeteer can navigate and screenshot any screen
- Documentable - Frame metadata can generate documentation
---
Core Concepts
$3
Canvas is the main container that displays multiple app instances in device frames.
#### Basic Setup
`tsx
import { Canvas, defineApp, ThemeProvider, DevTools } from 'protomobilekit'function App() {
return (
apps={[
defineApp({
id: 'customer',
name: 'Customer App',
device: 'iphone-14',
component: () => ,
}),
defineApp({
id: 'admin',
name: 'Admin Panel',
device: 'iphone-14-pro-max',
component: () => ,
}),
]}
layout="row" // 'row' | 'grid' | 'freeform'
gap={24} // Gap between devices (px)
scale={1} // Device scale (0.5 - 1.5)
showLabels // Show app names below devices
background="#f3f4f6"
/>
)
}
`#### Available Devices
`tsx
// Device presets
type DeviceType =
| 'iphone-14'
| 'iphone-14-pro'
| 'iphone-14-pro-max'
| 'iphone-se'
| 'pixel-7'
| 'pixel-7-pro'
| 'galaxy-s23'
| 'galaxy-s23-ultra'// Or custom dimensions
defineApp({
id: 'custom',
name: 'Custom Device',
deviceConfig: {
width: 375,
height: 812,
borderRadius: 40,
notch: true,
},
component: () => ,
})
`#### useApp Hook
Access app context and auth from any component:
`tsx
import { useApp } from 'protomobilekit'function ProfileScreen() {
const {
appId, // Current app ID
appName, // Current app name
user, // Current authenticated user
userId, // User ID (string | null)
isAuthenticated,
login,
logout,
} = useApp()
return (
App: {appName}
User: {user?.name}
)
}
`#### Canvas SDK
Programmatic API for controlling Canvas - show/hide apps, fullscreen mode, and navigation.
`tsx
import { canvas } from 'protomobilekit'// Get apps
canvas.getApps() // All registered apps
canvas.getApp('admin') // Single app by ID
canvas.getVisibleApps() // Only visible apps
canvas.getHiddenApps() // Only hidden apps
// Visibility control
canvas.show('admin') // Show app
canvas.hide('admin') // Hide app
canvas.toggle('admin') // Toggle visibility
canvas.showAll() // Show all apps
canvas.showOnly('admin') // Hide all except one
canvas.isVisible('admin') // Check if visible
// Fullscreen mode (single app, no device frame, max-width 720px)
canvas.fullscreen('admin') // Enter fullscreen
canvas.exitFullscreen() // Exit fullscreen
canvas.toggleFullscreen('admin') // Toggle fullscreen
canvas.isFullscreen('admin') // Check if fullscreen
canvas.hasFullscreen() // Any app in fullscreen?
canvas.getFullscreenApp() // Get fullscreen app or null
// Navigation (for automation/MCP)
canvas.navigateTo('admin', 'orders') // Navigate to screen
canvas.navigateTo('admin', 'orderDetails', { id: 'o1' }) // With params
canvas.getCurrentRoute() // { appId, screen, params }
canvas.getScreens('admin') // All screens for app
canvas.getScreenNames('admin') // ['home', 'orders', ...]
// Subscribe to changes
const unsubscribe = canvas.subscribe(() => {
console.log('Canvas state changed')
})
// Reset state
canvas.reset() // Show all, exit fullscreen
`#### Canvas Props
`tsx
apps={apps}
layout="row"
gap={32}
scale={1}
showLabels={true}
background="#f3f4f6"
// Fullscreen mode options
hideExitFullscreen={false} // Hide "Exit Fullscreen" button
showFrameInFullscreen={false} // Show device frame in fullscreen (for screenshots)
/>
`---
$3
Preview mode allows rendering screens in isolation - without Navigator guards, with mock auth, and with device frames. Perfect for:
- Automated screenshots
- Documentation generation
- Component testing
- LLM/MCP integration
#### usePreviewMode Hook
Parse URL parameters to detect preview mode:
`tsx
// URL: /prototype?mode=preview&screen=home&user=user-1&app=customerimport { usePreviewMode, ScreenPreview, Canvas } from 'protomobilekit'
function App() {
const { isPreview, screenId, userId, appId } = usePreviewMode()
if (isPreview && screenId) {
return (
screen={screenId}
appId={appId}
userId={userId}
showFrame={true}
device="iphone-14"
/>
)
}
return
}
`#### ScreenPreview Component
Render any registered screen in isolation:
`tsx
import { ScreenPreview } from 'protomobilekit'// Basic - no frame
// With device frame (for screenshots)
screen="profile"
appId="customer"
userId="alice" // Mock auth with test user
showFrame={true}
device="iphone-14"
scale={1}
platform="ios"
/>
// With route params
screen="order-details"
appId="customer"
params={{ orderId: 'o1' }}
/>
// Custom background
screen="home"
appId="customer"
background="#1a1a1a"
/>
`#### MockAuthProvider
Wrap components with mock auth state (used internally by ScreenPreview):
`tsx
import { MockAuthProvider } from 'protomobilekit'// Using test user from registry
// Using direct user object
// Unauthenticated state
`#### Screenshot Automation Example
`javascript
// Puppeteer script for automated screenshots
const screens = ['home', 'orders', 'profile', 'settings']
const users = ['alice', 'bob']for (const screen of screens) {
for (const user of users) {
const url =
http://localhost:5173/prototype?mode=preview&screen=${screen}&user=${user}&app=customer
await page.goto(url)
await page.waitForTimeout(500)
await page.screenshot({ path: screenshots/${screen}-${user}.png })
}
}
`#### URL Parameters
| Parameter | Description | Example |
|-----------|-------------|---------|
|
mode | Must be preview to enable preview mode | mode=preview |
| screen | Screen name to render | screen=home |
| user | Test user ID for mock auth | user=alice |
| app | App ID | app=customer |
| navigator | Navigator ID (default: main) | navigator=main |#### Global SDK for Automation (MCP/Puppeteer)
Canvas SDK is exposed globally as
window.__CANVAS_SDK__ for browser automation:`javascript
// In Puppeteer
await page.goto('http://localhost:5173')// Get list of apps
const apps = await page.evaluate(() => {
return window.__CANVAS_SDK__.getApps().map(a => ({ id: a.id, name: a.name }))
})
// [{ id: 'customer', name: 'Customer App' }, { id: 'admin', name: 'Admin' }]
// Get screens for an app
const screens = await page.evaluate(() => {
return window.__CANVAS_SDK__.getScreenNames('admin')
})
// ['home', 'orders', 'orderDetails', 'settings']
// Navigate to specific screen
await page.evaluate(() => {
window.__CANVAS_SDK__.navigateTo('admin', 'orders')
})
// Wait for render and take screenshot
await page.waitForTimeout(500)
await page.screenshot({ path: 'admin-orders.png' })
`Use cases:
- MCP Server - LLM can request screenshots of specific screens
- Visual testing - Automated screenshot comparison
- Documentation - Generate screen captures programmatically
- E2E testing - Navigate and verify screen state
---
$3
Unified navigation system supporting both stack and tab patterns.
#### Stack Navigation
`tsx
import { Navigator, useNavigate, useRoute } from 'protomobilekit'function App() {
return (
)
}
function HomeScreen() {
const { navigate, goBack, replace, reset, canGoBack } = useNavigate()
return (
{/ Navigate to screen with params /}
{/ Replace current screen /}
{/ Reset navigation stack /}
{/ Go back /}
{canGoBack() && (
)}
)
}
function DetailsScreen() {
const { params } = useRoute<{ id: string }>()
return (
}>
Item ID: {params.id}
)
}
`#### Tab Navigation
`tsx
import { Navigator } from 'protomobilekit'// Icons (use any icon library)
const HomeIcon = () =>
const OrdersIcon = () =>
const ProfileIcon = () =>
function App() {
return (
initial="home"
type="tabs"
tabBarPosition="bottom" // 'bottom' | 'top'
>
name="home"
component={HomeScreen}
icon={ }
label="Home"
/>
name="orders"
component={OrdersScreen}
icon={ }
label="Orders"
badge={3} // Badge count
/>
name="profile"
component={ProfileScreen}
icon={ }
label="Profile"
/>
{/ Non-tab screens (no icon) - accessible via navigate() /}
)
}
`#### Navigation Options
`tsx
initial="home"
type="stack" // 'stack' | 'tabs'
tabBarPosition="bottom" // 'bottom' | 'top' (for tabs)
tabBarHidden={false} // Hide tab bar
tabBarStyle={{ // Custom tab bar styles
backgroundColor: '#fff',
}}
screenOptions={{ // Default options for all screens
headerShown: true,
}}
useHash={false} // Enable hash-based URL routing
id="main" // Navigator ID for screen registry
>
`#### Hash-Based URL Routing
Enable URL synchronization with
useHash prop. Navigation state syncs to URL hash (e.g., #/screen?param=value).`tsx
// Enable hash routing
// URLs:
// #/home
// #/orders
// #/details?id=123
`Benefits:
- Bookmarkable - users can share/bookmark specific screens
- Browser navigation - back/forward buttons work
- External access - LLM bots and documentation tools can link to screens
- Deep linking - open app at specific screen with params
#### Screen Registry API
Access registered screens programmatically for external routing, documentation, or tooling.
`tsx
import {
getScreens,
getScreenNames,
hasScreen,
subscribeToScreenRegistry,
parseHash,
buildHash,
} from 'protomobilekit'// Get all registered screens
const screens = getScreens()
// [
// { name: 'home', navigatorId: 'main', navigatorType: 'stack', options: {...} },
// { name: 'orders', navigatorId: 'main', navigatorType: 'tabs', tab: { label: 'Orders', icon: ... } },
// ]
// Get screens for specific navigator
const mainScreens = getScreens('main')
// Get just screen names
const names = getScreenNames()
// ['home', 'orders', 'profile', 'details']
// Check if screen exists
if (hasScreen('orders')) {
// screen exists
}
// Subscribe to registry changes
const unsubscribe = subscribeToScreenRegistry(() => {
console.log('Screens updated:', getScreens())
})
// Hash utilities for manual URL building
parseHash('#/orders?id=123')
// { screen: 'orders', params: { id: '123' } }
buildHash('orders', { id: '123', tab: 'active' })
// '#/orders?id=123&tab=active'
`Use cases:
- Generate documentation - list all screens automatically
- External navigation - route to screens from outside React
- Testing - verify screen registration
- LLM integration - bots can discover available screens
---
$3
Create screens with View/ViewModel separation for better testability and code organization.
#### Basic Usage
`tsx
import { defineScreen, useNavigate, useQuery, useRepo } from 'protomobilekit'
import type { ViewModel } from 'protomobilekit'// 1. Define types
interface RestaurantParams {
id: string
}
interface RestaurantState {
restaurant: Restaurant | null
dishes: Dish[]
}
interface RestaurantActions {
goBack: () => void
orderDish: (dishId: string) => void
}
type RestaurantVM = ViewModel
// 2. Create View (pure UI, only renders VM)
function RestaurantView({ vm }: { vm: RestaurantVM }) {
const { state, actions } = vm
if (!state.restaurant) {
return Restaurant not found
}
return (
}>
items={state.dishes}
renderItem={(dish) => (
actions.orderDish(dish.id)}>
{dish.name} - ${dish.price}
)}
/>
)
}
// 3. Create useCase (logic + data → ViewModel)
function useRestaurantCase(params: RestaurantParams): RestaurantVM {
const { goBack } = useNavigate()
const { items: restaurants } = useQuery('Restaurant', {
filter: r => r.id === params.id
})
const { items: dishes } = useQuery('Dish', {
filter: d => d.restaurantId === params.id
})
const { create: createOrder } = useRepo('Order')
return {
state: {
restaurant: restaurants[0] ?? null,
dishes,
},
actions: {
goBack,
orderDish: (dishId) => {
const dish = dishes.find(d => d.id === dishId)
if (dish) {
createOrder({ dishId, price: dish.price })
}
},
},
}
}
// 4. Define screen (returns React component)
export const RestaurantScreen = defineScreen({
appId: 'customer',
name: 'restaurant',
View: RestaurantView,
useCase: useRestaurantCase,
})
// 5. Use in Navigator
`#### With Params Resolution
Use
resolveParams to fill in missing params from context (e.g., get first restaurant if no ID provided):`tsx
import { defineScreen, type ResolverContext, type ResolveResult } from 'protomobilekit'// Resolver function - fills defaults, validates params
function resolveRestaurantParams(
given: Partial,
ctx: ResolverContext
): ResolveResult {
// If ID provided, use it
if (given.id) {
return { ok: true, params: { id: given.id } }
}
// Otherwise, get first restaurant
const restaurant = ctx.repo('Restaurant').first()
if (restaurant) {
return { ok: true, params: { id: restaurant.id } }
}
// No restaurant available
return { ok: false, reason: 'No restaurants available' }
}
export const RestaurantScreen = defineScreen({
appId: 'customer',
name: 'restaurant',
View: RestaurantView,
useCase: useRestaurantCase,
resolveParams: resolveRestaurantParams, // ← Added
})
`#### With URL Params Coercion
Use
paramsCodec to convert URL string params to typed values:`tsx
import { defineScreen, coerce, coerceString } from 'protomobilekit'// Codec for URL → typed params conversion
const restaurantParamsCodec = {
coerce: (raw: Record) => ({
id: coerceString(raw.id),
}),
serialize: (params: RestaurantParams) => ({
id: params.id,
}),
}
export const RestaurantScreen = defineScreen({
appId: 'customer',
name: 'restaurant',
View: RestaurantView,
useCase: useRestaurantCase,
resolveParams: resolveRestaurantParams,
paramsCodec: restaurantParamsCodec, // ← Added
})
`#### ResolverContext API
The
ctx object in resolveParams provides type-safe data access:`tsx
function resolveParams(given: Partial, ctx: ResolverContext) {
// Access entity repository
const restaurant = ctx.repo('Restaurant').first()
const order = ctx.repo('Order').get(given.orderId)
const pending = ctx.repo('Order').find(o => o.status === 'pending')
const all = ctx.repo('Order').all() // Access fixture refs (deterministic defaults)
const defaultId = ctx.ref('customer', 'defaultRestaurantId')
// Current user
const user = ctx.user
// App ID
const appId = ctx.appId
return { ok: true, params: { ... } }
}
`#### Type-Safe Entities (Module Augmentation)
For full type safety in
ctx.repo(), augment the EntityMap:`tsx
// src/entities/types.ts
import type { Restaurant, Order, Dish } from './index'declare module 'protomobilekit' {
interface EntityMap {
Restaurant: Restaurant
Order: Order
Dish: Dish
}
}
`Now
ctx.repo('Restaurant') returns EntityRepo with proper typing.#### Fixture Refs
Set deterministic default values for screens accessed via DevTools or direct URL:
`tsx
import { setFixtureRefs } from 'protomobilekit'// Set after seeding data
setFixtureRefs('customer', {
defaultRestaurantId: 'r1',
defaultOrderId: 'o1',
})
// Use in resolveParams
function resolveParams(given, ctx) {
const id = given.id ?? ctx.ref('customer', 'defaultRestaurantId')
// ...
}
`#### Coerce Helpers
Built-in helpers for URL param coercion:
`tsx
import {
coerceString, // unknown → string | undefined
coerceNumber, // unknown → number | undefined
coerceBoolean, // unknown → boolean | undefined
coerceEnum, // unknown → EnumValue | undefined
coerceJson, // unknown → ParsedJSON | undefined
} from 'protomobilekit'const paramsCodec = {
coerce: (raw) => ({
id: coerceString(raw.id),
page: coerceNumber(raw.page),
active: coerceBoolean(raw.active),
status: coerceEnum(raw.status, ['pending', 'completed'] as const),
filters: coerceJson(raw.filters),
}),
serialize: (params) => ({ ... }),
}
`#### File Structure
Recommended structure for defineScreen:
`
screens/
└── restaurant/
├── index.ts # defineScreen + exports
├── types.ts # Params, State, Actions, VM types
├── RestaurantView.tsx # Pure UI component
├── useRestaurantCase.ts # useCase hook
├── resolve.ts # resolveParams function
└── params.ts # paramsCodec
`---
$3
Entity-based state management with Zustand, automatic persistence to localStorage.
#### Entity Definition
`tsx
import { entity, seed, fake } from 'protomobilekit'
import type { InferEntity } from 'protomobilekit'// Define entity with type inference
const Order = entity({
name: 'Order',
fields: {
status: { type: 'enum', values: ['pending', 'confirmed', 'delivered'] as const },
customerId: 'string',
courierId: { type: 'string', default: null },
total: 'number',
items: 'string', // JSON string
address: 'string',
createdAt: 'date',
},
// Optional: custom mock generator
mock: () => ({
total: Math.random() * 100,
items: JSON.stringify([{ name: 'Pizza', qty: 1 }]),
}),
})
// Infer TypeScript type from entity
type Order = InferEntity
// { id: string, status: 'pending' | 'confirmed' | 'delivered', customerId: string, ... }
`#### Field Types
`tsx
type FieldType =
| 'string' // Random lorem words
| 'number' // Random integer 1-1000
| 'boolean' // Random true/false
| 'date' // Timestamp (Date.now())
| 'email' // faker.internet.email()
| 'phone' // faker.phone.number()
| 'url' // faker.internet.url()
| 'image' // faker.image.url()
| 'uuid' // UUID string
| 'enum' // Random from values array
| 'relation' // Foreign key (null by default)// Extended field definition
const User = entity({
name: 'User',
fields: {
// Simple type
name: 'string',
// With custom faker path
firstName: { type: 'string', faker: 'person.firstName' },
lastName: { type: 'string', faker: 'person.lastName' },
avatar: { type: 'image', faker: 'image.avatar' },
// With default value
role: { type: 'enum', values: ['user', 'admin'] as const, default: 'user' },
// Enum type
status: { type: 'enum', values: ['active', 'inactive'] as const },
// Relation (foreign key)
companyId: { type: 'relation', entity: 'Company' },
},
})
`#### CRUD Operations with useRepo
`tsx
import { useRepo, useQuery, useEntity, useRelation } from 'protomobilekit'function OrdersScreen() {
// Get repository for CRUD operations
const orders = useRepo('Order')
// Create
const createOrder = () => {
const newOrder = orders.create({
status: 'pending',
customerId: 'user-1',
total: 29.99,
items: JSON.stringify([{ name: 'Burger', qty: 2 }]),
address: '123 Main St',
})
console.log('Created:', newOrder.id)
}
// Read all
const allOrders = orders.getAll()
// Read by ID
const order = orders.getById('order-123')
// Update
const confirmOrder = (id: string) => {
orders.update(id, { status: 'confirmed' })
}
// Delete
const cancelOrder = (id: string) => {
orders.remove(id)
}
return (...)
}
`#### useQuery for Filtered Data
`tsx
import { useQuery } from 'protomobilekit'function PendingOrders() {
const { items, total, isEmpty, hasMore } = useQuery('Order', {
// Filter
filter: (order) => order.status === 'pending',
// Sort (newest first)
sort: (a, b) => b.createdAt - a.createdAt,
// Pagination
limit: 10,
offset: 0,
})
if (isEmpty) {
return No pending orders
}
return (
items={items}
keyExtractor={(o) => o.id}
renderItem={(order) => (
Order #{order.id}
)}
/>
)
}
`#### useEntity for Single Item
`tsx
import { useEntity } from 'protomobilekit'function OrderDetails({ orderId }: { orderId: string }) {
const order = useEntity('Order', orderId)
if (!order) {
return Order not found
}
return (
Order #{order.id}
Status: {order.status}
Total: ${order.total}
)
}
`#### useRelation for Related Data
`tsx
import { useRelation, useRelations } from 'protomobilekit'function OrderWithCustomer({ orderId }: { orderId: string }) {
// Get single related entity
const customer = useRelation('Order', orderId, 'customerId', 'User')
return (
Customer: {customer?.name}
)
}
function CustomerOrders({ customerId }: { customerId: string }) {
// Get all related entities (one-to-many)
const orders = useRelations('Order', 'customerId', customerId)
return (
items={orders}
renderItem={(order) => {order.id} }
/>
)
}
`#### Data Seeding
`tsx
import { seed, fake, useStore, resetStore } from 'protomobilekit'// Seed multiple records
function initializeData() {
resetStore() // Clear existing data
// Generate 10 fake orders
const orders = seed(Order, 10)
console.log('Created orders:', orders)
// Generate single fake data (without saving)
const fakeOrder = fake(Order)
console.log('Fake order:', fakeOrder)
}
// Manual seeding with specific data
function seedProducts() {
const store = useStore.getState()
const products = [
{ id: 'p1', name: 'Pizza', price: 15, category: 'food' },
{ id: 'p2', name: 'Burger', price: 12, category: 'food' },
{ id: 'p3', name: 'Soda', price: 3, category: 'drink' },
]
for (const product of products) {
store.create('Product', product, { silent: true }) // No events
}
}
`#### Server Sync
`tsx
import { defineConfig, useSync } from 'protomobilekit'// Configure at app startup
defineConfig({
data: {
// Load data from server
onPull: async () => {
const res = await fetch('/api/data')
const data = await res.json()
// Return format: { CollectionName: { id: entity } }
return {
Order: data.orders.reduce((acc, o) => ({ ...acc, [o.id]: o }), {}),
User: data.users.reduce((acc, u) => ({ ...acc, [u.id]: u }), {}),
}
},
// Save data to server
onPush: async (data) => {
await fetch('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
},
},
})
// Use in components
function SyncButton() {
const { pull, push, isSyncing, lastSyncAt } = useSync()
useEffect(() => {
pull() // Load on mount
}, [])
return (
)
}
`#### Direct Store Access
`tsx
import { useStore } from 'protomobilekit'// Outside React components
const store = useStore.getState()
// All store methods
store.create('Order', { status: 'pending', ... })
store.update('Order', 'id', { status: 'confirmed' })
store.delete('Order', 'id')
store.getAll('Order')
store.getById('Order', 'id')
store.query('Order', o => o.status === 'pending')
// Sync helpers
store._mergeData({ Order: { 'id': {...} } })
store._getData() // Get all collections
`---
$3
Built-in OTP authentication with user registry for testing.
#### Define Users and Roles
`tsx
import { defineUsers, defineRoles } from 'protomobilekit'// Define roles for an app
defineRoles({
appId: 'customer',
roles: [
{ value: 'regular', label: 'Regular Customer' },
{ value: 'premium', label: 'Premium', color: '#f59e0b' },
{ value: 'vip', label: 'VIP', color: '#8b5cf6' },
],
})
// Define test users
defineUsers({
appId: 'customer',
users: [
{
id: 'alice',
name: 'Alice Johnson',
phone: '+1234567890',
role: 'premium',
avatar: 'https://example.com/alice.jpg',
// Custom fields
email: 'alice@example.com',
address: '123 Main St',
},
{
id: 'bob',
name: 'Bob Smith',
phone: '+0987654321',
role: 'regular',
},
],
})
`#### OTP Auth Component
`tsx
import { OTPAuth, useAuth, useIsAuthenticated } from 'protomobilekit'function LoginScreen() {
const { navigate } = useNavigate()
return (
onSuccess={() => navigate('home')}
countryCode="US" // Default country
otpLength={4} // OTP code length
allowTestUsers // Show "Quick Login" for test users
logo={ } // Optional logo
title="Welcome" // Optional title
subtitle="Sign in to continue"
/>
)
}
function ProfileScreen() {
const { user, logout, isAuthenticated, updateUser } = useAuth()
if (!isAuthenticated) {
return
}
return (
{user?.name}
{user?.phone}
)
}
`#### Auth Guards
`tsx
import { RequireAuth, RequireRole, AuthGuard, RoleGuard } from 'protomobilekit'// Require authentication
function ProtectedScreen() {
return (
}>
)
}
// Require specific role
function AdminPanel() {
return (
roles={['admin', 'superadmin']}
fallback={ }
>
)
}
// AuthGuard component (same as RequireAuth)
// RoleGuard component (same as RequireRole)
`#### Auth Hooks
`tsx
import { useAuth, useUser, useIsAuthenticated, useCurrentUserId, useRequireAuth } from 'protomobilekit'function MyComponent() {
// Full auth state
const { user, isAuthenticated, isLoading, login, logout, updateUser } = useAuth()
// Just the user
const user = useUser()
// Boolean check
const isLoggedIn = useIsAuthenticated()
// User ID only
const userId = useCurrentUserId()
// Redirect to login if not authenticated
useRequireAuth('/login')
}
`#### Quick Switch (DevTools)
`tsx
import { quickSwitch, quickLogout, getAppUsers } from 'protomobilekit'// Switch user instantly (for testing)
function DevUserSwitcher() {
const users = getAppUsers('customer')
return (
items={users}
renderItem={(user) => (
quickSwitch('customer', user.id)}>
{user.name}
)}
/>
)
}
// Logout from all apps
`---
$3
Global event bus for cross-component communication.
#### Basic Events
`tsx
import { dispatch, subscribe, useEvent, useDispatch } from 'protomobilekit'// Dispatch event (anywhere)
dispatch('order:created', { orderId: '123', total: 29.99 }, 'checkout')
// Subscribe to event (outside React)
const unsubscribe = subscribe('order:created', (payload, event) => {
console.log('Order created:', payload)
console.log('Event ID:', event.id)
console.log('Timestamp:', event.timestamp)
console.log('Source:', event.source)
})
// useEvent hook (in React)
function NotificationHandler() {
useEvent('order:created', (payload) => {
showToast(
Order ${payload.orderId} created!)
}) return null
}
// useDispatch hook
function CheckoutButton() {
const dispatchEvent = useDispatch()
const handleCheckout = () => {
dispatchEvent('order:created', { orderId: '123' })
}
return
}
`#### Typed Events
`tsx
import { defineEvents, createEvent } from 'protomobilekit'// Define typed events
const OrderEvents = defineEvents({
'order:created': (orderId: string, total: number) => ({ orderId, total }),
'order:updated': (orderId: string, changes: Partial) => ({ orderId, changes }),
'order:cancelled': (orderId: string, reason: string) => ({ orderId, reason }),
})
// Create type-safe dispatcher
const orderCreated = createEvent(OrderEvents, 'order:created')
// Use with full type safety
orderCreated.dispatch('order-123', 29.99)
// Subscribe with typed payload
orderCreated.subscribe((payload) => {
console.log(payload.orderId) // TypeScript knows this is string
console.log(payload.total) // TypeScript knows this is number
})
`#### Event History
`tsx
import { getEventHistory, clearEventHistory, useEventHistory, useLatestEvent } from 'protomobilekit'// Get all events
const history = getEventHistory()
// Clear history
clearEventHistory()
// React hooks
function EventDebugger() {
// All events
const allEvents = useEventHistory()
// Filtered events
const orderEvents = useEventHistory(['order:created', 'order:updated'])
// Latest event of type
const lastOrder = useLatestEvent<{ orderId: string }>('order:created')
return (
items={allEvents}
renderItem={(event) => (
{event.name}: {JSON.stringify(event.payload)}
)}
/>
)
}
`#### Wildcard Subscription
`tsx
// Subscribe to ALL events
subscribe('*', (payload, event) => {
console.log([${event.name}], payload)
})
`---
$3
Complete form state management with validation.
#### useForm Hook
`tsx
import { useForm, required, email, minLength, compose } from 'protomobilekit'function RegistrationForm() {
const form = useForm({
values: {
name: '',
email: '',
password: '',
confirmPassword: '',
},
validate: {
name: compose(required(), minLength(2)),
email: compose(required(), email()),
password: compose(required(), minLength(8)),
confirmPassword: compose(
required(),
match('password', 'Passwords must match')
),
},
validateOnBlur: true, // Validate when field loses focus
validateOnChange: false, // Don't validate on every keystroke
onSubmit: async (values) => {
await api.register(values)
},
onChange: (values) => {
console.log('Form changed:', values)
},
})
return (
)
}
`#### Built-in Validators
`tsx
import {
required,
minLength,
maxLength,
email,
phone,
url,
pattern,
range,
min,
max,
match,
custom,
compose,
optional,
} from 'protomobilekit'const validators = {
// Required field
name: required('Name is required'),
// String length
username: compose(
required(),
minLength(3, 'Min 3 characters'),
maxLength(20, 'Max 20 characters')
),
// Email validation
email: compose(required(), email('Invalid email')),
// Phone by country
phone: phone('us'), // 'us' | 'ru' | 'ua' | 'kz' | 'by' | 'default'
// URL validation
website: optional(url('Invalid URL')),
// Regex pattern
zipCode: pattern(/^\d{5}$/, 'Invalid zip code'),
// Number range
age: compose(required(), range(18, 120)),
quantity: compose(required(), min(1), max(100)),
// Match another field
confirmPassword: match('password', 'Passwords must match'),
// Custom validator
noSpaces: custom(
(value) => !value.includes(' '),
'No spaces allowed'
),
// Async validator
uniqueEmail: async(
async (email) => {
const exists = await api.checkEmail(email)
return !exists
},
'Email already exists'
),
}
`#### Form Components
`tsx
import { Form, FormField, FormRow, FormSection, FormActions } from 'protomobilekit'function ComplexForm() {
const form = useForm({ values: {...} })
return (
)
}
`#### Form State Access
`tsx
const form = useForm({ values: {...} })// Read values
form.values // All values
form.getValue('email') // Single value
// Set values
form.setValue('email', 'new@email.com')
form.setValues({ email: 'new@email.com', name: 'New Name' })
// Errors
form.errors // All errors
form.getError('email') // Single error
form.setError('email', 'Custom error')
form.setErrors({ email: 'Error 1', name: 'Error 2' })
// Touched state
form.touched // All touched
form.isTouched('email') // Single field
form.setTouched('email') // Mark as touched
// Form state
form.dirty // Any field changed from initial
form.valid // No errors
form.submitting // Submit in progress
form.submitted // Submit completed
// Validation
await form.validateField('email')
const isValid = await form.validateAll()
// Operations
await form.submit() // Validate + call onSubmit
form.reset() // Reset to initial values
form.reset({ email: '' }) // Reset with new values
form.clear() // Empty all fields
// Field props (for custom binding)
const props = form.getFieldProps('email')
// { value, onChange, onBlur, error, touched, disabled }
const props = form.field('email') // Alias
`#### Specialized Form Inputs
`tsx
import { PhoneInput, OTPInput, PinInput } from 'protomobilekit'// Phone input with country code
defaultCountry="US"
placeholder="(555) 123-4567"
/>
// OTP input (verification code)
length={6}
onComplete={(code) => verifyCode(code)}
autoFocus
/>
// PIN input (secure)
length={4}
secure // Hide digits
onComplete={(pin) => verifyPin(pin)}
/>
`#### Form Wizard
`tsx
import { FormWizard, useWizard } from 'protomobilekit'function MultiStepForm() {
return (
steps={[
{
id: 'personal',
title: 'Personal Info',
component: PersonalInfoStep,
},
{
id: 'address',
title: 'Address',
component: AddressStep,
},
{
id: 'payment',
title: 'Payment',
component: PaymentStep,
},
]}
onComplete={(data) => submitForm(data)}
/>
)
}
function PersonalInfoStep() {
const { next, data, setData } = useWizard()
return (
value={data.name || ''}
onChange={(e) => setData({ name: e.target.value })}
/>
)
}
`---
$3
Define screen frames and user flows for documentation and testing.
#### Define Frames
`tsx
import { defineFrames, createFrame } from 'protomobilekit'// Create reusable frame definitions
const homeFrame = createFrame({
id: 'home',
name: '1.1 Home',
description: 'Restaurant list with search and filters',
component: HomeScreen,
tags: ['main', 'entry'],
})
const menuFrame = createFrame({
id: 'menu',
name: '1.2 Menu',
description: 'Restaurant menu with categories',
component: MenuScreen,
tags: ['menu'],
// Custom navigation handler
onNavigate: (nav) => {
nav.navigate('menu', { restaurantId: 'demo' })
},
})
const cartFrame = createFrame({
id: 'cart',
name: '1.3 Cart',
description: 'Shopping cart with order summary',
component: CartScreen,
tags: ['checkout'],
})
// Register frames for an app
defineFrames({
appId: 'customer',
appName: 'Customer App',
initial: 'home',
frames: [homeFrame, menuFrame, cartFrame],
})
`#### Define Flows
`tsx
import { defineFlow, getFlowProgress, toggleStepComplete, toggleTaskComplete } from 'protomobilekit'// Define user flow (journey)
defineFlow({
id: 'order-flow',
name: 'Order Journey',
description: 'Complete flow from browsing to order delivery',
appId: 'customer',
steps: [
{
frame: homeFrame,
tasks: ['Browse restaurants', 'Use search', 'Apply filters'],
},
{
frame: menuFrame,
tasks: ['View menu', 'Add items to cart', 'Customize order'],
},
{
frame: cartFrame,
tasks: ['Review order', 'Apply promo code', 'Proceed to checkout'],
},
{
frame: checkoutFrame,
tasks: ['Enter address', 'Select payment', 'Place order'],
},
],
})
// Track flow progress
const progress = getFlowProgress('order-flow')
// { stepIndex: 1, completedSteps: [0], completedTasks: { 0: [0, 1] } }
// Toggle step completion
toggleStepComplete('order-flow', 0) // Toggle step 0
// Toggle task completion
toggleTaskComplete('order-flow', 1, 2) // Toggle task 2 in step 1
`#### Frame Registry Access
`tsx
import {
getAllApps,
getAppFrames,
getFrame,
searchFrames,
navigateToFrame,
} from 'protomobilekit'// Get all registered apps
const apps = getAllApps()
// [{ appId, appName, frames, initial }, ...]
// Get frames for specific app
const customerFrames = getAppFrames('customer')
// Get specific frame
const frame = getFrame('customer', 'home')
// Search frames
const results = searchFrames('menu')
// [{ app: AppFrames, frame: Frame }, ...]
// Navigate to frame programmatically
navigateToFrame('customer', 'cart')
`#### Frame Hooks
`tsx
import { useFrameRegistry, useAppFrames, useFrame } from 'protomobilekit'function FrameList() {
// All apps with frames
const { apps, frameCount } = useFrameRegistry()
// Frames for specific app
const frames = useAppFrames('customer')
// Specific frame
const frame = useFrame('customer', 'home')
return (...)
}
`---
UI Components
$3
#### Screen
Main screen wrapper with header/footer support.
`tsx
import { Screen, Header, BackButton } from 'protomobilekit' header={ }
footer={ }
scrollable={true} // Enable scrolling (default: true)
padding={true} // Add padding (default: false)
>
// With back button
header={
title="Details"
left={ }
right={ } onPress={openSettings} />}
/>
}
>
`#### Header
`tsx
title="Page Title"
subtitle="Optional subtitle"
left={ }
right={ } onPress={...} />}
transparent={false}
large={false} // iOS large title style
/>
`#### ScrollView
`tsx
horizontal={false}
showsScrollIndicator={true}
refreshControl={ }
>
`#### Section
`tsx
title="Section Title"
subtitle="Optional description"
action={See All }
>
`#### Card
`tsx
variant="elevated" // 'elevated' | 'outlined' | 'filled'
padding="md" // 'none' | 'sm' | 'md' | 'lg'
onPress={() => ...} // Makes card clickable
>
`#### Divider & Spacer
`tsx
// Fixed size in px
// Preset: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
// Flexible spacer (flex: 1)
`---
$3
#### Button
`tsx
variant="primary" // 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'link'
size="md" // 'sm' | 'md' | 'lg'
fullWidth={false}
loading={false}
disabled={false}
icon={ }
iconRight={ }
onClick={() => ...}
>
Button Text
// Text button (iOS style)
color="primary" // 'primary' | 'danger' | 'secondary'
onPress={() => ...}
>
Cancel
// Icon button
icon={ }
variant="danger"
size="md"
onPress={() => ...}
/>
`#### Input
`tsx
label="Email"
placeholder="your@email.com"
type="email" // HTML input types
size="md" // 'sm' | 'md' | 'lg'
variant="outline" // 'outline' | 'filled' | 'underline'
error="Invalid email"
helper="We'll never share your email"
leftAddon={ }
rightAddon={ }
disabled={false}
/>// TextArea
label="Description"
rows={4}
maxLength={500}
showCount
/>
`#### Select
`tsx
label="Country"
placeholder="Select country"
value={country}
onChange={setCountry}
options={[
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'de', label: 'Germany' },
]}
error="Please select a country"
/>// Searchable select
label="City"
options={cities}
value={city}
onChange={setCity}
searchPlaceholder="Search cities..."
emptyMessage="No cities found"
/>
// Autocomplete (with custom input)
label="Product"
options={suggestions}
value={product}
onChange={setProduct}
onSearch={(query) => fetchSuggestions(query)}
renderOption={(option) => (
{option.label} - ${option.price}
)}
/>
`#### Switch & Checkbox
`tsx
label="Notifications"
value={enabled}
onChange={setEnabled}
/> label="I agree to terms"
checked={agreed}
onChange={setAgreed}
/>
// Radio group
label="Payment Method"
value={payment}
onChange={setPayment}
options={[
{ value: 'card', label: 'Credit Card' },
{ value: 'cash', label: 'Cash on Delivery' },
{ value: 'wallet', label: 'Digital Wallet' },
]}
/>
`#### Slider
`tsx
label="Volume"
value={volume}
onChange={setVolume}
min={0}
max={100}
step={1}
showValue
/>
`#### SearchBar
`tsx
value={query}
onChange={setQuery}
placeholder="Search..."
onSubmit={(query) => search(query)}
showCancel
onCancel={() => setQuery('')}
/>
`---
$3
#### Text
Typography component. Renders as
(inline) by default.`tsx
// Basic usage (inline by default)
Inline text // → ...
Secondary color
Primary bold
Error message // Sizes
Extra small
Small
Medium (default)
Large
Extra large
2X large
// Weights
Light
Medium
Semibold
Bold
// Block display (renders as
automatically)
Block-level text // → ...// Change HTML element with 'as' prop
Paragraph // →
...
Heading // → ...
Div wrapper // → ...
Form label // → // Alignment
Centered
Right aligned
// Shorthand components (pre-configured)
Page Title // h2, xl, bold
Section // h3, lg, semibold
Body text // p, block element
Small note // xs, secondary
// label, sm, medium
`#### List & ListItem
`tsx
items={orders}
keyExtractor={(o) => o.id}
renderItem={(order, index) => (
left={ }
right={{order.status} }
subtitle={$${order.total}}
description={order.address}
onPress={() => openOrder(order.id)}
showChevron
>
Order #{order.id}
)}
dividers={true}
dividerInset="left" // 'none' | 'left' | 'both'
header="Recent Orders"
footer="Load more..."
emptyContent={No orders yet }
/>// MenuItem (for settings-like lists)
label="Account Settings"
value="John Doe"
icon={ }
onPress={() => navigate('settings')}
/>
`#### Avatar
`tsx
src="https://example.com/photo.jpg"
name="John Doe" // Fallback initials
size="md" // 'xs' | 'sm' | 'md' | 'lg' | 'xl'
/>// Avatar group
avatars={[
{ src: '...', name: 'Alice' },
{ src: '...', name: 'Bob' },
{ src: '...', name: 'Charlie' },
]}
max={3}
size="sm"
/>
`#### Badge & Chip
`tsx
New
Active
Pending
Error
Info label="React"
onPress={() => selectTag('react')}
selected={selectedTags.includes('react')}
dismissible
onDismiss={() => removeTag('react')}
/>
`#### Status Badges
`tsx
// Generic status badge
status="active"
config={{
active: { label: 'Active', color: '#22c55e' },
inactive: { label: 'Inactive', color: '#ef4444' },
pending: { label: 'Pending', color: '#f59e0b' },
}}
/>// Pre-built status badges
`#### InfoRow & InfoGroup
`tsx
openUrl('...')} /> items={[
{ label: 'Name', value: 'John Doe' },
{ label: 'Email', value: 'john@example.com' },
{ label: 'Role', value: 'Admin' },
]}
/>
`#### StatCard & DashboardStats
`tsx
title="Total Orders"
value={1234}
change={+12.5} // Percentage change
icon={ }
/> stats={[
{ title: 'Orders', value: 1234, change: +5 },
{ title: 'Revenue', value: '$12,345', change: +12 },
{ title: 'Customers', value: 567, change: -2 },
]}
columns={3}
/>
stats={[...]}
layout="grid" // 'grid' | 'row'
/>
`#### Tabs
`tsx
tabs={[
{ id: 'all', label: 'All' },
{ id: 'active', label: 'Active', badge: 5 },
{ id: 'completed', label: 'Completed' },
]}
activeTab={activeTab}
onChange={setActiveTab}
/>// Tab bar (bottom navigation style)
items={[
{ id: 'home', label: 'Home', icon: },
{ id: 'orders', label: 'Orders', icon: , badge: 3 },
{ id: 'profile', label: 'Profile', icon: },
]}
activeItem={activeTab}
onChange={setActiveTab}
/>
`#### Accordion
`tsx
Content 1
Content 2
// Single accordion
title="Show Details"
open={isOpen}
onChange={setIsOpen}
>
`#### Carousel
`tsx
items={[
{ id: '1', image: '...', title: 'Slide 1' },
{ id: '2', image: '...', title: 'Slide 2' },
]}
renderItem={(item) => (

{item.title}
)}
autoPlay
interval={5000}
showDots
showArrows
/>
`---
$3
``tsx
// Dropdown menu
trigger={ } onPress={() => {}} />}
items={[
{ label: 'Edit', icon: , onPress: () => ... },
{ label: 'Share', icon: , onPress: () => ... },
{ type: 'divider' },
{ label: 'Delete', icon: , onPress: () => ..., dest