Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

@enc-protocol/ui-kit -- SDK API Reference

Pure UI Kit SDK. Algebra engine, page resolution, actions, theme. No React, no DOM.

Install

npm config set @enc-protocol:registry https://npm-registry.ocrybit.workers.dev/
npm install @enc-protocol/ui-kit
import { resolvePageTree, applyAction, createTestRunner } from '@enc-protocol/ui-kit'
import { TG } from '@enc-protocol/ui-kit/theme'

Table of Contents

  1. app-adaptor.ts -- Platform-Agnostic Adaptor
  2. algebra.js -- State Machine Engine
  3. tree-utils.ts -- Tree Utilities
  4. resolve-bindings.ts -- Page Resolution
  5. theme.ts -- Color Tokens
  6. sql-algebra.js -- SQL Query Engine
  7. synth.js -- App Synthesizer
  8. Re-exported Constants

1. app-adaptor.ts -- Platform-Agnostic Adaptor

The central module that connects algebra state to UI rendering. Every platform (React DOM, React Native, terminal, Chrome extension) imports from here.

Source: sdk-ui-kit/app-adaptor.ts


resolvePageTree(hostApp, state, allApps?) -> ResolvedPage

Given an app and current state, produce the renderable tree for the current page.

Parameters:
ParamTypeDefaultDescription
hostAppany (App definition)requiredThe host app definition object
stateRecord<string, any>requiredCurrent state object
allAppsany[]ENC_APPSAll available app definitions

Returns: ResolvedPage

interface ResolvedPage {
  tree: any          // The renderable tree (JSON structure)
  page: any          // The matched page definition
  app: any           // The app being rendered (may differ from hostApp if mini-app is open)
  path: string       // The resolved path
}
Behavior:
  1. Checks state.last_open_action?.app_id for mini-app context switching. If set, switches to rendering the opened app from allApps.
  2. Also checks state._renderAppId for test runner context.
  3. Applies gates: iterates through the render app's gates. First matching gate overrides the current path.
  4. Calls findPage() to locate the page definition for the current path.
  5. Calls buildPageTree() to construct the renderable tree with styles and bindings applied.
  6. Returns the tree, page, rendering app, and resolved path.
Example:
import { resolvePageTree } from '@enc-protocol/ui-kit'
 
const { tree, page, app, path } = resolvePageTree(helloApp, currentState)
// tree is ready for platform-specific rendering
// page contains data bindings, actions, etc.
// app is the actual app being rendered (may be a mini-app)
Edge Cases:
  • If no page matches the current path, falls back to pages[default_page] (usually index 0).
  • If state.last_open_action references an unknown app ID, falls back to the host app.
  • Returns { tree: null, ... } if no page definition has a matching component in COMPONENTS.

applyAction(app, state, clickPath, walletPub, allApps?, inputValue?) -> Record<string, any>

Translate a user interaction (click/tap) into a state transition.

Parameters:
ParamTypeDefaultDescription
appanyrequiredApp definition
stateRecord<string, any>requiredCurrent state
clickPathstringrequiredThe data-path of the clicked element
walletPubstringrequiredCurrent wallet public key
allAppsany[]ENC_APPSAll available apps
inputValuestringundefinedValue from input onChange

Returns: New state object (or current state if no action matched).

Click Path Tokenization:

The clickPath is $-delimited. The function parses it to determine the action key:

Click Path PatternResolved Action KeyDescription
...$keylt;stateKey>(auto-save)Form field keystroke
...$listlt;idx>#list.selectList item click
...lt;nodeId>$avatarlt;idx>#<nodeId>.avatarAvatar click in list
...lt;nodeId>$acceptlt;idx>#<nodeId>.acceptAccept button in list
...lt;nodeId>$rejectlt;idx>#<nodeId>.rejectReject button in list
...lt;nodeId>$likelt;idx>#<nodeId>.likeLike button in list
...lt;tabId>$selectlt;val>#<tabId>.selectTab selection
...lt;nodeId>$submit#<nodeId>.submitSubmit action
...lt;nodeId>$input#<nodeId>.inputInput change
...lt;name>#<name>Simple click
Behavior:
  1. Resolves the current page via resolvePageTree.
  2. Tokenizes the click path.
  3. Looks up the action expression in page.actions.
  4. If the action expression is a mock name, executes via the mock system.
  5. Otherwise, executes via execAction from algebra.js.
  6. Calls synthIfNeeded to handle mini-app synthesis if applicable.
  7. Returns the new state.
