Skip to content

Components

PAM Native has one retained render tree and several authoring styles. Typed trees, .pam templates, class components, functional components, and single-file *.pam.php components all become the same Renderable → Element → PNT1/PNP1 plan.

There is no WebView, JavaScript runtime, virtual DOM, or second bridge.

use Pam\Native\App;
use Pam\Native\Style;
use Pam\Native\UI\Button;
use Pam\Native\UI\Column;
use Pam\Native\UI\Screen;
use Pam\Native\UI\Text;
App::run(fn () => Screen::make(
Column::make(
Text::make('Checkout')->style(
new Style(fontSize: 28),
),
Button::make('Pay')->onPress($pay),
)->style(
new Style(
flexGrow: 1,
padding: 24,
gap: 16,
),
),
));

Use typed trees for explicit construction, IDE navigation, and low-level control.

final class Checkout extends Component
{
private string $email = '';
private bool $loading = false;
public function render(): View
{
return View::make('screens.checkout');
}
public function pay(): void
{
// Business logic remains ordinary PHP.
}
}
<Screen>
<SafeAreaView class="flex-1 bg-white">
<Column class="flex-1 p-6 gap-4">
<Text height="56" fontSize="28">Checkout</Text>
<Input
model="email"
keyboardType="email"
sync="debounced"
placeholder="Email"
/>
<Button loading="$loading" on:press="pay">Pay</Button>
</Column>
</SafeAreaView>
</Screen>

Register view roots and the theme before running:

App::views(
__DIR__.'/resources/native',
__DIR__.'/.pam-native/views',
);
App::theme(Theme::pamLab());
App::run(new Checkout());

Templates are parsed and validated once. Expressions use a restricted engine, read component or data paths, and never call eval.

<?php
declare(strict_types=1);
namespace App\Components;
use Pam\Native\Attributes\State;
use Pam\Native\Component;
final class ProfileCard extends Component
{
#[State]
public bool $following = false;
public function __construct(
public string $name,
public ?string $subtitle = null,
) {
}
public function toggleFollowing(): void
{
$this->following = !$this->following;
$this->emit('changed', $this->following);
}
}
?>
<template>
<Column class="card p-4">
<Text class="profile-name">{{ $name }}</Text>
<Text p-if="$subtitle" class="profile-subtitle">
{{ $subtitle }}
</Text>
<Button @press="toggleFollowing">
{{ $following ? 'Following' : 'Follow' }}
</Button>
<Slot />
</Column>
</template>
<style scoped>
:root {
--muted: #5C5C55;
}
@font-face {
font-family: "Brand";
src: url("asset://assets/fonts/Brand-Bold.ttf");
font-weight: 700;
}
.profile-name {
font-family: "Brand";
font-weight: 700;
font-size: 15px;
}
.profile-subtitle {
margin-top: 2px;
color: var(--muted);
font-size: 13px;
}
</style>

Register component directories:

App::components(
__DIR__.'/src',
__DIR__.'/.pam-native/components',
);

Constructor-promoted public properties become props. Required parameters are required props; PHP defaults define optional props. A changed private or readonly constructor prop remounts the child rather than mutating it.

The compiler supports:

  • {{ expression }} interpolation;
  • :prop="expression" and conditional class bindings;
  • @press="method" native events;
  • @event="method" component events;
  • bind:value and bind:checked;
  • p-if, p-else-if, and p-else;
  • p-for over arrays, Traversable, or a positive integer;
  • default and named slots; and
  • key for stable identity.

Keep business rules in PHP methods rather than embedding them in markup. Use the PAM-native p-* directive spellings in every new or migrated template. A component root may itself use p-if. A false root becomes an inert invisible placeholder with no layout footprint, while multiple rendered roots remain an error.

As expressões preservam operadores familiares do PHP sem executar eval. Elas aceitam aritmética, concatenação com ., ??, ternários, caminhos em arrays/objetos e métodos públicos do componente.

Desde o PAM Native 0.5.87, templates também podem compor um conjunto explícito de funções puras:

<Text p-if="mb_strlen(trim($query)) < 2">
Digite pelo menos dois caracteres.
</Text>
<Text>{{ mb_strtoupper(mb_substr($displayName, 0, 1)) }}</Text>
<Text>{{ count($selectedIds) }} selecionados</Text>

A allowlist contém trim, ltrim, rtrim, strlen, mb_strlen, substr, mb_substr, strtolower, strtoupper, mb_strtolower, mb_strtoupper, count e in_array. Funções de arquivo, processo, rede e qualquer outra função PHP não listada continuam indisponíveis no markup. Regras de negócio e efeitos colaterais devem permanecer em métodos públicos do componente.

One optional <style scoped> block may follow </template>. PAM compiles its native-safe subset to the same typed properties used by inline attributes. The mobile application does not carry a browser CSS engine, selector runtime, or WebView.

The deterministic cascade is:

  1. native tag rule;
  2. matching .class rules in stylesheet source order; and
  3. inline PAM attributes.

