@pyreon/primitives — API Reference
Generated from
primitives'ssrc/manifest.ts— the same source that powersllms.txtand MCPget_api. Do not edit this page by hand; edit the manifest. For the conceptual guide, see primitives.
The multiplatform UI vocabulary for Pyreon. ONE canonical name per concept (<Stack> not <View>/<VStack>/<div>; onPress everywhere, not onClick vs action:). Web renders real DOM via @pyreon/runtime-dom; on iOS/Android the PMTC compiler intercepts the JSX at build time and emits idiomatic SwiftUI / Compose (the import is a type-anchor on native). Tokens-first styling (padding={4}, gap="md") resolves through the theme per target. No responsive props / animations in v1 — apps needing responsive web use @pyreon/elements directly. CRITICAL boundary for native: PMTC compiles your component SOURCE in a narrow declarative TS subset, NOT npm libraries — see get_pattern({ name: "multiplatform" }) for the supported subset + the silent-failure cliff.
Features
15 canonical primitives compile to web DOM + iOS SwiftUI + Android Compose from one .tsx
One canonical name + event per concept —
<Stack>(not View/VStack/div),onPresseverywhereTokens-first styling (
padding={4},gap="md") resolves through the theme per targetPMTC compiles your component SOURCE in a narrow declarative TS subset — NOT npm libraries
<WebView>hosts a web-only component (charts/flow/editor) natively with a bidirectional data bridge<Web>/<NativeIOS>/<NativeAndroid>escape hatches for genuinely per-platform UIuseNativeModuleFFI — add a platform capability the framework does not ship (Bluetooth, ARKit, a vendor SDK) as an app-level Swift/Kotlin class, no framework PRNo responsive props or animations in v1 — responsive web uses
@pyreon/elementsdirectly
Complete example
A full, end-to-end usage of the package:
import { Stack, Inline, Text, Heading, Button, Field, Toggle } from '@pyreon/primitives'
import { signal, computed } from '@pyreon/reactivity'
type Todo = { id: number; title: string; done: boolean } // type alias, NOT interface (PMTC drops interface on native)
export function App() {
const todos = signal<Todo[]>([])
const draft = signal('')
const remaining = computed(() => todos().filter((t) => !t.done).length)
return (
<Stack gap="md" padding={4}>
<Heading level={1}>Todos ({remaining()} left)</Heading>
<Inline gap="sm">
<Field value={draft()} onChangeText={(t) => draft.set(t)} />
<Button onPress={() => { todos.set([...todos(), { id: todos().length, title: draft(), done: false }]); draft.set('') }}>
Add
</Button>
</Inline>
<For each={todos()} by={(t) => t.id}>
{(t) => (
<Inline gap="sm">
<Toggle value={t.done} onChange={(v) => todos.set(todos().map((x) => x.id === t.id ? { ...x, done: v } : x))} />
<Text>{t.title}</Text>
</Inline>
)}
</For>
</Stack>
)
}Exports
| Symbol | Kind | Summary |
|---|---|---|
Stack | component | Primary layout container. |
Inline | component | Horizontal row — sugar for <Stack direction="row">. |
Layer | component | Stacked / overlay container. |
Scroll | component | Scrollable region. |
Spacer | component | Flexible gap that pushes siblings apart. |
Text | component | Inline text. |
Heading | component | Heading text. |
Image | component | Image. |
Icon | component | Icon by canonical name. |
Button | component | Styled CTA. |
Press | component | Unstyled tap target (no chrome). |
Link | component | Navigation link. |
Field | component | Text input. |
Toggle | component | Boolean switch/checkbox. |
Modal | component | Modal/sheet. |
WebView | component | Host a web page/component natively (WKWebView on iOS, Android WebView; <iframe srcdoc> on web). |
Web / NativeIOS / NativeAndroid | component | The Layer-4 per-platform escape hatch — one source carries a platform-specific subtree and exactly ONE branch renders pe |
defineNativeModule / useNativeModule | function | The Layer-4 FFI escape hatch — how an APP adds a platform capability the framework does not ship (Bluetooth, ARKit, a pa |
init / resetPrimitivesConfig | function | One-time app-boot configuration for @pyreon/primitives. |
API
Stack component
(props: { direction?: 'column' | 'row'; align?: Align; justify?: Justify; gap?: Space; wrap?: boolean; padding?: Space; children }) => VNodePrimary layout container. Web → <div style="display:flex;flex-direction:column|row">; iOS → VStack/HStack; Android → Column/Row. Default direction="column". gap/padding are theme-space tokens (number index OR "sm"|"md"|"lg").
Example
<Stack gap="md" align="center"><Text>a</Text><Text>b</Text></Stack>Common mistakes
Using
<View>/<VStack>/<div>— the canonical name is<Stack>(one name, all platforms)Expecting responsive props (breakpoint arrays) — not supported in v1; use @pyreon/elements for responsive web
See also: Inline · Layer · Scroll
Inline component
(props: { align?: Align; justify?: Justify; gap?: Space; wrap?: boolean; padding?: Space; children }) => VNodeHorizontal row — sugar for <Stack direction="row">. Web flex-row; iOS HStack; Android Row. ⚠ On Android <Inline> is a NON-WRAPPING Row (SwiftUI HStack shrinks to fit, but Compose Row overflows + clips the last children). Keep horizontal groups short, or use a vertical <Stack> for action lists.
Example
<Inline gap="sm"><Field value={q()} onChangeText={(t) => q.set(t)} /><Button onPress={search}>Go</Button></Inline>Common mistakes
Putting 5+ buttons in an <Inline> — they overflow + clip (become untappable) on Android; stack vertically or split
Relying on
wrapfor native multi-line — wrapping behavior differs per target
See also: Stack
Layer component
(props: { align?: Align; padding?: Space; children }) => VNodeStacked / overlay container. Web → position:relative + abs children; iOS → ZStack; Android → Box. Use for badges, overlays, layered composition.
Example
<Layer><Image src={hero} alt="" /><Text>overlaid caption</Text></Layer>Common mistakes
Using it for flow layout — Layer stacks children on the z-axis, not in a row/column
See also: Stack
Scroll component
(props: { direction?: 'vertical' | 'horizontal'; padding?: Space; children }) => VNodeScrollable region. Web → overflow:auto; iOS → ScrollView; Android → Column(verticalScroll) / Row(horizontalScroll). ⚠ Do not put a weighted <Spacer> inside a Scroll on Android (weight inside a scroll is invalid Compose).
Example
<Scroll><Stack gap="md">{/* long content */}</Stack></Scroll>Common mistakes
Nesting a
<Spacer>(weight) inside<Scroll>— invalid on Android Compose
See also: Stack
Spacer component
() => VNodeFlexible gap that pushes siblings apart. Web → flex spacer; iOS → Spacer; Android → Spacer(Modifier.weight(1f)). Use in an <Inline>/<Stack> to right-align or space-between.
Example
<Inline><Text>left</Text><Spacer /><Text>right</Text></Inline>Common mistakes
Using it inside a
<Scroll>on Android (weight + scroll conflict)
See also: Inline · Stack
Text component
(props: { color?: ColorToken; size?: 'xs'|'sm'|'md'|'lg'|'xl'; weight?: 'regular'|'medium'|'bold'; truncate?: boolean; children }) => VNodeInline text. Web <span>; iOS/Android Text. Read signals directly in children: <Text>{count()}</Text> (the compiler wraps it reactively). Avoid template literals on native — use string concat.
Example
<Text size="lg" weight="bold" color="primary">{label()}</Text>Common mistakes
Using a template literal
{Count: ${n()}}— partial native support; prefer{"Count: " + n()}Wrapping in
String(...)— unnecessary, numbers coerce in JSX text
See also: Heading
Heading component
(props: { level?: 1|2|3|4|5|6; color?: ColorToken; children }) => VNodeHeading text. Web <h1>–<h6> by level; iOS/Android a sized/weighted Text.
Example
<Heading level={2}>Section</Heading>Common mistakes
Omitting
levelwhen document outline matters (web a11y)
See also: Text
Image component
(props: { src: string; alt: string; fit?: 'cover'|'contain'|'fill'|'none'; width?: number|string; height?: number|string }) => VNodeImage. Web <img>; iOS Image; Android AsyncImage (Coil). src + alt REQUIRED. Bundled assets (via the asset pipeline) vs remote URLs dispatch per target.
Example
<Image src={logo} alt="Logo" width={120} height={40} fit="contain" />Common mistakes
Omitting
alt(required — a11y + it is the native contentDescription)
See also: Icon
Icon component
(props: { name: string; size?: 'sm'|'md'|'lg'; color?: ColorToken }) => VNodeIcon by canonical name. Web → svg; iOS → SF Symbol (Image(systemName:)); Android → Material Icons.Filled.*. The name maps through ICON_MAP; unmapped names warn + fall back.
Example
<Icon name="star" size="md" color="primary" />Common mistakes
Using a platform-specific icon id — use the canonical name; the compiler maps it per target
See also: Image
Button component
(props: { onPress: () => void; disabled?: boolean; variant?: 'primary'|'secondary'|'ghost'|'danger'; children }) => VNodeStyled CTA. Web <button>; iOS/Android Button. Handler is onPress (NOT onClick). Multi-statement handlers work: onPress={() => { a.set(1); b.set(2) }}.
Example
<Button variant="primary" onPress={() => count.set(count() + 1)}>Increment</Button>Common mistakes
Using
onClick— the canonical event isonPress(mapped to onClick/action:/onClick per target)Passing
onPress={maybeUndefined}— guard it; a non-function handler is a footgun
See also: Press · Link
Press component
(props: { onPress: () => void; onLongPress?: () => void; disabled?: boolean; children }) => VNodeUnstyled tap target (no chrome). Web <div role="button">; iOS Button {} (plain); Android Box(clickable). Use to make arbitrary content tappable; supports onLongPress.
Example
<Press onPress={() => select(item)}><Card item={item} /></Press>Common mistakes
Using
<Press>for a primary action — use<Button>for styled CTAs
See also: Button
Link component
(props: { to: string; external?: boolean; children }) => VNodeNavigation link. Web <a>; iOS/Android router-aware navigation. Integrates with @pyreon/router (to is a route path). external opens outside the app.
Example
<Link to="/profile">Profile</Link>Common mistakes
Hardcoding an href for internal routes — use
toso it routes natively too
See also: Button
Field component
(props: { value: string | (() => string); onChangeText: (next: string) => void; kind?: 'text'|'number'|'password'|'email'|'search'|'tel'|'url'; placeholder?: string; disabled?: boolean; onSubmit?: () => void }) => VNodeText input. Web <input>; iOS/Android TextField. Handler is onChangeText(next) (NOT onInput/onChange). value accepts a signal accessor for two-way binding.
Example
<Field value={draft()} onChangeText={(t) => draft.set(t)} placeholder="Search…" onSubmit={search} />Common mistakes
Using
onChange/onInput— the canonical handler isonChangeText(next: string)Forgetting
valueis the source of truth — write back viaonChangeText→ signal.set
See also: Toggle
Toggle component
(props: { value: boolean | (() => boolean); onChange: (next: boolean) => void; disabled?: boolean }) => VNodeBoolean switch/checkbox. Web checkbox; iOS Toggle; Android Switch. onChange(next: boolean).
Example
<Toggle value={enabled()} onChange={(v) => enabled.set(v)} />Common mistakes
Using
onPress/onClick— Toggle usesonChange(next: boolean)
See also: Field
Modal component
(props: { open: boolean | (() => boolean); onClose: () => void; children }) => VNodeModal/sheet. Web overlay; iOS .sheet(isPresented:); Android Dialog(onDismissRequest). Drive open with a signal; onClose fires on dismiss.
Example
<Modal open={showSheet()} onClose={() => showSheet.set(false)}><Stack>{/* sheet body */}</Stack></Modal>Common mistakes
Forgetting
onClose— needed so the platform dismiss gesture updates your signal
See also: Layer
WebView component
(props: { html?: string; src?: string; data?: unknown; onMessage?: (message: string) => void }) => VNodeHost a web page/component natively (WKWebView on iOS, Android WebView; <iframe srcdoc> on web). THE escape hatch for web-only packages (charts/flow/code/document) on native — they run inside the WebView. Bidirectional bridge: data is pushed in as window.__pyreonData (+ a pyreondata event, live, no reload); the page calls window.pyreonPostMessage(payload) → your onMessage closure.
Example
<WebView html={CHART_HTML} data={metrics()} onMessage={(m) => selected.set(m)} />Common mistakes
Using it for core UI (nav/forms/lists) — pays WebView boot + bundle cost; use native primitives there. Reserve <WebView> for self-contained web-island panes (charts/editors/diagrams)
Expecting native look-and-feel — content renders as a web view, not native widgets
See also: Web
Web / NativeIOS / NativeAndroid component
Web(props: { children }) => VNodeChild · NativeIOS(props: { children }) => VNodeChild · NativeAndroid(props: { children }) => VNodeChildThe Layer-4 per-platform escape hatch — one source carries a platform-specific subtree and exactly ONE branch renders per target. <Web> renders its children on WEB only (a layout-transparent Fragment, no wrapper element); <NativeIOS> / <NativeAndroid> render NOTHING on web (they return null — their children are emitted only on the iOS / Android target by PMTC). Reach for these for the rare genuinely-per-platform UI branch the 15 canonical primitives can't express (a web-only-rich chart/flow/table view vs a native equivalent or a <WebView> embed).
Example
<Web>{/* web-only-rich: <Chart>, <Flow>, <Table> */}</Web>
<NativeIOS>{/* Swift Charts, or a <WebView> embed */}</NativeIOS>
<NativeAndroid>{/* Compose chart, or a <WebView> embed */}</NativeAndroid>Common mistakes
Overusing them — defeats the one-source model; reach for them only when a target genuinely needs different UI.
Putting web-visible content in
<NativeIOS>/<NativeAndroid>— both render NOTHING on web (they are no-ops there); only<Web>content reaches the browser.
See also: WebView · init / resetPrimitivesConfig · defineNativeModule / useNativeModule
defineNativeModule / useNativeModule function
defineNativeModule<T>(name: string, webImpl: T) => T · useNativeModule<T>(name: string) => T · hasNativeModule(name: string) => booleanThe Layer-4 FFI escape hatch — how an APP adds a platform capability the framework does not ship (Bluetooth, ARKit, a payments/analytics SDK). PMTC lowers useNativeModule('X') to an instance of a class YOU provide (X() on iOS, X(context) on Android) and passes member calls through verbatim, so the platform compiler type-checks the surface; on web the same call resolves the defineNativeModule registration, keeping one source running on all three targets. await mod.method() composes with the async lowering with no extra machinery. This is distinct from <NativeIOS>/<NativeAndroid>, which only BRANCH between canonical-primitive subtrees — they cannot host raw platform code.
Example
type Bluetooth = { connect(id: string): Promise<boolean> }
// web implementation (native targets never run this)
defineNativeModule<Bluetooth>('Bluetooth', { connect: async () => false })
function Pairing() {
const bt = useNativeModule<Bluetooth>('Bluetooth')
return <Button onPress={() => { void bt.connect('cuff') }}>Connect</Button>
}Common mistakes
Passing a non-literal module name (
useNativeModule(NAME)) — it is emitted verbatim as the native class name and PMTC resolves one file at a time, so only a STRING LITERAL at the call site works; anything else warns and the declaration is skipped on native.Forgetting
defineNativeModule— native targets compile the call away, so a missing registration only surfaces on WEB, whereuseNativeModulethrows.Giving the Android class a no-arg constructor — the Compose emit injects
LocalContext.current, so it must take a SINGLEContextparameter (ignore it if unused); iOS is the opposite (no-argument initialiser).Declaring the Kotlin class in a different package from the generated sources — the emit references it UNQUALIFIED, so it must live in the
--kotlin-packagepackage.Expecting reactive re-render for free — mark the Swift class
@Observable/ back Kotlin state withmutableStateOfif its state should drive the view.Writing a method-bearing
type/interfaceand expecting a struct — a method-only contract type is intentionally not lowered to a native struct (its methods live on the platform class); a MIXED data+method type emits the data fields and warns about the dropped methods.
See also: Web / NativeIOS / NativeAndroid · WebView
init / resetPrimitivesConfig function
init(options: { navigate?: (to: string) => void }) => void · resetPrimitivesConfig() => voidOne-time app-boot configuration for @pyreon/primitives. The package is deliberately router-AGNOSTIC (a consumer using only <Stack>/<Text> never pulls a router into their graph), so <Link> needs a navigation handler supplied ONCE via init({ navigate }). With it, <Link> intercepts plain left-clicks and routes via navigate (SPA — no full reload); WITHOUT it, <Link> is a plain <a href> that does a normal full-page navigation — so links always WORK, init only UPGRADES them to SPA. init merges with any previous config (later calls override the keys they set). resetPrimitivesConfig() clears it back to defaults (primarily for tests / teardown). The config is a module-level singleton and SSR-safe (the server renders a static <a href>; navigate is read only inside a client click handler).
Example
import { init } from '@pyreon/primitives'
// at app boot, wire your router's navigate so <Link> does SPA navigation:
init({ navigate: (to) => myRouter.push(to) })Common mistakes
Wondering why
<Link>does a FULL PAGE RELOAD — you did not callinit({ navigate }). Without a navigate handler,<Link>falls back to a plain<a href>full-load; callinitonce at app boot with your router push.Expecting it to import a router — it is router-AGNOSTIC by design (works with any router, or none); YOU supply the
navigateclosure so the package never depends on@pyreon/router.
See also: Link · Web / NativeIOS / NativeAndroid