Skip to content

Kinetix Infolists Complete Reference

Kinetix Infolists is a schema-driven, DTO-powered system for rendering read-only record details. It is the display-only twin of Kinetix Forms: you describe what to show in PHP — entries, layouts, formatting, visibility — and the configuration is serialized into a structured JSON DTO that is consumed natively by Vue 3 components.

Use an infolist on a resource's "View" page, inside a detail drawer, or anywhere you need to present a record without editing it.


1. Core Architecture & Concept

Unlike forms, infolists carry no client-side state, validation, or hydration lifecycle. Each entry resolves its display value on the server during serialization, so the frontend only renders pre-computed, already-formatted data. This keeps the Vue layer stateless, fast, and free of memory leaks (no watchers or two-way bindings are created).

Key Principles

  1. Server-side state resolution — Every entry's value (and its formatting) is computed in PHP via getState(). The frontend never recomputes it.
  2. Stateless renderingKinetixInfolistEntries.vue holds no reactive state beyond a transient "copied" flag, eliminating leak-prone watchers.
  3. Tailwind JIT compliance — Column spans map to inline grid-column styles and colors map to a static class table, never to dynamically interpolated Tailwind class names.
  4. Shared contracts — Entries honour the same HasLabel, HasColor, and HasIcon enum contracts used by Tables, so a single Enum drives badges, icons, and colors everywhere.

2. Building an Infolist

php
use Happones\Kinetix\Infolists\Infolist;
use Happones\Kinetix\Infolists\Components\Section;
use Happones\Kinetix\Infolists\Components\Grid;
use Happones\Kinetix\Infolists\Components\TextEntry;
use Happones\Kinetix\Infolists\Components\IconEntry;

$infolist = Infolist::make($user)
    ->schema([
        Section::make('Account')
            ->description('Read-only account details.')
            ->icon('user')
            ->columns(12)
            ->schema([
                TextEntry::make('name')->columnSpan(6),
                TextEntry::make('email')
                    ->icon('mail')
                    ->copyable()
                    ->columnSpan(6),
                IconEntry::make('is_active')->boolean()->columnSpan(4),
                TextEntry::make('created_at')->dateTime()->columnSpan(8),
            ]),
    ]);

// Serialize for Inertia
return inertia('Users/Show', [
    'infolist' => $infolist->toArray(),
]);

One-line rendering

php
// Build + bind a record + serialize in a single call
$payload = UserInfolist::render($user);

Reusable Infolist classes

Extend Infolist and declare the schema in buildSchema() so it can be reused across controllers:

php
use Happones\Kinetix\Infolists\Infolist;

class UserInfolist extends Infolist
{
    protected function buildSchema(): array
    {
        return [
            Section::make('Account')->schema([
                TextEntry::make('name'),
                TextEntry::make('email')->copyable(),
            ]),
        ];
    }
}

// Usage
$payload = UserInfolist::render($user);

3. State Resolution Lifecycle

getRawState()

Resolves the value via dot-notation data_get($record, $name) — supporting relationship attributes like company.name — or via a custom ->state(fn ($record) => ...) callback. If the value is null/empty, the configured ->default() is applied.

formatStateUsing()

A closure to transform the raw value for display. Receives (mixed $state, ?Model $record):

php
TextEntry::make('full_name')
    ->state(fn ($record) => "{$record->first} {$record->last}");

TextEntry::make('status')
    ->formatStateUsing(fn ($state) => ucfirst($state));

Enum resolution

Backed enums resolve to their value, pure enums to their name, and enums implementing HasLabel resolve to getLabel() — automatically.


4. Entry Types

All entries share these base methods (defined on Entry):

MethodDescription
::make(string $name)Create an entry bound to a model attribute (dot-notation supported)
->label(string|Closure)Override the auto-generated label
->placeholder(string|Closure)Text shown when the state is empty (defaults to )
->default(mixed)Fallback value when the attribute is null/empty
->state(Closure)Resolve the value with a custom callback
->formatStateUsing(Closure)Transform the value before display
->icon(string|Closure)Leading icon (Lucide name)
->color(string|Closure)primary · success · warning · danger · info · gray
->url(string|Closure, bool $newTab = false)Render the value as a link
->columnSpan(int|string)Grid span ('full' or a column count)
->visible() / ->hidden()Boolean or Closure visibility
->visibleOn() / ->hiddenOn()Restrict by operation
->extraAttributes(array)Arbitrary attributes passed to the wrapper