Static class and dynamic :class use the same component-local rules. Reordering class names in markup does not change specificity. :root declares reusable --variables; nested references and var(--name, var(--fallback, value)) resolve at component compile time. Cycles, missing values without a fallback, and excessive expansion depth fail with the component path.

Text color, font family/size/weight/style, letter spacing, line height, alignment, and case inherit through native layout containers and nested .pam.php component templates. Since PAM Native 0.5.82, inherited CSS uses private render context instead of public constructor props or component variants. A strict icon or control inside a colored container therefore receives only attributes authored on its own tag, while its internal text still inherits the cascade. Logical font families stay intact during inheritance, so a descendant that changes only its weight can select the corresponding packaged @font-face.

PAM Native 0.5.54+ automatically prepends the conventional src/app.css sheet to every component. It is the application-wide home for fonts, tokens, tag defaults, and reusable semantic classes:

src/app.css
:root {
--ink-muted: #5C5C55;
}
@font-face {
font-family: "Space Grotesk";
src: url("asset://assets/fonts/SpaceGrotesk-Regular.ttf");
font-weight: 400;
}
Text,
Input {
font-family: "Space Grotesk";
}

src/app.css and local component styles may use nested relative @import statements. Every import resolves from the file that declared it and must stay inside the nearest Composer project; network imports, cycles, traversal, and oversized graphs fail compilation. The complete graph participates in component cache invalidation. The order is global tag rules, global classes, local tag rules, local classes, then inline PAM attributes.

On Android, PAM Native 0.5.85+ snaps each retained frame’s absolute start and end edges to the physical-pixel grid, then derives its native width and height from those shared edges. At fractional display densities this keeps centered text-and-icon controls on one physical center and prevents one-pixel seams or drift between adjacent flex siblings.

<template>
<Column class="profile-card">
<Text class="profile-name">{{ $name }}</Text>
<Text
p-if="$bio !== ''"
class="profile-bio"
:textColor="$highlighted ? '#1B7A4E' : '#5C5C55'"
>
{{ $bio }}
</Text>
</Column>
</template>
<style scoped>
.profile-card {
padding: 12px 16px;
border: 1px solid #D8D7CF;
border-radius: 12px;
box-shadow: 3px 3px 0 #FFD23F;
}
.profile-name {
font-weight: 700;
font-size: 15px;
line-height: 19px;
}
.profile-link {
color: #1B7A4E;
text-decoration: underline;
}
.profile-bio {
margin-top: 2px;
font-size: 13.5px;
}
</style>

The subset covers native dimensions and percentages, min/max constraints, position edges and inset, flex growth/shrink/direction/wrapping, gap and alignment, padding and margin shorthands, borders and radii, backgrounds, opacity, box-shadow, elevation, compositor transforms, overflow, images, and text/font properties. Logical padding-inline/block, margin-inline/block, and inset-inline/block map to the native edges. transform supports translateX/Y, scale/X/Y, and rotate; object-fit, visibility, box-sizing: border-box, aspect-ratio: 16 / 9, percentage opacity, border: none, and background: none are also compiled.

Text decoration accepts none, underline, line-through, and both the spaced and hyphenated underline-plus-line-through form. Plain numbers and px, dp, or pt represent PAM logical points; rem uses a stable native root of 16 logical points. Absolute left, top, right, and bottom offsets also accept percentages. When both insets on an axis remain auto, an absolutely positioned flex child uses its CSS static position. The parent’s justify-content controls the main axis, while align-items or the child’s align-self controls the cross axis. This keeps centered tab indicators, badges, and layered logos centered without hard-coded device offsets. border-radius accepts the standard one-to-four circular-radius shorthand and compiles it to native per-corner radii; elliptical slash syntax is unsupported.

box-shadow accepts one native shadow in familiar CSS order: x-offset y-offset [blur-radius] [spread-radius] [color], or none. The color may appear before or after the lengths. Android and iOS draw the typed shadow directly, including colored hard shadows; comma-separated and inset shadows fail compilation because they have no equivalent lightweight native contract.

Since PAM Native 0.5.84, auto-width text declared with @font-face is measured from the selected TTF/OTF face’s real glyph advances. Rust reads and caches those normalized metrics from the extracted application assets before the first native mount; there is no UI-thread measurement, second render, or visible correction. The platform renderer aligns visible glyphs from the relevant flex axis: align-items/align-self in columns and justify-content in rows. Missing, installed, or unsupported families retain the allocation-free generic estimator. Explicitly sized or growing text keeps normal start alignment unless text-align is authored. Use the familiar CSS values left, center, and right, or the logical aliases start and end.

border, border-top, border-right, border-bottom, and border-left accept <width> solid <color>. Directional forms compile to native edge widths and the shared border color; no CSS parser runs on the device. Individual border-*-color declarations are also accepted and update that shared native color.

Stylesheet colors follow CSS syntax. PAM supports the complete named-color set, transparent, #RGB, #RGBA, #RRGGBB, CSS #RRGGBBAA, comma/space rgb() and rgba(), and hsl()/hsla() with alpha:

.glass {
color: rgb(255 255 255 / 92%);
background-color: #FFFFFF29;
border: 1px solid hsl(140deg 45% 35% / 35%);
}

