Project reference

Examples

Conditions are evaluated immediately by both adapters and remain serializable across the Inertia boundary:

Laravel form definition

final class UserForm
{
    public static function make(?User $user = null): Form
    {
        return Form::make('user')
            ->action($user ? route('users.update', $user) : route('users.store'))
            ->method($user ? 'put' : 'post')
            ->columns(2)
            ->validation(UserRules::class, $user ? 'update' : 'create')
            ->precognitive(mode: 'blur', debounce: 350)
            ->schema([
                TextInput::make('name')->required()->maxLength(255),
                TextInput::make('email')->email()->required(),
                Select::make('account_type')
                    ->options(['personal' => 'Personal', 'company' => 'Company'])
                    ->default('personal')
                    ->live(),
                TextInput::make('company_name')
                    ->visibleWhen('account_type', 'company')
                    ->requiredWhen('account_type', 'company'),
                Select::make('role')->options(Role::labels())->searchable(),
                Toggle::make('active')->default(true),
                Repeater::make('addresses')->schema([
                    TextInput::make('street')->required(),
                    TextInput::make('city')->required(),
                ])->columns(2),
            ])
            ->data($user?->toArray() ?? []);
    }
}

Reactive form conditions

use Inlay\Support\Condition;

TextInput::make('company_name')
    ->visibleWhen('account_type', 'company')
    ->requiredWhen('account_type', 'company')
    ->disabledWhen(Condition::falsy('enabled'));

TextInput::make('tax_id')
    ->hiddenWhen(Condition::blank('country'))
    ->live(onBlur: true, debounce: 350);

Available operators are equals, not-equals, in, not-in, truthy, falsy, filled, and blank. An equality-based requiredWhen() also produces the matching Laravel required_if validation rule.

Client adapters can observe fields marked live() without replacing their ordinary change handlers:

<Form
  resource={userForm}
  onLiveChange={({ path, value, data }) => {
    console.log(path, value, data)
  }}
/>
<Form
  :resource="userForm"
  @live-change="({ path, value, data }) => console.log(path, value, data)"
/>

Immediate, per-field debounce, and blur modes are supported. The live event is an integration hook; it does not automatically make a server request.

Laravel table definition

final class UsersTable
{
    public static function make(LengthAwarePaginator $users): Table
    {
        return Table::make('users')
            ->columns([
                TextColumn::make('name')->searchable()->sortable(),
                TextColumn::make('email')->searchable(),
                BadgeColumn::make('status')->colors([
                    'active' => 'success',
                    'disabled' => 'danger',
                ]),
            ])
            ->filters([
                SelectFilter::make('status')->options(Status::labels()),
            ])
            ->actions([
                Action::make('edit')->url('/users/{id}/edit'),
                Action::make('delete')->method('delete')->color('danger')->requiresConfirmation(),
            ])
            ->rows($users->items())
            ->pagination([
                'currentPage' => $users->currentPage(),
                'lastPage' => $users->lastPage(),
                'total' => $users->total(),
            ]);
    }
}

Buildable adapter examples live in examples/react and examples/vue.

React import wizard

import { ImportWizard } from '@inlayphp/imports-react'

<ImportWizard
  resource={userImport}
  onUpload={({ file }) => imports.upload(file)}
  onPreview={(request) => imports.preview(request)}
  onStart={(request) => imports.start(request)}
  onPoll={({ job }) => imports.status(job.id)}
/>

The Vue adapter exposes the same four transport callbacks through @inlayphp/imports-vue. Both adapters support PHP-preloaded previews, per-step overrides, class maps, and semantic theme tokens.

Laravel infolist

use Inlay\Infolists\Entries\IconEntry;
use Inlay\Infolists\Entries\TextEntry;
use Inlay\Infolists\Infolist;
use Inlay\Schemas\Components\Section;

$details = Infolist::make('user-details')
    ->schema([
        Section::make('Account')->columns(2)->schema([
            TextEntry::make('name')->copyable(),
            TextEntry::make('email')->url('mailto:{state}'),
            IconEntry::make('active')->boolean(),
        ]),
    ])
    ->data($user->toArray());

Render the resource with Infolist from @inlayphp/infolists-react or @inlayphp/infolists-vue.

PHP-defined admin panel

use Inlay\NavigationGroup;
use Inlay\NavigationItem;
use Inlay\Panel;
use Inlay\PanelProvider;

final class AdminPanelProvider extends PanelProvider
{
    public function panel(Panel $panel): Panel
    {
        return $panel
            ->path('/admin')
            ->brandName('Inlay Admin')
            ->collapsible()
            ->navigationGroups([
                NavigationGroup::make('management')->items([
                    NavigationItem::make('users')->url('/admin/users'),
                ]),
            ]);
    }
}

The application frontend mounts one Panel from @inlayphp/panels-react or @inlayphp/panels-vue; navigation, branding, layout mode, and theme come from PHP.

React theming and CSS customization

The React adapters accept lightweight theme tokens and root class names. Table also exposes typed per-slot class overrides:

When using Tailwind CSS 4, include the renderer-neutral class vocabulary in the application stylesheet so control, select, and button tokens are generated:

@source '../../node_modules/@inlayphp/*/src/**/*.{ts,tsx,vue}';
const theme = {
  accent: '#0f766e',
  radius: '0.75rem',
}

<Form className="my-form" resource={userForm} theme={theme} />
<Table
  className="my-table"
  classNames={{ filtersPanel: 'my-filter-panel', applyButton: 'my-apply-button' }}
  resource={usersTable}
  theme={{ ...theme, controlHeight: '2.5rem' }}
/>

Stable data-slot attributes expose structural elements without depending on internal Tailwind classes. Form fields also expose data-field, while filters expose data-filter:

.my-form [data-field='email'] {
  grid-column: 1 / -1;
}

.my-table [data-slot='toolbar'] {
  align-items: end;
}

.my-table [data-filter='status'] {
  min-width: 11rem;
}

Theme tokens cover fast brand changes. Root classes and data slots cover layout or product-specific styling without changing the PHP resource contract.