TextEntry

The default entry for scalar values.

php
TextEntry::make('title');
TextEntry::make('email')->copyable()->icon('mail');
TextEntry::make('status')->badge()->color(fn ($state) => $state === 'active' ? 'success' : 'gray');
TextEntry::make('published_at')->dateTime();          // M j, Y g:i a
TextEntry::make('created_at')->date('d/m/Y');
TextEntry::make('price')->money('USD');               // $1,234.50 USD
TextEntry::make('bio')->limit(120);                   // truncate long strings
TextEntry::make('role')->inlineLabel();               // label + value on one row
MethodDescription
->badge(bool = true)Render the value as a colored badge
->copyable(bool = true)Show a copy-to-clipboard button
->date(string $format = 'M j, Y')Format a date value
->dateTime(string $format = 'M j, Y g:i a')Format a datetime value
->money(string $currency = 'USD')Format as currency
->limit(int)Truncate long strings with an ellipsis
->inlineLabel(bool = true)Place the label and value side by side

IconEntry

Renders an icon driven by the state — ideal for booleans and statuses.

php
IconEntry::make('is_verified')->boolean();   // check-circle/success or x-circle/danger

IconEntry::make('status')
    ->options([
        'heroicon-draft'     => 'draft',
        'heroicon-published' => 'published',
    ])
    ->colors([
        'warning' => 'draft',
        'success' => 'published',
    ]);
MethodDescription
->boolean()Map truthy → check-circle/success, falsy → x-circle/danger
->options(array $map)Map icon => value|Closure
->colors(array $map)Map color => value|Closure
->size(int|string)Icon size in pixels (default 24)

ImageEntry

php
ImageEntry::make('avatar')
    ->circular()
    ->size(80)
    ->defaultImageUrl(fn ($record) => "https://ui-avatars.com/api/?name={$record->name}");
MethodDescription
->circular() / ->square()Image shape
->size(int|string)Width/height in pixels (default 96)
->defaultImageUrl(string|Closure)Fallback when the attribute is empty

ColorEntry

php
ColorEntry::make('brand_color')->copyable();   // swatch + hex value
MethodDescription
->copyable(bool = true)Show a copy-to-clipboard button for the hex value

5. Layout Components

Layouts group entries and never resolve a value of their own. They honour the same visibility methods as entries.

Section

A titled card container.

php
Section::make('Billing')
    ->description('Invoicing & payment details.')
    ->icon('credit-card')
    ->columns(12)
    ->columnSpan('full')
    ->schema([
        TextEntry::make('plan')->columnSpan(6),
        TextEntry::make('renews_at')->date()->columnSpan(6),
    ]);
MethodDescription
::make(string|Closure $heading)Create a section with a heading
->description(string|Closure)Sub-heading text
->icon(string|Closure)Heading icon
->columns(int)Inner grid columns (default 12)
->schema(array)Child components

Grid

A column wrapper without card chrome.

php
Grid::make(12)->schema([
    TextEntry::make('width')->columnSpan(6),
    TextEntry::make('height')->columnSpan(6),
]);

Fieldset

A labelled, bordered group (<fieldset> + legend) — lighter than a Section card.

php
Fieldset::make('Billing')
    ->columns(12)
    ->schema([
        TextEntry::make('plan')->columnSpan(6),
        TextEntry::make('renews_at')->date()->columnSpan(6),
    ]);
MethodDescription
::make(string|Closure $label)Group label (rendered as the legend)
->columns(int)Inner grid columns (default 12)
->schema(array)Child components

Tabs

Groups entries into switchable tabs. Each Tab has its own label, optional icon, columns, and schema. The active tab is tracked client-side per Tabs instance (no shared state, no leaks); hidden tabs are stripped during serialization.

php
use Happones\Kinetix\Infolists\Components\Tabs;
use Happones\Kinetix\Infolists\Components\Tab;

Tabs::make()
    ->tabs([
        Tab::make('Profile')
            ->icon('user')
            ->columns(12)
            ->schema([
                TextEntry::make('name')->columnSpan(6),
                TextEntry::make('email')->columnSpan(6),
            ]),

        Tab::make('Activity')
            ->schema([
                TextEntry::make('last_login_at')->dateTime(),
            ]),
    ]);