The compiler immediately normalizes these values to the protocol’s ARGB integer. For source compatibility, an eight-digit color written directly as a PAM attribute remains legacy #AARRGGBB. Prefer stylesheets for CSS #RRGGBBAA; direct attributes also accept named colors, transparent, short hex, and CSS color functions.

Template bindings support safe numeric arithmetic with conventional precedence: +, -, *, /, integer %, and parentheses. Expressions such as :height="72 + $bottomSpacing" stay inside PAM’s restricted interpreter; they never use eval. PHP . concatenation is also supported for scalar, null, and Stringable operands, so expressions such as '@'.$username remain natural while array coercions fail explicitly. Since PAM Native 0.5.81, right-associative PHP ?? safely falls back from null or a missing nested array/property path:

<Text>{{ $profile['bio'] ?? $profile['username'] ?? 'Sem bio' }}</Text>

Only tag selectors, .class selectors, comma-separated selectors, and component-local :root variables are accepted. Packaged font aliases may be declared with @font-face; each element resolves the closest weight and italic variant before the typed tree crosses into native code. Font-family stacks are accepted and their first family is the native preferred family. Descendant selectors, nested rules, media queries, and properties without a native protocol contract fail compilation instead of being silently ignored.

overflow: hidden clips descendants to the native border path, including rounded View, Row, Column, Pressable, and ImageBackground containers. PAM retains the rounded path between frames on Android and uses the UIKit layer mask on iOS; no WebView mask or application-side wrapper is created.

The Composer package installs a deterministic formatter:

Terminal window
vendor/bin/pam-native-format src
vendor/bin/pam-native-format --check src

It discovers *.pam.php recursively, preserves the PHP block for the project’s PHP formatter, indents templates and scoped CSS, and migrates deprecated v-* directives to p-*. Compile-time @import lines are normalized and preserved, while empty <style scoped> blocks are removed. The --check form is intended for CI.

The component reference is split by behavior so examples, events, cache policies, limits, and platform boundaries stay discoverable.

Family Components Guide
Layout Screen, View, Column, Row, Grid, SafeAreaView, Spacer Layout & views
Text and controls Text, Input, TextInput, Button, Pressable, Toggle, NativeSwitch Text, input & controls
Images Image, ImageBackground, DrawingCanvas Images & cache
Media MediaPlayer for video and audio Video & audio
Scrolling Scroll, ScrollView, FlatList, VirtualizedList, SectionList, VirtualGrid, RefreshControl Scroll & lists
Interaction GestureDetector, InteractionRegion, Animated, touchable compatibility aliases Interaction & motion
Presentation Modal, BottomSheet, ActivityIndicator, StatusBar, KeyboardAvoidingView, DrawerLayoutAndroid, InputAccessoryView Overlays & system UI
Web content WebView WebView
Extension CustomView and generated native factories Custom native views

Navigation hosts are documented separately in Navigation.

Every element can receive a stable key, typed protocol properties, children where its contract permits them, and a Style. Layout, transforms, opacity, accessibility, test identifiers, pointer behavior, intersection observation, resize observation and native events remain on the shared element contract.

$card = View::make(
Text::make('Native card'),
)
->key('card:42')
->style(new Style(
padding: 16,
borderRadius: 20,
backgroundColor: 0xFFFFFFFF,
))
->accessibilityLabel('Project PAM');

Use enums rather than numeric magic values for coded variants. Protocol IDs remain sequential integers internally, but application code should name their meaning.

The same numeric NodeKind reaches a platform-specific native control.

Family Android iOS
Layout Android ViewGroup hosts UIView
Text and buttons Android text/control views UILabel, UIButton
Input Native editable text PamInputField over UITextField
Images Native image pipeline UIImageView with cancelable URLSessionDownloadTask lifecycle
Scroll and lists Scroll host and recycled RecyclerView lists UIScrollView; list virtualization still requires parity evidence
Toggle and progress Native switch and indicator UISwitch, UIActivityIndicatorView
Presentation Android modal/drawer/refresh hosts PamModalHost, PamDrawerLayout, PamRefreshContainer
Custom native view Generated registry NativeViewRegistry and generated Swift factories

Declaring a node kind means the iOS protocol can create and update it. It does not imply that Android-only behavior, list recycling, accessibility, or every composed component state has already passed iOS verification.

Grid defaults to twelve columns and accepts any PAM element. Spans can change at breakpoints, rows size to their tallest child, and the grid reflows for orientation, split screen, and foldable posture.

<Grid gutterX="16" gutterY="16">
<Column span="12" spanSm="6" spanMd="4">
<Image :source="$photo->url" aspectRatio="1" />
<Text>{{ $photo->title }}</Text>
</Column>
</Grid>

For spans, offsets, order, rich two-column image grids, VirtualGrid, flex rules, and integer repetition, use the complete responsive layout guide.

Terminal window
pam mobile make:screen Orders
pam mobile make:component MetricCard
pam mobile make:native-view CameraPreview

Generators are non-destructive. The package also ships a JSON schema for pam-native.json and custom HTML data for .pam tag completion.