@enc-protocol/emulator -- SDK API Reference
React phone-frame renderer for the UI Kit. Renders resolved UI trees as interactive previews.
Install
npm config set @enc-protocol:registry https://npm-registry.ocrybit.workers.dev/
npm install @enc-protocol/emulatorPeer dependencies: react >= 19, react-dom >= 19
import { RenderNode } from '@enc-protocol/emulator/tree-renderer'
import * as Chrome from '@enc-protocol/emulator/chrome'
import { PostCard, Tabs, EmptyState } from '@enc-protocol/emulator/common'Table of Contents
- tree-renderer.tsx -- React Tree Renderer
- chrome.tsx -- Host Shell Components
- common.tsx -- Cross-App Primitives
- dm.tsx -- DM-Specific Components
- registry.ts -- Component Registry
- Package Entry (index.ts)
1. tree-renderer.tsx -- React Tree Renderer
Source: sdk-emulator/tree-renderer.tsx
The core renderer that turns UI Kit trees into interactive React DOM. It recursively walks a JSON tree and dispatches each node to the appropriate atom, component, or directory renderer.
RenderNode
The top-level dispatcher component. Determines node type and delegates.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
node | any | required | Tree node (atom, component, or dir) |
path | string | '' | Data-path for action routing |
onAction | (path: string, value?: string) => void | optional | Action callback (only on root node) |
- If
onActionis provided, wraps content in anActionCtx.Providerso all descendants can access the action callback via React context. - Checks node type in order:
isComponent(node): Hascomponentproperty ->RenderComponenthasFiles(node): Has./children ->RenderDirisAtom(node): Hasatomor typedtype->RenderAtom- Otherwise: renders
[unknown]error text.
Every rendered element gets a data-path attribute. When a user interacts
with an element, the onAction callback is called with the element's
data-path (and optionally an input value). The host app translates these
paths into state transitions via applyAction.
import { RenderNode } from '@enc-protocol/emulator'
function MyApp({ tree, onAction }) {
return <RenderNode node={tree} path="root" onAction={onAction} />
}Internal: RenderAtom
Renders leaf nodes (atoms). Not exported directly.
Supported Atom Types:str (text)
Renders as text. If style has primary: true, renders as a Button.
Supports grow, size, color, weight, mono, pad style properties.
| Data Prop | Description |
|---|---|
value | Text content to display |
label | Alternative text (buttons) |
media (avatar)
Renders as a gradient circle with first-letter monogram. Clickable.
| Data Prop | Description |
|---|---|
src | Name/key for monogram |
name | Fallback for src |
size | Circle size (px) |
input (text input)
Renders as a text input field. Supports text, file, and textarea
types.
| Data Prop | Description |
|---|---|
value | Current value |
placeholder | Placeholder text |
type | 'text' (default), 'file', 'textarea' |
text: Standard pill-shaped input. FiresonAction(path, value)on change,onAction(path + '$submit')on Enter.file: File picker display. Shows hash or placeholder.textarea: Multiline display (read-only in current impl).
select (single choice)
Renders as a horizontal row of selectable chips.
| Data Prop | Description |
|---|---|
selected | Currently selected item |
value | Alternative for selected |
options | Array of option strings (or comma-separated string) |
Fires onAction(path + '$select#x27; + item_key) on chip click.
multiselect (multiple choice)
Same as select but multiple items can be highlighted.
| Data Prop | Description |
|---|---|
selected | Array of selected items |
options | Array of option strings |
bool (toggle)
Renders as a sliding toggle switch (44x24px).
| Data Prop | Description |
|---|---|
value | true/'true' for on |
Fires onAction(path, String(!current)) on click.
slider (range)
Renders as a horizontal bar with min/max labels and current value.
| Data Prop | Description |
|---|---|
value | Current value |
min | Minimum (0) |
max | Maximum (100) |
arr (string array)
Renders as a row of teal chips, one per array element.
| Data Prop | Description |
|---|---|
value | Array or comma-separated string |
null (spacer/divider)
Renders as empty space or a 1px separator line.
| Style Prop | Description |
|---|---|
height | Fixed height (px) |
line | If true, render as line |
grow | If true, flex-grow |
Internal: RenderComponent
Renders component nodes. Dispatches by node.component name.
header
Renders via DMHeader. Shows title, optional subtitle, optional back button.
| Data Prop | Description |
|---|---|
title | Header title text |
subtitle | Optional subtitle |
back | Show back button |
tabs
Renders as a horizontal tab bar with accent-colored active indicator. Supports badge counts (numbers in parentheses after label).
| Data Prop | Description |
|---|---|
items | Array of tab label strings |
active | Currently active tab label |
Fires onAction(path + '$select#x27; + tab_key) on tab click.
Tab key = lowercase, spaces to underscores, badge stripped.
bar / compose_bar
Horizontal flex row: growing input + primary button.
| Data Prop | Description |
|---|---|
value | Current input value |
placeholder | Input placeholder |
button | Button label (default "Send") |
Fires:
onAction(path + '$input', value)on input changeonAction(path + '$submit')on button click or Enter
card / list_item
Renders differently based on data shape:
- With
body: PostCard (avatar + title + body text) - Without body: ListItem (title + subtitle + avatar + trailing)
| Data Prop | Description |
|---|---|
title | Primary text |
subtitle | Secondary text |
body | Body content |
media | Avatar name/key |
avatar | Alternative to media |
trailing | Trailing text (time) |
outgoing | Boolean outgoing flag |
post_card
Always renders as a PostCard (regardless of data shape).
field / form_field
Label text above a rectangle-variant input.
| Data Prop | Description |
|---|---|
label | Field label text |
placeholder | Input placeholder |
message_bubble
Single chat bubble.
| Data Prop | Description |
|---|---|
body | Message text |
outgoing | Alignment flag |
empty_state
Centered message text in muted color.
| Data Prop | Description |
|---|---|
message | Display text |
connect_gate
Full-page connect wallet CTA: avatar, title, subtitle, primary button.
| Data Prop | Description |
|---|---|
app_name | App name in text |
Fires onAction(path + '$connect') on button click.
error_view
Error state: danger-colored title, message, retry button.
| Data Prop | Description |
|---|---|
message | Error message |
retry_label | Retry button text |
card_list
Dynamic list of card items. The most complex component. Each item is rendered based on its shape:
-
Full-width button (accept only, no title/subtitle): Primary button. Fires
path + '$accept#x27; + idx. -
Post card (has
body): Avatar + title + body + optional engagement bar (likes/replies). Fires:path + '$avatar#x27; + idxon avatar clickpath + '#x27; + idxon body clickpath + '$like#x27; + idxon like click
-
KV detail row (no media/trailing/accept, has subtitle): Label above, monospace value below.
-
App catalog row (accept + media + subtitle, no reject): ListItem with action button. Fires
path + '$accept#x27; + idx. -
Stat row with link (accept, no reject): Title left, accent link right. Fires
path + '$accept#x27; + idx. -
Accept/reject row (both accept and reject): ListItem with two buttons. Fires
path + '$accept#x27; + idxorpath + '$reject#x27; + idx. -
Standard list item (fallback): Clickable ListItem. Fires
path + '#x27; + idx.
| Data Prop | Description |
|---|---|
items | Array of item objects |
bubble_list
Dynamic list of chat bubbles. Messages are reversed (newest at bottom). Author names are colored (cycling through 6 colors for group chats).
| Data Prop | Description |
|---|---|
messages | Array of message objects |
Message object fields: body, outgoing, author, tx_amount, txid.
field_list
Dynamic list of form fields. Each field maintains local React state
(controlled input). Uses FieldInput internal component.
| Data Prop | Description |
|---|---|
fields | Array of field objects |
Field object: { label, placeholder, value, key }.
If key is set, fires onAction(path + '$key#x27; + key, value) on change.
Default (unknown component)
If a component name matches a COMPONENTS catalog entry with a tree definition, it is rendered recursively:
- Bind data via
bindTree. - Apply styles via
applyStyles. - Pass parent data to child components (card_list gets items, bubble_list gets messages, bar gets placeholder/button/value, tabs gets items/active, header gets title/subtitle/back).
If no match: renders [component:name] error text.
Internal: RenderDir
Renders directory (container) nodes. Pure flexbox layout.
| Style Prop | CSS Mapping |
|---|---|
axis | 'h' -> row, 'v' -> column |
gap | gap (px) |
pad | padding (px, or [t,r,b,l]) |
align | alignItems (start/center/end/stretch) |
justify | justifyContent (start/center/end/between) |
scroll | overflowY: 'auto' |
grow | flex: 1; minHeight: 0 |
Children are rendered via RenderNode with $-separated path segments.
Exported: applyStyles(node, styles, path?) -> any
Duplicated from tree-utils.ts (same implementation). Merges style map
into tree nodes. See tree-utils.ts documentation for details.
Exported: validateTree(node, path?) -> string[]
Duplicated from tree-utils.ts. See tree-utils.ts documentation.
2. chrome.tsx -- Host Shell Components
Source: sdk-emulator/chrome.tsx
Chrome components render the host shell around apps -- top bar, wallet connection, phone frame. These are distinct from app-scoped components.
PhoneFrame
iPhone-shaped chrome container (390x844 max). Includes system status bar (time, signal, battery), content area, and bottom system bar with optional home button.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | required | Content to render inside |
onHome | () => void | undefined | Home button click handler |
- Top system bar:
9:41time, dynamic island notch, signal/wifi/battery icons. - Content area:
{children}in a flex column. - Bottom system bar: Home button (if
onHomeprovided) or swipe indicator.
data-testid: phone-frame
<PhoneFrame onHome={() => navigateHome()}>
<MyAppContent />
</PhoneFrame>TopBarContainer
Fixed top bar layout (50px height, flex row).
Props:| Prop | Type | Description |
|---|---|---|
children | React.ReactNode | Bar contents |
Renders: Fixed-position bar at top with border-bottom, centered items.
Branding
ENC logo monogram (gradient circle with "E") + "ENC" label.
Props: None.
Spacer
Flex-1 filler for flex rows.
Props: None.
ConnectButton
Connect/Disconnect pill button.
Props:| Prop | Type | Description |
|---|---|---|
connected | boolean | Connection state |
onClick | () => void | Click handler |
- Connected: Red-tinted "Disconnect" pill.
- Disconnected: Green gradient "Connect" pill.
PubkeyChip
Small monogram circle + truncated pubkey (first 8 chars). Click to copy.
Props:| Prop | Type | Description |
|---|---|---|
pubKey | string | Full public key hex |
onCopy | (text: string) => void | Called after copy with full key |
Renders: Rounded pill with gradient circle + monospace truncated key.
Copies full pubkey to clipboard on click (both navigator.clipboard and
fallback execCommand).
CopyToast
Green confirmation pill, fixed at top center. Auto-dismissing (parent controls visibility).
Props:| Prop | Type | Description |
|---|---|---|
message | string | Toast text |
Renders: Fixed-position green pill with shadow at top: 60px.
WalletChooserModal
Full wallet connection modal. Supports extension, passkey, and dev account options.
Props:| Prop | Type | Description |
|---|---|---|
onClose | () => void | Backdrop click handler |
extensionAvailable | boolean | Show extension option |
onExtensionConnect | () => void | Extension connect handler |
accounts | WalletAccount[] | Dev accounts to show |
onAccountClick | (a: WalletAccount) => void | Account selection handler |
passkeys | WalletPasskey[] | Existing passkeys to show |
onPasskeyClick | (p: WalletPasskey) => void | Passkey selection handler |
passkeySupported | boolean | Show passkey options |
onUseExistingPasskey | () => void (optional) | Use existing passkey handler |
onCreatePasskey | () => void | Create new passkey handler |
onCreateDevAccount | () => void | Create dev account handler |
interface WalletAccount {
pubHex: string
name: string
}
interface WalletPasskey {
credentialId: string
pubHex: string
name: string
}Renders: Centered modal dialog (320px wide, max 80vh scroll) with:
- Header: "Connect Wallet" + description
- Extension button (if available, accent-bordered)
- Account buttons (gradient-circle monogram + name + truncated key)
- Passkey buttons (purple-themed)
- "Use Existing Passkey" button (if
onUseExistingPasskeyprovided) - "Create Passkey (secure)" button (purple gradient)
- "+ Dev Account (insecure)" button (green gradient)
Backdrop click dismisses. Modal click stops propagation.
QRCodeButton
Top-bar QR icon that opens a modal with a scannable QR code.
Props:| Prop | Type | Description |
|---|---|---|
url | string | URL to encode in QR |
qrSrc | string | QR code image source URL |
Renders: Small icon button (22x22). On click, opens a modal showing:
- "Scan to open on phone" header
- QR code image (260x260 on white background)
- URL text (monospace, word-break)
- "Copy URL" button
ConnectPrompt
Full-page "Connect Wallet" CTA shown when app is opened without wallet.
Props:| Prop | Type | Description |
|---|---|---|
appName | string | App name to display |
onBack | () => void | Back button handler |
onConnect | () => void | Connect button handler |
- Header bar with back arrow + app name
- Centered content: wallet icon, "Connect Wallet" heading, description text, green gradient "Connect Wallet" button
DetectingExtensionScreen
Full-screen loading state while detecting ENC extension.
Props: None.
Renders: Centered spinner + "Detecting ENC Extension..." text.
ExtensionInstallScreen
Full-page extension install guide with dev mode fallback.
Props:| Prop | Type | Description |
|---|---|---|
installStatus | string | Status message after download |
onInstall | () => void | Download button handler |
onCopyChromeExtensionsUrl | () => void | Copy chrome://extensions handler |
onDevMode | () => void | Dev mode button handler |
onRetry | () => void | Retry detection handler |
- Shield icon
- "ENC Extension Required" heading
- Install instructions
- "Download Extension" button
- Install status + "Copy chrome://extensions" (if installStatus set)
- Developer mode instructions
- "Dev Mode (local keys)" button (purple)
- "Retry Detection" button (muted)
ExtensionUpdateButton
Yellow pill in top bar indicating extension needs update.
Props:| Prop | Type | Description |
|---|---|---|
currentVersion | `string | null` |
expectedVersion | string | Required version |
onClick | () => void | Click handler |
Renders: Warning-colored pill: "Update Extension (old -> expected)".
HomeGrid
4-column app launcher tile grid.
Props:| Prop | Type | Description |
|---|---|---|
items | HomeGridItem[] | Apps to display |
onOpen | (name: string) => void | App open handler |
interface HomeGridItem {
name: string
iconUrl: string
}- "ENC Emulator" heading
- "N apps loaded" subtitle
- 4-column CSS grid of icon tiles (48x48 images with labels)
3. common.tsx -- Cross-App Primitives
Source: sdk-emulator/common.tsx
Stage-2 axioms used by 2+ unrelated apps. This file must NOT reference any app by name.
PageScaffold
Outer flex container for any app page. Owns text color, font, background.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
background | 'default' 'chat' 'transparent' | 'default' | Background mode |
children | React.ReactNode | required | Page content |
'default': Inherits from parent (undefined background)'chat':TG.chatBg(#0e0e1a)'transparent': Explicit transparent
ScrollBody
Flex-1 body container with optional scroll.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
scroll | boolean | undefined | Enable overflow scroll |
children | React.ReactNode | required | Body content |
- No scroll:
flex: 1; minHeight: 0; display: flex; flexDirection: column - Scroll:
flex: 1; overflowY: auto
FixedItem
Layout wrapper that prevents flex shrinking. Used to pin tabs/bars.
Props:| Prop | Type | Description |
|---|---|---|
children | React.ReactNode | Wrapped content |
Renders: div with flexShrink: 0.
Stack
Vertical flex layout container.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
gap | number | 0 | Gap between children |
padding | number or [number, number] | undefined | Padding (px) |
align | 'start' 'center' 'end' 'stretch' | undefined | Cross-axis alignment |
onClick | () => void | undefined | Click handler |
children | React.ReactNode | required | Content |
When onClick is set, adds cursor pointer and bottom border.
Row
Horizontal flex layout container.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
gap | number | 0 | Gap between children |
padding | number or [number, number] | undefined | Padding (px) |
align | 'start' 'center' 'end' 'stretch' | 'center' | Cross-axis alignment |
wrap | boolean | undefined | Enable flex wrap |
children | React.ReactNode | required | Content |
Text
Typography axiom. Renders a styled text div.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
value | string | required | Text content |
size | 'sm' 'md' 'lg' 'xl' | 'md' | Font size |
color | 'primary' 'secondary' 'accent' 'danger' 'warning' | 'primary' | Color token |
weight | 'regular' 'medium' 'bold' | 'regular' | Font weight |
align | 'left' 'center' 'right' | 'left' | Text alignment |
padding | number or [number, number] | undefined | Padding |
mono | boolean | undefined | Monospace font |
onClick | () => void | undefined | Click handler |
Size Mapping: sm=12, md=14, lg=16, xl=18.
Weight Mapping: regular=400, medium=600, bold=700.
Returns null if value is null or empty string.
Mono mode: Uses 'SF Mono', monospace with word-break: break-all.
Input
Text input axiom.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
value | string | required | Current value |
placeholder | string | optional | Placeholder text |
onChange | (v: string) => void | optional | Change handler |
onSubmit | () => void | optional | Enter key handler |
disabled | boolean | optional | Disable input |
variant | 'pill' or 'rectangle' | 'pill' | Border radius style |
Radius: pill=20, rectangle=12.
Enter key triggers onSubmit.
Style: width: 100%; flex: 1 1 auto; minWidth: 0.
Button
Push-button axiom.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
label | string | required | Button text |
onClick | () => void | optional | Click handler |
primary | boolean | undefined | Primary style |
disabled | boolean | undefined | Disable button |
fullWidth | boolean | undefined | width: 100% |
Primary style: accent background, black text. Default style: semi-transparent white background, primary text. Disabled: 50% opacity, not-allowed cursor.
Banner
Clickable callout box with colored border.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
title | string | required | Banner title |
subtitle | string | optional | Banner description |
kind | 'info' 'warning' 'danger' 'success' | 'info' | Color scheme |
onClick | () => void | optional | Click handler |
info: accent (teal)warning: warning (orange)danger: danger (red)success: accent (teal, stronger)
If onClick is set, renders as a button element.
Divider
Visual separator (1px line).
Props:| Prop | Type | Default | Description |
|---|---|---|---|
spacing | number | 0 | Vertical margin (px) |
Avatar
Gradient circle with first-letter monogram.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
name | string | required | Name for monogram |
size | number | 50 | Circle diameter (px) |
Monogram: First character (using Array.from for proper Unicode/emoji
handling). ASCII letters are uppercased; emoji/non-ASCII rendered as-is.
Font size: Math.round(size * 0.36).
Tabs
Bottom navigation tab bar.
Props:| Prop | Type | Description |
|---|---|---|
tabs | Array<{ key: string; label: string }> | Tab definitions |
active | string | Active tab key |
onChange | (key: string) => void | Tab change handler |
Renders: Horizontal flex row with role="tablist". Each tab is a
button with role="tab" and aria-selected. Active tab has accent-colored
top border.
ListItem
Generic list row with leading/content/trailing slots.
Props:| Prop | Type | Description |
|---|---|---|
title | string | Primary text |
subtitle | string | Secondary text (optional) |
leading | React.ReactNode | Left slot (avatar/icon) |
trailing | React.ReactNode | Right slot (time/badge) |
mono | boolean | Monospace title font |
onClick | () => void | Click handler |
PostCard
X/Twitter-style timeline post.
Props:| Prop | Type | Description |
|---|---|---|
author | string | Display name or handle |
authorPub | string | Optional pub for fallback |
body | string | Post content |
timestamp | number | Unix timestamp (for relative time) |
outgoing | boolean | Accent color for own posts |
onClick | () => void | Click handler |
Renders: 40px avatar circle + author name + relative time + body text.
If body starts with {, attempts JSON parse and joins string values.
MessageBubble
Chat message bubble (Telegram-style).
Props:| Prop | Type | Description |
|---|---|---|
outgoing | boolean | Alignment + color |
body | string | Message text |
timestamp | number | Optional timestamp (HH) |
tx_hash | string | Optional transaction hash |
amount | `string | number` |
token | string | Optional token name (default ETH) |
Outgoing: Right-aligned, green background, checkmark. Incoming: Left-aligned, dark purple background. Transaction: Gradient background, accent border, amount header, truncated tx hash footer.
ComposeBar
Input + submit button pinned to bottom.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
draft | string | required | Current draft text |
onChange | (v: string) => void | required | Text change handler |
onSubmit | () => void | required | Submit handler |
placeholder | string | 'Message' | Input placeholder |
buttonLabel | string | undefined | Button text (null = send icon) |
disabled | boolean | undefined | Disable input + button |
pendingMessage | string | undefined | Show warning instead of input |
Submit guard: Only submits if draft.trim() is non-empty.
Pending mode: Shows warning icon + message, hides input.
LoadingSpinner
Animated spinning circle.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
size | number | 24 | Spinner diameter |
message | string | undefined | Text below spinner |
EmptyState
Centered muted text for empty lists.
Props:| Prop | Type | Description |
|---|---|---|
message | string | Display text |
ErrorState
Error display with optional retry button.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
message | string | required | Error message |
retryLabel | string | 'Retry' | Retry button text |
onRetry | () => void | optional | Retry handler |
AppErrorBoundary
React error boundary that catches render crashes.
Props:| Prop | Type | Description |
|---|---|---|
appName | string | App name for error msg |
children | React.ReactNode | Content to protect |
On error: Renders ErrorState with "<appName> crashed: <message>".
Retry button resets the boundary (re-mounts children).
Header
Generic page header with optional back button and trailing slot.
Props:| Prop | Type | Description |
|---|---|---|
title | string | Header title |
subtitle | string | Optional subtitle |
showBack | boolean | Show back arrow |
onBack | () => void | Back handler |
trailing | React.ReactNode | Right-side slot |
FormField
Label + input combination.
Props:| Prop | Type | Description |
|---|---|---|
label | string | Field label |
placeholder | string | Input placeholder |
value | string | Current value |
onChange | (v: string) => void | Change handler |
mono | boolean | Monospace input font |
IconButton
Compact circular action button.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
icon | React.ReactNode | required | Icon content |
onClick | () => void | optional | Click handler |
size | number | 36 | Button diameter |
variant | 'default' 'accent' 'danger' | 'default' | Color scheme |
StatusView
Unified loading/error/empty state.
Props:| Prop | Type | Description |
|---|---|---|
type | 'loading' 'error' 'empty' | State type |
message | string | Display message |
action | string | Optional button text |
onAction | () => void | Button handler |
Timestamp
Relative time display.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
value | number | required | Unix timestamp |
size | 'sm' or 'md' | 'sm' | Font size |
Output: Ns, Nm, Nh, Nd relative format.
ChatListContainer
Flex-grow scrollable container for chat message lists.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
reverse | boolean | undefined | Column-reverse mode |
children | React.ReactNode | required | Chat bubbles |
Reverse mode: flex-direction: column-reverse; padding: 8px 10px.
4. dm.tsx -- DM-Specific Components
Source: sdk-emulator/dm.tsx
Stage-1 axioms specific to the DM app, plus re-exports of promoted Stage-2 components.
DMHeader
DM-specific page header with optional avatar.
Props:| Prop | Type | Description |
|---|---|---|
title | string | Header title |
subtitle | string | Optional subtitle |
showBack | boolean | Show back arrow |
onBack | () => void | Back handler |
showAvatar | boolean | Show contact avatar |
avatarLetter | string | Letter for avatar monogram |
trailing | React.ReactNode | Right-side slot |
Same visual layout as Header but with optional inline avatar circle
(32x32, gradient background).
EnvelopeBadge
Envelope icon with notification count. Used in header trailing slot.
Props:| Prop | Type | Description |
|---|---|---|
count | number | Number of message requests |
onClick | () => void | Click handler |
Active state (count > 0): Red background, red icon stroke, count text. Inactive: Transparent background, secondary icon stroke.
aria-label: "N message request(s)" or "No message requests".
ContactRow
DM contact list item with state indicators.
Props:| Prop | Type | Description |
|---|---|---|
name | string | Display name |
pub | string | Public key |
state | 'FRIEND' 'PENDING' 'BLOCKED' | Contact state |
preview | string | Last message preview |
outgoing | boolean | Last msg was outgoing |
timestamp | number | Last message time |
onClick | () => void | Click handler |
PENDING state: Shows warning-colored "Awaiting acceptance" text. Outgoing: Prefixes preview with checkmark. data-pub: Set to pub for testing.
MessageBubble (re-export)
Re-exported from common.tsx for backward compatibility. Same component
and props as common.MessageBubble.
ChatInputBar
DM-specific chat input bar.
Props:| Prop | Type | Description |
|---|---|---|
draft | string | Current draft |
onChange | (v: string) => void | Text change handler |
onSend | () => void | Send handler |
disabled | boolean | Disable input |
pendingMessage | string | Warning text instead of input |
Similar to ComposeBar but with circular send icon button (no label
variant). Only sends if draft.trim() is non-empty.
EmptyChat
Centered empty chat state with contact avatar.
Props:| Prop | Type | Description |
|---|---|---|
name | string | Contact name |
Renders: 60px gradient avatar, name, "End-to-end encrypted" label.
LookupResultCard
Found-user card with inline invite form.
Props:| Prop | Type | Description |
|---|---|---|
name | string | Found user's name |
enclaveId | string | Enclave ID (truncated) |
draft | string | Greeting message draft |
onChange | (v: string) => void | Draft change handler |
onSend | () => void | Invite send handler |
Renders: Accent-bordered card with avatar + name + truncated ID + "Found" badge. Below: input ("Say hello...") + "Invite" button.
PublishBanner
Warning banner prompting user to publish profile to registry.
Props:| Prop | Type | Description |
|---|---|---|
onPublish | () => void | Publish handler |
publishing | boolean | Loading state |
Renders: Warning-themed banner with "Profile not in registry" title, explanation, and "Publish" button (shows "..." when publishing).
DMSetupPage
Profile setup form for first-time DM users.
Props:| Prop | Type | Description |
|---|---|---|
initialName | string | Pre-filled name |
onContinue | (name: string) => void | Continue with name |
onChange | (name: string) => void | Name change handler |
Renders: Large avatar (80px), "Your Profile" heading, description,
centered text input, "Continue" button. Uses local React state for the
name field. Only calls onContinue if name is trimmed non-empty.
InviteRow
Message request row with accept/dismiss buttons.
Props:| Prop | Type | Description |
|---|---|---|
fromPub | string | Sender's public key |
message | string | Invitation message |
onAccept | () => void | Accept handler |
onDismiss | () => void | Dismiss handler |
Renders: Truncated monospace pubkey, message text, Accept (accent) and Dismiss (muted) buttons.
DMPageScaffold
DM-specific page wrapper.
Props:| Prop | Type | Default | Description |
|---|---|---|---|
chatBg | boolean | undefined | Use chat background |
children | React.ReactNode | required | Page content |
DMPageBody
DM page body container (flex-1, column, min-height 0).
Props:| Prop | Type | Description |
|---|---|---|
children | React.ReactNode | Body content |
5. registry.ts -- Component Registry
Source: sdk-emulator/registry.ts
The protocol surface for manifest-driven rendering. Every visible primitive that an app's manifest can reference MUST be registered here.
REGISTRY_VERSION
const REGISTRY_VERSION = '0.1.0'Bumped on incompatible changes (prop removal, axiom rename). Additive changes (new axiom, new optional prop) keep the same version.
ComponentSpec Interface
interface ComponentSpec {
/** React component implementing the axiom */
Component: React.ComponentType<any>
/** Manifest-prop -> component-prop mapping.
* 'expr': evaluated from state/bindings
* 'action': wired as callback function
* 'literal': passed as-is from manifest */
props: Record<string, 'expr' | 'action' | 'literal'>
/** Optional auto-wired props from runtime */
wireAuto?: Array<'onBack' | 'lookupName' | 'lookupEnclave' | 'runtime'>
/** If set, children are rendered and passed under this prop name */
childrenSlot?: string
}REGISTRY Object
Complete registry of all components. Organized by category:
Common (Stage-2) Axioms
| Key | Component | Props (type) |
|---|---|---|
PageScaffold | PageScaffold | background(literal) |
ScrollBody | ScrollBody | scroll(literal) |
Avatar | Avatar | name(expr), size(literal) |
EmptyState | EmptyState | message(expr) |
ComposeBar | ComposeBar | draft(expr), onChange(action), onSubmit(action), placeholder(literal), buttonLabel(literal), disabled(expr), pendingMessage(expr) |
Tabs | Tabs | tabs(literal), active(expr), onChange(action) |
PostCard | PostCard | author(expr), authorPub(expr), body(expr), timestamp(expr), outgoing(expr), onClick(action) |
Stack | Stack | gap(literal), padding(literal), align(literal). childrenSlot='children' |
Row | Row | gap(literal), padding(literal), align(literal), wrap(literal). childrenSlot='children' |
Text | Text | value(expr), size(literal), color(literal), weight(literal), align(literal), padding(literal), mono(literal) |
Input | Input | value(expr), placeholder(literal), onChange(action), onSubmit(action), disabled(expr), variant(literal) |
Button | Button | label(expr), onClick(action), primary(literal), disabled(expr), fullWidth(literal) |
Banner | Banner | title(expr), subtitle(expr), kind(literal), onClick(action) |
LoadingSpinner | LoadingSpinner | size(literal), message(expr) |
ErrorState | ErrorState | message(expr), retryLabel(literal), onRetry(action) |
Lowercase Aliases
For backward compatibility with existing manifests: stack, row, text,
label, button, banner, divider, timestamp.
Generic Patterns
| Key | Component | Props (type) |
|---|---|---|
Header | Header | title(expr), subtitle(expr), showBack(literal), onBack(action), trailing(literal) |
ListItem | ListItem | title(expr), subtitle(expr), mono(literal), onClick(action) |
FormField | FormField | label(literal), placeholder(literal), value(expr), onChange(action), mono(literal) |
IconButton | IconButton | onClick(action), size(literal), variant(literal) |
StatusView | StatusView | type(literal), message(expr), action(literal), onAction(action) |
Divider | Divider | spacing(literal) |
Timestamp | Timestamp | value(expr), size(literal) |
DM (Stage-1) Axioms
| Key | Component | Props (type) |
|---|---|---|
DMHeader | DMHeader | title(expr), subtitle(expr), showBack(literal), onBack(action), showAvatar(literal), avatarLetter(expr), trailing(literal) |
EnvelopeBadge | EnvelopeBadge | count(expr), onClick(action) |
ContactRow | ContactRow | name(expr), pub(expr), state(expr), preview(expr), outgoing(expr), timestamp(expr), onClick(action) |
MessageBubble | MessageBubble | outgoing(expr), body(expr), timestamp(expr), tx_hash(expr), amount(expr), token(expr) |
ChatInputBar | ChatInputBar | draft(expr), onChange(action), onSend(action), disabled(expr), pendingMessage(expr) |
EmptyChat | EmptyChat | name(expr) |
LookupResultCard | LookupResultCard | name(expr), enclaveId(expr), draft(expr), onChange(action), onSend(action) |
PublishBanner | PublishBanner | onPublish(action), publishing(expr) |
DMSetupPage | DMSetupPage | initialName(expr), onContinue(action), onChange(action) |
InviteRow | InviteRow | fromPub(expr), message(expr), onAccept(action), onDismiss(action) |
Re-exports from registry.ts
export * as DM from './dm'
export { TG } from './theme'6. Package Entry (index.ts)
Source: sdk-emulator/index.ts
export * from './common' // All common components
export * from './dm' // All DM components
export * as Chrome from './chrome' // Chrome components under namespace
export { TG, type PaletteKey } from '../sdk-ui-kit/theme'
export { REGISTRY, REGISTRY_VERSION, type ComponentSpec } from './registry'Chrome components are namespaced (Chrome.PhoneFrame, Chrome.ConnectButton, etc.)
to avoid naming conflicts with app-level components.