Tabs methodDescription
::make(?string $label = null)Optional accessible label for the tab group
->tabs(array) / ->schema(array)The Tab components
Tab methodDescription
::make(string|Closure $label)Tab label
->icon(string|Closure)Tab icon (Lucide name)
->columns(int)Inner grid columns for the tab body (default 12)
->schema(array)Child components shown when the tab is active

6. Conditional Visibility

php
TextEntry::make('internal_notes')
    ->visible(fn ($record) => auth()->user()->isAdmin());

TextEntry::make('deleted_at')
    ->visibleOn('view')
    ->hidden(fn ($record) => $record->deleted_at === null);

Hidden entries are dropped during serialization, so they never reach the client.


7. Frontend Integration

After publishing the components (php artisan vendor:publish --tag=kinetix-components), render the serialized infolist:

vue
<script setup lang="ts">
import KinetixInfolist from '@/components/kinetix/KinetixInfolist.vue';
import type { KinetixInfolistData } from '@/types';

defineProps<{ infolist: KinetixInfolistData }>();
</script>

<template>
    <KinetixInfolist :infolist="infolist" />
</template>
ComponentResponsibility
KinetixInfolist.vueRoot wrapper; lays out top-level columns
KinetixInfolistEntries.vueRecursive renderer for sections, grids, and every entry type

8. Resource Integration

Resource exposes an infolist() hook alongside form() and table():

php
use Happones\Kinetix\Infolists\Infolist;
use Happones\Kinetix\Infolists\Components\TextEntry;

class UserResource extends Resource
{
    protected static ?string $model = User::class;

    public static function infolist(Infolist $infolist): Infolist
    {
        return $infolist->schema([
            TextEntry::make('name'),
            TextEntry::make('email')->copyable(),
            TextEntry::make('created_at')->dateTime(),
        ]);
    }
}
php
// In the resource's "Show" controller action
$infolist = UserResource::infolist(Infolist::make($user));

return inertia('Users/Show', ['infolist' => $infolist->toArray()]);

8b. Recipe: a record "Show" page with tabs + actions

A polished detail page: the record's data organized in tabs, plus Edit / Delete actions in a page header. Infolists are read-only and have no inline actions, so the actions live in a KinetixPageHeader (or Action::toArrayMany()) next to the infolist — wire them to your routes.

1. The infolist with tabs:

php
use Happones\Kinetix\Infolists\Infolist;
use Happones\Kinetix\Infolists\Components\{Tabs, Tab, Section, TextEntry};

$infolist = Infolist::make($user)->schema([
    Tabs::make()->tabs([
        Tab::make('Profile')->icon('user')->columns(2)->schema([
            TextEntry::make('name'),
            TextEntry::make('email')->copyable(),
            TextEntry::make('status')->badge()
                ->color(fn ($s) => $s === 'active' ? 'success' : 'gray'),
            TextEntry::make('created_at')->dateTime(),
        ]),
        Tab::make('Billing')->icon('credit-card')->schema([
            Section::make('Plan')->schema([
                TextEntry::make('plan.name')->label('Current plan'),
                TextEntry::make('plan.price')->money('USD'),
            ]),
        ]),
    ]),
]);

2. Page actions (header) — built server-side and passed to the page:

php
use Happones\Kinetix\Actions\{EditAction, DeleteAction};

return inertia('Users/Show', [
    'infolist' => $infolist->toArray(),
    'actions'  => \Happones\Kinetix\Actions\Action::toArrayMany([
        EditAction::make()->url(fn () => route('users.edit', $user)),
        DeleteAction::make()->inertiaVisit(route('users.destroy', $user), ['method' => 'delete']),
    ], $user),
]);

3. The Vue page pairs a header (actions) with the infolist:

vue
<script setup lang="ts">
import KinetixPageHeader from '@/components/kinetix/KinetixPageHeader.vue';
import KinetixInfolist from '@/components/kinetix/KinetixInfolist.vue';

defineProps<{ infolist: any; actions: any[] }>();
</script>

<template>
  <KinetixPageHeader heading="User details" :actions="actions" />
  <KinetixInfolist :schema="infolist" />
</template>

The Tabs/Section layouts render as a tabbed card with sections inside; EditAction/DeleteAction respect authorize() (drop when unauthorized) and route binding, exactly like in tables.


9. TypeScript Types

Serialized infolists map to generated TypeScript interfaces (KinetixInfolistData, KinetixInfolistEntry) in resources/js/types/index.ts, kept in sync via Spatie's Laravel TypeScript Transformer.

Released under the MIT License.