Example:
import { applyAction } from '@enc-protocol/ui-kit'
 
// User clicked the Post button
const nextState = applyAction(app, state, 'content$bar$submit', walletPub)
 
// User typed in an input
const nextState2 = applyAction(app, state, 'content$bar$input', walletPub, ENC_APPS, 'hello')
 
// User selected a tab
const nextState3 = applyAction(app, state, 'content$tabs$select$my_posts', walletPub)
Edge Cases:
  • Form field auto-save: If click path contains $keylt;stateKey>, the input value is directly written to state[stateKey] without looking up an action expression.
  • Auto-syncs profiles table when name fields (profile_name, display_name, my_name) change.
  • If the action expression is a function (legacy), calls it directly.
  • Falls back to simpler action key if compound key has no match (e.g., #content.submit falls back to #submit).

createTestRunner(app, walletPub, opts?) -> runTest

Create a test runner for an app that exercises workflow tests in a rendered environment.

Parameters:
ParamTypeDefaultDescription
appanyrequiredApp definition with tests
walletPubstringrequiredWallet public key for testing
opts.allAppsany[]ENC_APPSAll available apps
opts.delaynumber400Delay between steps (ms)

Returns: An async function:

async function runTest(
  testIdx: number,
  setState: (state: Record<string, any>) => void,
  probeDOM: () => { text: string; inputValues: string },
  delay?: number,
): Promise<StepResult[]>
StepResult Interface:
interface StepResult {
  status: 'pass' | 'fail'
  label: string              // e.g., "do: #bar.submit" or "path, title"
  error: string | null       // e.g., '[#title] "Hello" not visible'
}
Behavior:
  1. Runs the algebra workflow (runWorkflow) to get expected states.
  2. Resets the app to initial state.
  3. For each step: a. Syncs the algebra state to the renderer via setState. b. Waits delay ms for rendering. c. Calls probeDOM() to get visible text. d. Checks each see assertion against the visible text. e. Skips $ assertions (state-only, not visible). f. Skips / values (path assertions, not visible text).
Example:
const runner = createTestRunner(helloApp, ALICE_WALLET)
const results = await runner(
  0,  // test index
  (state) => setReactState(state),
  () => ({
    text: document.querySelector('[data-testid="phone"]')?.textContent || '',
    inputValues: '',
  }),
  300,
)
// results: [{ status: 'pass', label: 'do: #button', error: null }, ...]

matchPath(pattern, path) -> boolean

Match a route pattern against a concrete path.

Parameters:
ParamTypeDescription
patternstringRoute pattern (e.g., /nodes/:id)
pathstringConcrete path (e.g., /nodes/abc)

Returns: true if the pattern matches the path.

Behavior: Splits both on /. Each segment must match exactly, except segments starting with : which match any value. Both must have the same number of segments.

Example:
matchPath('/nodes/:id', '/nodes/abc123')  // true
matchPath('/nodes/:id', '/nodes/abc/sub') // false (different length)
matchPath('/feed', '/feed')              // true
matchPath('/feed', '/my_posts')          // false

findPage(pages, path, defaultIdx?) -> any

Find the page definition for a given path.

Parameters:
ParamTypeDefaultDescription
pagesany[]requiredArray of page definitions
pathstringrequiredPath to match
defaultIdxnumber0Fallback page index

Returns: The matched page definition, or pages[defaultIdx].

Behavior:
  1. Exact match: page.path === path
  2. Try with / prefix: if path doesn't start with /, try / + path
  3. Parameter match: matchPath(page.path, path)
  4. Parameter match with / prefix
  5. Fallback: pages[defaultIdx]
Example:
findPage(app.pages, '/feed')          // exact match
findPage(app.pages, 'feed')           // tries '/feed' as well
findPage(app.pages, '/nodes/abc123')  // matches /nodes/:id
findPage(app.pages, '/unknown', 0)    // returns pages[0]

pageTitle(page, app) -> string

Extract the display title for a page.

Parameters:
ParamTypeDescription
pageanyPage definition
appanyApp definition

Returns: Title string.

Resolution order:
  1. page.data['#header.title']
  2. page.data['#title']
  3. app.name
  4. ''

mergeServerFeed(state, table, serverRows) -> Record<string, any>

Merge server-sourced feed data into local state with deduplication.

Parameters:
ParamTypeDescription
stateRecord<string, any>Current state
tablestringState key (e.g., 'messages')
serverRowsany[]Rows from server

Returns: New state with merged table.

Behavior:
  1. Gets existing array from state[table].
  2. Identifies server rows by body text.
  3. Filters local-only rows (not in server set).
  4. Merges: local-only first, then server rows.
  5. Sorts by _ts descending (newest first).

parseDataViewRow(raw, myPub?) -> any

Parse a DataView row into a flat state row.

Parameters:
ParamTypeDefaultDescription
rawanyrequiredRaw DataView row
myPubstringundefinedCurrent user's public key

Returns: Flat row object:

{
  from: string,         // author pubkey
  body: string,         // content body
  media: string,        // same as from (for avatar)
  trailing: string,     // relative time
  outgoing: boolean,    // true if from === myPub
  _ts: number,          // created_at timestamp
  _id: string,          // event_id
}

Behavior: Parses raw.content as JSON if it's a string. Extracts from, body, trailing from content. Sets outgoing based on whether from === myPub.


shouldIngest(table, eventFrom, myPub?) -> boolean

Check if an event should be ingested into a state table.

Parameters:
ParamTypeDescription
tablestringTarget table name
eventFromstringEvent author pubkey
myPubstringCurrent user's pubkey (optional)

Returns: boolean

Rule: Tables prefixed with my_ only accept events where eventFrom === myPub. All other tables accept all events.


2. algebra.js -- State Machine Engine

The pure-function state machine. No DOM, no React, no framework. Runs identically in Node.js tests and in the browser.

Source: sdk-ui-kit/algebra.js


execAction(state, actionExpr, walletPub?, inputValue?) -> Record<string, any>

Execute set() action expressions against state.

Parameters:
ParamTypeDefaultDescription
stateRecord<string, any>requiredCurrent state
actionExprstringrequired+-separated set() expressions
walletPubstring''Current wallet pubkey
inputValuestring''Input value ($value/$input)

Returns: New state object (shallow copy with modifications).

Supported Value Forms in set(key, value):
ValueResolution
nullnull
[]Empty array []
$walletwalletPub parameter
$pubinputValue parameter
$valueinputValue parameter
$inputinputValue parameter (alias)
/pathLiteral string (navigation path)
push(key)Prepend state[key] as new item to target array
delete(key)Remove items from array where pub/from matches
insert(key)Add item with pub field to array
push(key) Details:

Creates a new item: { from: shortPub, body: state[key], media: shortPub, trailing: 'now', outgoing: true } and prepends it to the target array.

delete(key) Details:

If key starts with $, resolves from state. Filters out items where item.pub or item.from matches the resolved value.

insert(key) Details:

Creates a minimal row using the template of existing items. Sets pub to the resolved value and trailing to 'now'.

Side Effect: Auto-syncs profiles table when profile_name, display_name, or my_name changes and state.identity exists:

state.profiles[state.identity.slice(0, 16)] = { name: newValue }
Example:
import { execAction } from '@enc-protocol/ui-kit/algebra'
 
// Navigate
execAction(state, 'set(path, /feed)')
 
// Connect wallet + navigate
execAction(state, 'set(identity, $wallet) + set(path, /feed)', '79be...')
 
// Post a message (push from draft)
execAction(state, 'set(messages, push(draft)) + set(draft, null)', '79be...')
 
// Clear
execAction(state, 'set(draft, null) + set(messages, [])')
 
// Delete by pub
execAction(state, 'set(contacts, delete($pub))', '', 'c6047f...')
 
// Insert by pub
execAction(state, 'set(following, insert($pub))', '', 'c6047f...')

apply(state, mock, ctx?) -> Record<string, any>

Execute a mock operation against state.

Parameters:
ParamTypeDefaultDescription
stateRecord<string, any>requiredCurrent state
mockanyrequiredMock definition (see below)
ctxobjectundefinedContext (inputValue, etc.)
Mock Forms:
  1. Function (legacy): (state, ctx) => patchObject. Called with state and context. Return value is merged into state.
  2. Plain object (legacy): { key: value, ... }. No op field. Deep-cloned and merged into state.
  3. Single structured op: { op: 'insert'|'delete'|'update'|'query'|'set', ... }
  4. Compound ops: { ops: [op1, op2, ...] }. Executed sequentially.
Structured Operation Details:

insert

{ op: 'insert', table: 'messages', values: { body: 'hello' }, append?: boolean }

Prepends (or appends if append: true) a row to state[table].

delete

{ op: 'delete', table: 'invites', where: { pub: 'abc' } }

Removes rows where ALL where fields match.

update

{ op: 'update', table: 'users', where: { pub: 'abc' }, set: { role: 'admin' } }

Merges set fields into all matching rows.

query

{ op: 'query', table: 'profiles', where: { key: '$target' }, into: 'found' }

For array tables: filters rows matching where, stores in state[into]. For object tables: looks up by first where value, stores in state[into].

set

{ op: 'set', values: { path: '/feed', draft: '' } }

Merges values directly into state.

All values support $key references resolved from state.


currentPage(app, state) -> any

Find the current page from gates + path.

Parameters:
ParamTypeDescription
appanyApp definition
stateanyCurrent state

Returns: Page definition object.

Behavior:
  1. Check gates in order. First matching gate returns app.pages[gate.page].
  2. Exact path match: page.path === state.path.
  3. Variable path match: /nodes/:node_id matches /nodes/abc.
  4. Fallback: app.pages[0].

resolve(app, state, path) -> any

Resolve #path to a value from page bindings + state.

Parameters:
ParamTypeDescription
appanyApp definition
stateanyCurrent state
pathstringBinding path (e.g., #header.title)

Returns: Resolved value (string, array, object, or undefined).

Resolution Forms:
Path FormResolution
#node.paramLook up page.data['#node.param'], resolve refs
#nodeLook up page.data['#node']
#list[N].fieldLook up list binding, access item N, map field

Resolver handling: If the value starts with @ and contains :, it's a cross-table resolver: @table.field:stateKey -> state[table][state[stateKey]][field].

State key handling: If the value matches /^[a-z_]+$/ and exists in state, returns state[value].

List assertion handling (#list[N].field):
  1. Finds the list binding (entry containing listId<).
  2. Gets the source array from state[stateKey].
  3. Accesses item at index N.
  4. Maps the field through the field map.
  5. If the mapped field starts with @, performs a resolver lookup.

runStep(app, state, step, walletPub?) -> Record<string, any>

Run a single do/see step. Execute mock or UI action, return new state.

Parameters:
ParamTypeDefaultDescription
appanyrequiredApp definition
stateRecord<string, any>requiredCurrent state
stepanyrequiredStep definition
walletPubstring''Wallet pubkey
Step Shape:
{
  set: { key: value, ... },   // Optional: state patches applied FIRST
  do: '#action' | 'mock',     // Optional: action to execute
  see: { path: expected },    // Optional: assertions (checked by caller)
  idx: number,                // Optional: target list item index
  in: 'app_id',               // Optional: target app for cross-app tests
}

Returns: New state object after applying set patches and executing the do action.

Behavior:
  1. Apply step.set patches (with $wallet resolution and profiles sync).
  2. If step.do starts with #: look up action expression from current page's actions map.
    • Tab/filter select: resolves $value to next tab item (cycling). Explicit step.idx picks a specific tab.
    • List item actions: resolves $pub from clicked item. Default idx=0.
    • If action expression is a mock name, executes via mock system.
    • Post detail navigation: populates post_body, post_liked, post_action_label, post_stats, post_comments.
    • Profile navigation: populates profile_bio, profile_action, profile_posts, profile_stats, browsing_following, browsing_followers.
  3. If step.do doesn't start with #: execute as mock from app.mocks.
  4. If step.action (legacy): execute via execAction.

runWorkflow(app, steps, walletPub?, opts?) -> { state, states, results }

Run a full test workflow. Execute all steps and return results.

Parameters:
ParamTypeDefaultDescription
appanyrequiredHost app definition
stepsany[]requiredArray of step definitions
walletPubstring''Wallet pubkey
opts.allAppsany[][app]All available apps
opts.synthesizefunctionundefinedsynthesizeApp function
Returns:
{
  state: { ... },           // Final state of the host app
  states: {                 // Per-app final states (keyed by app.id)
    'hello': { ... },
    'mini_feed': { ... },
  },
  results: [                // Per-step results
    {
      step: { ... },        // Original step
      state: { ... },       // State after this step (deep clone)
      status: 'pass'|'fail',
      error: null|string,   // e.g., '[in: hello] #title: expected "X" got "Y"'
    },
  ]
}
Cross-App Features:
  • step.in: Run step against a different app's state.
  • last_synth_manifest: Auto-synthesizes mini-app and registers in allApps.
  • last_open_action.app_id: Auto-initializes target app with host identity (for synthesized mini-apps only; standalone apps keep their own gate).
  • Per-app states are maintained independently throughout the workflow.

assertSee(app, state, see, assertFn) -> void

Assert UI tree values match expected.

Parameters:
ParamTypeDescription
appanyApp definition
stateanyCurrent state
seeobject{ path: expectedValue } pairs
assertFnfunction(actual, expected, message) => void

Behavior: For each path-value pair:

  • $key paths: check state[key].
  • Other paths: call resolve(app, state, path).
  • Convert to string, compare, call assertFn.

Event-Centric Pathway

These functions mirror Enc.UI.Interp in Lean.

resolveValue(ctx, v) -> any | undefined

Resolve a WriteOp Value.

Value FormResult
{ lit: s }String s
{ litInt: n }Number n
{ litNull: true }null
{ placeholder: k }ctx.bindings[k] or ctx.wallet
{ binOp: {op,lhs,rhs}}String concat (`

Returns undefined for func, subquery, bare col.

resolveWhereVal(ctx, wv) -> any | undefined

Restricted subset of resolveValue (no binOp/func/subquery).

toEvent(ctx, op) -> UIEvent | null

WriteOp -> UIEvent. Resolves all values up-front.

UIEvent Shape:
{
  kind: 'created' | 'updated' | 'deleted',
  table: string,
  from: string,                    // ctx.wallet
  row?: [col, value][],            // for 'created'
  sets?: [col, value][],           // for 'updated'
  whereClause?: [col, op, val][], // for 'updated'/'deleted'
}

applyEvent(state, event) -> Record<string, any>

UIEvent -> state. Pure function.

  • created: Prepend row to table.
  • updated: Map rows, apply sets to matching.
  • deleted: Filter out matching rows.

applyWriteOp(ctx, state, op) -> Record<string, any>

Bridge: toEvent then applyEvent. Returns unchanged state if toEvent returns null.

applyWriteSeq(ctx, state, writeSeq) -> Record<string, any>

Apply list of WriteOps left-to-right.

setExprToWriteOp(key, rawValue) -> WriteOp

Desugar set(key, value) to UPDATE against _state table.

setExprToWriteOp('draft', '$value')
// { update: { table: '_state',
//             sets: [['value', { placeholder: 'value' }]],
//             whereClause: [['key', '=', { lit: 'draft' }]] } }

SDK Codegen Functions

pascalize(s) -> string

snake_case -> PascalCase. pascalize('send_message') -> 'SendMessage'.

kindSuffix(kind) -> string

'created' -> 'Created', 'updated' -> 'Updated', 'deleted' -> 'Deleted'.

writeOpKind(op) -> string | null

Extract kind from WriteOp.

writeOpTable(op) -> string | null

Extract table name from WriteOp.

defaultEventName(writeName, seqLen, op) -> string

Single-op: PascalCase(writeName). Multi-op: PascalCase(writeName) + PascalCase(table) + Suffix.

writeOpParams(op) -> string[]

INSERT: col list. UPDATE: SET cols + WHERE cols. DELETE: WHERE cols.

lookupParamType(manifest, table, col) -> [string, boolean]

Returns [type, encrypted]. Supports bare primitives ("string"), qualified primitives ({ type: "string", encrypted: true }). Falls back to ['string', false].

typedParams(manifest, op) -> [string, string, boolean][]

[name, type, encrypted] triples for all parameters.

emitSdk(appName, manifest) -> { name, methods }

Generate SDK class. Returns { name: string, methods: SdkMethod[] }.

interface SdkMethod {
  name: string              // PascalCase event name
  event: string             // target table
  kind: string              // 'created'|'updated'|'deleted'
  paramList: [string, string, boolean][]  // [name, type, encrypted]
}

emitEnclaveEvents(manifest) -> EventDecl[]

Generate enclave event declarations.

interface EventDecl {
  eventName: string
  tableName: string
  kind: string
  fields: [string, string, boolean][]
}

extractEvents(ctx, astWrites) -> [[string, UIEvent[]]]

Extract UIEvents from astWrites. Accepts array or object form.

extractFromManifest(ctx, manifest) -> [[string, UIEvent[]]]

Convenience: extractEvents(ctx, manifest.astWrites).


3. tree-utils.ts -- Tree Utilities

Source: sdk-ui-kit/tree-utils.ts


applyStyles(node, styles, path?) -> any

Merge a styles map into a tree.

Parameters:
ParamTypeDefaultDescription
nodeanyrequiredTree node
stylesRecord<string, any>requiredPath-to-style map
pathstring''Current path (for recursion)

Returns: New tree with styles applied (does not mutate input).

Path Matching:
  • . matches root (when path === '')
  • #id matches node with that id
  • #parent>[N] matches N-th anonymous child
Style Resolution:
  • String: resolved via resolveStyle(hash) from style registry
  • Object: merged with spread into existing node.style

validateTree(node, path?) -> string[]

Check tree for unknown atoms/components.

Parameters:
ParamTypeDefaultDescription
nodeanyrequiredTree root
pathstring''Current path

Returns: Array of error strings. Empty = valid.

Checks:
  1. node.atom in ATOM_NAMES (str, media, input, select, multiselect, bool, slider, arr, null)
  2. node.component found in COMPONENTS array
  3. Recursively validates node['./'] children

4. resolve-bindings.ts -- Page Resolution

Source: sdk-ui-kit/resolve-bindings.ts


resolveBindings(tree, bindings, state) -> any

Resolve page data bindings into a render tree.

Parameters:
ParamTypeDescription
treeanyComponent tree (deep-cloned)
bindingsRecord<string, any>page.data bindings
stateRecord<string, any>Current app state

Returns: New tree with bindings injected.

Binding Processing (Phase 1 -- collect per-node data):
  1. Arrow bindings (#node<stateKey): Maps state[stateKey] array through the field map. Sets nodeData[node].items and .messages.

  2. Function bindings (value contains (): Skipped (action expressions are not data).

  3. Dotted bindings (#node.param): Sets nodeData[node][param]. String values: resolver (@table.field:stateKey) or state key (/^[a-z_]+$/ test) or literal. Array values: badge-count substitution ({key} -> (N)).

  4. Direct bindings (#node): For arrays, sets .items and .fields. For strings, sets ._direct.

Binding Processing (Phase 2 -- inject into tree):

Walks the tree. For each node with a matching id in nodeData:

  • _direct values: mapped by atom type (str->value, media->src, input->label, component->title).
  • Other values: set directly on node.data[key].

Recurses into node['./'] children.


buildPageTree(page, state, components, applyStyles, opts?) -> any

Build a renderable tree for one page.

Parameters:
ParamTypeDescription
pageobject{ page: string, data: object }
stateobjectCurrent state
componentsobject[]COMPONENTS catalog
applyStylesfunctionapplyStyles function
opts.stripHeaderbooleanRemove header comp (default true)
Pipeline:
  1. Find component by page.page name.
  2. Apply component's styles via applyStyles.
  3. Resolve bindings via resolveBindings.
  4. Optionally strip header (for hosts that render their own).

5. theme.ts -- Color Tokens

Source: sdk-ui-kit/theme.ts

TG Object

Telegram-dark theme tokens. Single source of truth for all UI Kit colors.

const TG = {
  bg:              '#0e0e1a',
  headerBg:        '#1a1a2e',
  chatBg:          '#0e0e1a',
  bubbleOut:       '#00564a',
  bubbleOutText:   '#e0f0ec',
  bubbleIn:        '#1e1e38',
  bubbleInText:    '#e0e0e0',
  inputBg:         '#1a1a30',
  textPrimary:     '#e6e6f0',
  textSecondary:   '#7e7e96',
  separator:       'rgba(255,255,255,0.06)',
  accent:          '#00d4aa',
  danger:          '#ff4444',
  warning:         '#f0a030',
  iconPlaceholder: '#333',
} as const

PaletteKey Type

type PaletteKey = keyof typeof TG

Use instead of string for compile-time color token validation.


6. sql-algebra.js -- SQL Query Engine

Source: sdk-ui-kit/sql-algebra.js

init() -> Promise<Database>

Initialize SQLite (async). Browser: WASM. Node.js: asm.js. Cached.

initSync() -> Database

Synchronous init (Node.js only, uses require).

loadState(state) -> void

Load JS state into SQLite. Drops all tables first.

Mapping:
  • Arrays -> tables (columns from first row, _rowid autoincrement)
  • Objects -> KV tables (key PRIMARY KEY + expanded fields)
  • Scalars -> _state (key, value) rows
  • Booleans are stored as '1'/'0' strings

query(sql, params?) -> object[]

Execute SELECT. Returns rows as objects. Resolves $key from params then from _state.

mutate(sql, params?) -> boolean

Execute INSERT/UPDATE/DELETE. Same param resolution. Returns success flag.

getScalar(key) -> string | null

Read from _state table.

setScalar(key, value) -> void

Write to _state table (INSERT OR REPLACE).

exportState() -> object

Reverse mapping: SQLite -> JS state.

  • _state rows -> scalars (with boolean/null conversion)
  • KV tables (with key column) -> objects
  • Array tables -> arrays (with '1'/'0' boolean conversion)

schema() -> string

Returns all CREATE TABLE statements as a string.


7. synth.js -- App Synthesizer

Source: sdk-ui-kit/synth.js

synthesizeApp(manifest) -> AppDefinition

Expand manifest into full app definition.

Manifest shape: { id: string, name: string, template: 'feed'|'chat'|'form', params?: object }

Throws on missing id/name or unknown template.

Template params documented in main spec (docs/ui-kit.md section 18).

Output: Complete app definition with synthesized: true, including pages, mocks, tests, initial_state, preset_state. No /connect gate (mini-apps inherit identity from host).

TEMPLATE_CATALOG

[
  { id: 'feed', name: 'Feed', description: 'List of posts with a compose bar.' },
  { id: 'chat', name: 'Chat', description: 'Contacts list + 1:1 chat threads.' },
  { id: 'form', name: 'Form', description: 'Form that collects submissions.' },
]

8. Re-exported Constants

From app-adaptor.ts:

  • COMPONENTS -- from config/comps/index.ts. Array of 18 ComponentDef.
  • ENC_APPS -- from config/apps/index.js. Array of 10 app definitions.
  • resolveBindings -- from resolve-bindings.ts.
  • buildPageTree -- from resolve-bindings.ts.

From config/apps/index.js:

ALICE_PUB     = '79be667ef9dcbbac'
ALICE_WALLET  = '79be667ef9dcbbac55a06295ce870b07'
BOB_PUB       = 'c6047f9441ed7d6d'
CHARLIE_PUB   = 'f9308a019258c310'

Each app is now stored as config/apps/<id>/ui.json (pure data) plus an optional config/apps/<id>/mocks.js sidecar (function-valued mocks). The index.js loader is dual-mode — it uses import.meta.glob under Vite and falls back to node:fs under Node, then merges ui.json + mocks.js back into the canonical ENC_APPS[] shape. Downstream consumers see the same flat array they did before the split.

From index.ts (package entry):

export { resolvePageTree, applyAction, createTestRunner, getVisibleText } from './app-adaptor'
export { TG, type PaletteKey } from './theme'