Search by

idei / usim

idei

UI Services Implementation Model Framework

Package info

github.com/idei/usim

pkg:composer/idei/usim

Statistics

Installs: 122

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.3.1 2026-10-20 13:09 UTC

README

A Server-Driven UI framework for Laravel. Define your entire user interface in PHP — screens, menus, forms, tables, modals — and let the framework render, diff, and update everything automatically on the client.

Table of Contents

Requirements

Dependency Version
PHP ^8.2
Laravel 10.x / 11.x / 12.x / 13.x
laravel/sanctum ^3.0 / ^4.0
spatie/laravel-permission ^6.0
symfony/finder ^6.0 / ^7.0
nikic/php-parser ^5.7
symfony/var-dumper ^6.0 / ^7.0
illuminate/contracts ^10.0 / ^11.0 / ^12.0 / ^13.0

What Is New Since 0.6.0 (v0.7.0)

  • Container appearance API: ->card() and ->plain() fluent helpers on Container to switch between card and flat visual variants.
  • Container tabs API: ->tabs(), ->tabItem(), ->activeTab(), ->onTabChange(), ->onTabClose() and ->tabColors() allow server-driven tab strips with theme-aware defaults, disabled tabs, closable tabs, and per-tab color overrides.
  • Carousel and calendar components consume CSS theme tokens for consistent light/dark styling.
  • Bug fix: Screen now always persists state after postLoadUI(), fixing stale cache on ?reset=true reloads.
  • Bug fix: Checkbox checked state now syncs correctly from incremental server responses.

For the full release details, see CHANGELOG.md.

Installation

composer require idei/usim

Laravel's package auto-discovery will register UsimServiceProvider automatically.

Quick Start

Run the install command to scaffold a complete working application with authentication, profile, menus, configuration, and routes:

php artisan usim:install

Then follow the printed instructions:

php artisan migrate
./start.sh [-r]

Starting the Application

The ./start.sh script uses RoadRunner instead of Laravel's artisan serve command. This is necessary because artisan serve is single-threaded and does not properly support the framework's concurrent execution requirements.

Usage

Note:

Visit http://localhost:8000 — you have a working USIM app.

Use --force to overwrite existing files.

Core Concepts

Screens

A Screen is a PHP class that defines a full page. Each screen extends Screen and builds its UI inside buildBaseUI():

Screen Operational Model

In USIM, treat a Screen as a stateful backend UI service, not as a passive template:

  • A Screen owns UI structure, interaction rules, and persisted state.
  • buildBaseUI() defines the initial component tree.
  • getRoutePath() derives the canonical URL from namespace/class naming.
  • Event actions resolve to on<ActionName>(array $params) handlers on the same class.
  • Request cycle is: restore state -> run handler -> compute diff -> send only delta.
  • Authorization is part of the Screen contract (authorize, checkAccess).
  • Menu metadata also belongs to the Screen contract (getMenuLabel, getMenuIcon, getRoutePath).

This model keeps backend as the source of truth and avoids business-logic duplication across client and server.

Important clarification: USIM does not register one Laravel route per Screen class. Instead, it uses a catch-all web route plus a generic /api/ui/{screen} loader and resolves URL <-> Screen class through naming convention.

<?php

namespace App\UI\Screens;

use Idei\Usim\UI;
use Idei\Usim\Enums\LayoutType;
use Idei\Usim\Screen;
use Idei\Usim\Components\Container;

class HelloScreen extends Screen
{
    protected function buildBaseUI(Container $container, ...$params): void
    {
        $container
            ->layout(LayoutType::VERTICAL)
            ->padding(20);

        $container->add(
            UI::label('title')
                ->text('Welcome to the UsersManager')
                ->style('h1')
        );

        $container->add(
            UI::button('hello_btn')
                ->label('Hello USIM!')
                ->primary()
                ->action('hello_button_clicked')
        );
    }

    public function onHelloButtonClicked(array $params): void
    {
        $this->toast('Data refreshed!', 'success');
    }
}

After creating the file, register it:

php artisan usim:discover

Then visit /hello-screen in your browser.

UI — The Component Factory

UI is a static factory that creates component builders. Every builder uses a fluent API:

// Labels
UI::label('greeting')->text('Hello World')->style('h2')->center();
UI::label('legal_copy')->html('legal.terms-snippet');

// Buttons
UI::button('save')->label('Save')->style('primary')->action('save_form');
UI::button('floating_help')
    ->label('Help')
    ->style('secondary')
    ->position('BOTTOM_RIGHT')
    ->offsets(24, 24)
    ->action('open_help');

// Inputs
UI::input('email')->label('Email')->type('email')->required(true)->placeholder('you@example.com');

// Containers (layouts)
$row = UI::container('toolbar')
    ->layout(LayoutType::HORIZONTAL)
    ->gap('10px');

$row->add(UI::button('btn_a')->label('A'));
$row->add(UI::button('btn_b')->label('B'));

$container->add($row);

$tabs = UI::container('workspace_tabs')
    ->tabs([
        ['id' => 'overview', 'label' => 'Overview', 'color' => '#dbeafe', 'active_color' => '#2563eb'],
        ['id' => 'reports', 'label' => 'Reports', 'disabled' => true, 'disabled_color' => '#e2e8f0'],
        ['id' => 'settings', 'label' => 'Settings', 'closable' => true],
    ], 'overview')
    ->onTabChange('workspace_tab_changed')
    ->onTabClose('workspace_tab_closed')
    ->tabColors([
        'active_tab_color' => 'var(--ui-surface)',
        'active_tab_text_color' => 'var(--ui-text-strong)',
    ]);

$tabs->add(UI::label('overview_copy')->text('Overview content'), tab: 'overview');
$tabs->add(UI::label('reports_copy')->text('Reports content'), tab: 'Reports');

$container->add($tabs);

Event Handlers

When a button fires an action (e.g. ->action('save_form')), the framework calls a handler method on the same screen class. The convention is on + PascalCase action name:

Action string Handler method
save_form onSaveForm(array $params)
delete_item onDeleteItem(array $params)
navigate_home onNavigateHome(array $params)

The $params array contains all current component values (inputs, selects, checkboxes, etc.) from the client.

State Management

Screen state is server-side. The framework automatically:

  1. Builds the UI tree on first load
  2. Stores the serialized state
  3. On events, restores state → runs your handler → diffs old vs new → sends only the delta to the client

Properties prefixed with store_ are persisted across requests and mirrored on the client as part of the USIM storage payload.

Persistence is now plain by default:

  • store_* values are serialized as regular JSON values so the client can inspect and use them directly.
  • Add the _crypt suffix only for sensitive values that must be protected before being sent to client storage.
  • Browser clients persist storage in sessionStorage (scoped per browser tab) and send X-UI-Client-Id alongside X-USIM-Storage, guaranteeing state and authentication isolation across tabs.
  • This makes client-side decisions possible for non-sensitive state such as store_theme, and also enables finer-grained storage synchronization because the client can apply only the changed keys instead of replacing one fully encrypted blob each time.
class MyScreen extends Screen
{
    protected string $store_username = '';       // persisted in plain text
    protected int $store_page = 1;              // persisted in plain text
    protected string $store_theme = 'light';    // readable by the client
    protected string $store_token_crypt = '';   // persisted encrypted
    protected string $tempValue = '';           // NOT persisted
}

Use _crypt only when the value should not be readable from client storage.

Available Components

Factory Method Builder Class Description
UI::label() Label Text labels, headings, paragraphs
UI::button() Button Action buttons with styles
UI::input() Input Text, email, password, hidden inputs
UI::select() Select Dropdown selects
UI::checkbox() Checkbox Checkboxes and toggles
UI::form() Form Form grouping
UI::table() Table Data tables with pagination
UI::card() Card Cards with title, description, actions
UI::container() Container Layout container (vertical/horizontal/grid) with card() / plain() appearance and optional server-driven tabs
UI::menuDropdown() MenuDropdown Navigation dropdown menus
UI::uploader() Uploader File upload with preview and crop
UI::calendar() Calendar Calendar/date picker
UI::carousel() Carousel Media carousel for image/audio/video with manual/auto modes
UI::split() Split Resizable/collapsible split panel layout
UI::timer() Timer Autonomous timer for polling, delays, and scheduled events

Builders share a fluent API across UIComponent and Container with common methods like:

->visible(bool $visible)
->width(string $width)
->padding(mixed $padding)
->margin(mixed $margin)
// ... and many more styling options

Screens in Depth

Creating a Screen

  1. Create a class in app/UI/Screens/ (or a subdirectory):
// app/UI/Screens/Products/List.php
namespace App\UI\Screens\Products;

use Idei\Usim\Screen;
use Idei\Usim\Components\Container;
use Idei\Usim\UI;

class List extends Screen
{
    protected function buildBaseUI(Container $container, ...$params): void
    {
        $container->add(
            UI::label('title')->text('Products')->style('h1')
        );

        // Add your table, filters, etc.
    }
}
  1. Run discovery:
php artisan usim:discover
  1. Visit /products/list — the URL is automatically derived from the namespace path.

Screen Discovery

USIM uses Symfony Finder to scan the configured screens directory and generates a manifest cache:

php artisan usim:discover
# Output: Found 12 screens. USIM manifest generated successfully!

The manifest maps URL slugs to screen classes using a CRC32-based offset system for deterministic component IDs.

Authorization

Override authorize() to control access:

// Public screen (default)
public static function authorize(): bool
{
    return true;
}

// Authenticated users only
public static function authorize(): bool
{
    return self::requireAuth();
}

// Guests only (e.g. login screen)
public static function authorize(): bool
{
    return !self::requireAuth();
}

// Role-based
public static function authorize(): bool
{
    return self::requireRole('admin');
}

// Permission-based
public static function authorize(): bool
{
    return self::requirePermission('manage-users');
}

When authorization fails, the framework automatically redirects to login (for guests) or shows a 403 (for insufficient permissions).

Menu Integration

Screens integrate with the navigation menu via static methods:

public static function getMenuLabel(): string
{
    return 'My Screen';
}

public static function getMenuIcon(): ?string
{
    return '📊';
}

Then in your Menu screen, use $menu->screen(MyScreen::class) for automatic linking with permission checks:

$menu->screen(UsersManager::class);                          // auto label + icon
$menu->screen(Products\List::class, 'All Products', '📦'); // custom label + icon

Lifecycle Hooks

Method When
buildBaseUI($container) Called on first load to build the initial UI tree
postLoadUI() Called after state is restored — update components with live data
onResetScreen() Called when ?reset=true is passed in the URL
protected function postLoadUI(): void
{
    // Update components with current data after state restoration
    $user = Auth::user();
    $this->input_name->value($user->name);
    $this->input_email->value($user->email);
}

Component auto-injection: If you declare a typed property with the same name as a component ID, USIM automatically injects the builder instance. For example, protected Input $input_name; will be populated with the input created as UI::input('input_name').

Event System

Handling Button Actions

// In buildBaseUI:
$container->add(
    UI::button('btn_save')
        ->label('Save')
        ->action('save_item')    // → calls onSaveItem()
);

// Handler:
public function onSaveItem(array $params): void
{
    $name = $params['input_name'] ?? '';
    $email = $params['input_email'] ?? '';

    // Save to database...

    $this->toast('Item saved!', 'success');
}

Cross-Service Events

Emit events that ALL active screen services receive using UsimEvent:

use Idei\Usim\Events\UsimEvent;

// Emit from anywhere:
event(new UsimEvent('user_logged_in', ['user' => $user]));

Any screen with a matching handler will react:

// In Menu screen or any other screen:
public function onUserLoggedIn(array $params): void
{
    $user = $params['user'];
    $this->updateMenuForUser($user);
}

Built-in UI Helpers

Available inside any Screen handler:

// Show a toast notification
$this->toast('Operation successful', 'success');  // types: success, error, info, warning

// Navigate to another URL
$this->redirect('/products');
$this->redirect();           // reload current screen

// Close the currently open modal
$this->closeModal();

// Update modal fields
$this->updateModal([
    'field_name' => ['error' => 'This field is required']
]);

// Show an error page
$this->abort(404, 'Not found');

// Switch theme on the client
$this->changeTheme('dark');

// Switch language on the client and set application locale
$this->changeLanguage('es');

Labels can now render raw HTML or an existing Blade view:

UI::label('welcome_copy')
    ->html('emails.verify-email', ['user' => $user]);

Most components also support anchor-based positioning helpers for floating or pinned UI:

UI::container('floating_panel')
    ->position('TOP_RIGHT')
    ->positionMode('fixed')
    ->offsets(16, 16);

Database Translations

USIM now supports package-level database translations with key-based identifiers and language fallback.

Published migration stubs create these tables:

  • usim_languages
  • usim_text_keys
  • usim_text_values

The TranslationService manages CRUD for languages, keys, and values (including optional media fields).

Global utility helper:

t('app.welcome', ['name' => 'Emilio']);

Resolution order:

  1. current locale in DB
  2. fallback locale in DB (en by default)
  3. key literal

Translation values support placeholders (:name, :count, etc.) and optional media metadata (media_url, media_meta).

Auto-key behavior for human-readable text (t('Some text')):

  • key length limit is configurable via usim.i18n.auto_key_max_length (env: USIM_I18N_AUTO_KEY_MAX_LENGTH, default 20)
  • when truncation is needed, USIM tries to continue to the next separator so the current word is not cut mid-word
  • escaped and real line breaks are normalized before key generation and fallback text storage

I18n suggestion logging:

  • when a key is auto-generated from human-readable text, USIM emits an i18n warning suggesting to replace the literal text with the generated key
  • log context includes generated key, source text, group, file, line, and best-effort character position
  • configure channel with usim.i18n.log_channel (env: USIM_I18N_LOG_CHANNEL, default i18n)
  • enable/disable with usim.i18n.log_autokey_suggestions (env: USIM_I18N_LOG_AUTOKEY_SUGGESTIONS, default true)

Recommended key naming for package and scaffolded code:

  • usim.component.* for reusable package component defaults.
  • usim.dialog.* and usim.time_unit.* for framework-level dialog and timer labels.
  • usim.common.* for shared scaffold labels/placeholders.
  • usim.auth.*, usim.admin.*, usim.menu.* for generated screen/component UI text.
  • usim.service.* for scaffolded service response and validation messages.

When adding or changing scaffold text, prefer t('...') with one of the namespaces above and include the key in UsimTranslationSeeder.

Modals & Dialogs

Quick Confirmation Dialogs

Use ConfirmDialog for standard dialogs:

use Idei\Usim\Modals\ConfirmDialog;
use Idei\Usim\Enums\DialogType;

public function onDeleteItem(array $params): void
{
    ConfirmDialog::open(
        caller: $this,
        type: DialogType::CONFIRM,
        title: 'Delete Item',
        message: 'Are you sure you want to delete this item?',
        confirmAction: 'confirm_delete',
        cancelAction: 'cancel_delete',
    );
}

public function onConfirmDelete(array $params): void
{
    // Perform the delete...
    $this->closeModal();
    $this->toast('Item deleted', 'success');
}

Dialog types: INFO, CONFIRM, WARNING, ERROR, SUCCESS, CHOICE, TIMEOUT.

Custom Modals

Build custom modal content using any component and set ->parent('modal'):

$modal = UI::container('my_modal')
    ->parent('modal')
    ->padding('20px');

$modal->add(UI::input('field_a')->label('Name'));
$modal->add(
    UI::button('btn_submit')
        ->label('Submit')
        ->action('submit_modal')
);

Data Tables

For paginated server-side data tables, extend AbstractTableModel:

use Idei\Usim\DataTable\AbstractTableModel;

class ProductsTable extends AbstractTableModel
{
    public function getColumns(): array
    {
        return [
            ['name' => 'id', 'type' => 'int'],
            ['name' => 'name', 'type' => 'string'],
            ['name' => 'price', 'type' => 'float'],
        ];
    }

    public function getFormattedPageData(int $currentPage, int $perPage): array
    {
        return Product::paginate($perPage, ['*'], 'page', $currentPage)
            ->items();
    }
}

Then use it in your screen:

$table = UI::table('products_table');
$dataModel = new ProductsTable($table);
// configure columns, pagination, etc.
$container->add($table);

File Uploads

Use the Uploader for file uploads with temporary storage, preview, and image cropping:

use Idei\Usim\Enums\AspectRatio;

$uploader = UI::uploader('avatar')
    ->label('Profile Photo')
    ->allowedTypes(['image/*'])
    ->maxFiles(1)
    ->maxSize(2)       // MB
    ->aspect(AspectRatio::_1_1)    // crop ratio
    ->size(1);         // display size

$container->add($uploader);

In your event handler, confirm the upload to move it from temporary to permanent storage:

public function onSaveProfile(array $params): void
{
    if ($filename = $this->uploader_avatar->confirm($params, 'images', $oldFilename)) {
        $user->avatar = $filename;
        $user->save();
    }
}

Temporary uploads are automatically cleaned up hourly via a scheduled job.

Image placeholders

An Image without a src receives a generated placeholder by default. The default generator creates an SVG locally using the component caption and configured colors. Its dimensions use pixel-sized width/height when provided, or the configured placeholder dimensions; AspectRatio fills in a missing dimension:

use Idei\Usim\Enums\AspectRatio;
use Idei\Usim\ValueObjects\Size;

$image = UI::image('post_image')
    ->width(Size::px(640))
    ->placeholderAspect(AspectRatio::_16_9)
    ->placeholderId($post->id)
    ->placeholderColors('#e2e8f0', '#475569')
    ->caption($post->title);

For images saved by USIM's upload flow, storage() resolves a relative filename with UploadService::fileUrl(). Absolute URLs and root-relative paths remain unchanged:

$image = UI::image("post_img_{$post->id}")
    ->src($post->image ?? '')
    ->storage('uploads/images')
    ->placeholderAspect(AspectRatio::_16_9)
    ->placeholderId($post->id);

placeholderId() provides a stable identity for seeded image services. To use Picsum, set USIM_IMAGE_PLACEHOLDER_GENERATOR to Idei\Usim\Services\ImagePlaceholders\PicsumImagePlaceholderGenerator; the URL uses the configured seed prefix and the same identity to return a stable image. To integrate another image API (including one requiring an API key), implement ImagePlaceholderGeneratorInterface and configure its fully-qualified class in USIM_IMAGE_PLACEHOLDER_GENERATOR, or bind that interface in the application.

The available .env settings are USIM_IMAGE_PLACEHOLDER_ENABLED, USIM_IMAGE_PLACEHOLDER_WIDTH, USIM_IMAGE_PLACEHOLDER_HEIGHT, USIM_IMAGE_PLACEHOLDER_BACKGROUND, USIM_IMAGE_PLACEHOLDER_TEXT_COLOR, and USIM_IMAGE_PLACEHOLDER_SEED_PREFIX. An explicit src always takes precedence; an action-based image shows its placeholder while the request is in progress. fallback() remains the separate behavior for a source that fails to load.

Authentication Scaffolding

php artisan usim:install provides a complete authentication and admin system:

Screen Path Description
Login /auth/login Email/password login with Sanctum tokens
ForgotPassword /auth/forgot-password Send password reset link via email
ResetPassword /auth/reset-password Reset password form
EmailVerified /auth/email-verified Email verification handler
Profile /auth/profile User profile (name, photo, password change)
Admin\UsersManager /admin/dashboard User management table with CRUD and role assignment (admin only)

Supporting files:

  • AuthController — API endpoints for register, login, logout, verify email, reset password
  • UsimUser trait — Custom notification methods for password reset and email verification
  • UserService — Full user management: find, get, create, update (with role sync, email validation, notifications)
  • Default root/admin/user provisioning is now handled during php artisan usim:install through config-driven database upsert.
  • EventServiceProvider — App-level event/listener registration scaffold
  • Email view stubs — Styled Blade views for password reset and email verification emails
  • Terms view — Blade view for terms and conditions display
  • Migrations — temporary_uploads, profile_image column, and terms_accepted_at column on users table

Default Users (via .env)

After install, configure your .env:

ADMIN_FIRST_NAME=Admin
ADMIN_LAST_NAME=User
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=your-secure-password

USER_FIRST_NAME=Regular
USER_LAST_NAME=User
USER_EMAIL=user@example.com
USER_PASSWORD=your-secure-password

The installer now provisions the default access data during php artisan usim:install; no separate package seeder is required.

Testing Screens

This package ships a self-contained testing guide under docs/:

  • docs/SCREEN_TESTING_GUIDE.md — Human-oriented guide with patterns, helpers, and examples.
  • docs/tests_prompt.md — Copy/paste prompt template to ask any agent/chat to generate new screen tests.

Recommended workflow:

  1. Read the guide to follow the project conventions (uiScenario, component-level assertions, response contracts).
  2. Use docs/tests_prompt.md as a base when delegating test generation to an AI agent.
  3. Validate locally with php artisan test (or file-level execution first).

Core approach used across this project:

$ui = uiScenario($this, SomeScreen::class, ['reset' => true]);

$ui->component('btn_submit')->expect('action')->toBe('submit_form');

$response = $ui->click('btn_submit', ['field' => 'value']);
$response->assertOk();
expect($response->json('toast.type'))->toBe('success');

$ui->assertNoIssues();

Core Contracts & Mockable Architecture

USIM is decoupled from the host application using Dependency Inversion (DIP). The core package never depends on host models like App\Models\User or concrete database libraries directly. Instead, it relies on strict interfaces in Idei\Usim\Contracts:

Interface Namespace Purpose
AuthorizableActorInterface Idei\Usim\Contracts Entities that can be checked for permissions (getAuthIdentifier())
UsimUserInterface Idei\Usim\Contracts User profile representation (getEmail(), getName())
PairableActorInterface Idei\Usim\Contracts Kiosk and physical device actor abstraction (isPaired(), getPairingIdentifier())
ScreenAuthorizerInterface Idei\Usim\Contracts Evaluates permissions/roles for screens without tight coupling to Spatie or DB
UnitsServiceInterface Idei\Usim\Contracts Contract for organizational units synchronization
UnitContextResolverInterface Idei\Usim\Contracts Resolves active operational unit slugs
UnitSyncResult Idei\Usim\Contracts Readonly, strongly-typed result DTO for unit synchronizations

In-Memory Testing with Mocks

Because screens authorize via ScreenAuthorizerInterface, you can test authorization in unit tests without database migrations, seeds, or Spatie permissions:

use App\Models\User;
use App\UI\Screens\Admin\UsersManager;
use Idei\Usim\Contracts\ScreenAuthorizerInterface;
use Mockery;

it('authorizes admin via mock authorizer', function () {
    $user = new User(['id' => 1, 'email' => 'admin@example.com']);

    $authorizer = Mockery::mock(ScreenAuthorizerInterface::class);
    $authorizer->shouldReceive('can')->with($user, 'users.manage', Mockery::any())->once()->andReturn(true);
    app()->instance(ScreenAuthorizerInterface::class, $authorizer);

    $screen = new UsersManager();
    expect($screen->authorize($user))->toBeTrue();
});

Configuration

Publish the config file (done automatically by usim:install):

php artisan vendor:publish --tag=usim-config

This creates config/usim.php:

return [
    'front_store_key'           => env('FRONT_STORE_KEY', 'my-app'),
    'screens_namespace' => 'App\\UI\\Screens',
    'screens_path'      => app_path('UI/Screens'),
    'api_url'           => env('API_BASE_URL', env('APP_URL')),
    'upload_disk'       => env('UPLOAD_DISK', 'local'),
    'users'             => [
        'roles' => [
            // admin/user role metadata + seed_user defaults
        ],
    ],
    'i18n'              => [
        'default_locale'  => env('USIM_DEFAULT_LOCALE', env('APP_LOCALE', 'en')),
        'fallback_locale' => env('USIM_FALLBACK_LOCALE', 'en'),
    ],
];
Key Description Default
front_store_key Unique application identifier used to scope persisted UI storage keys my-app (override via FRONT_STORE_KEY)
screens_namespace PSR-4 namespace where screens live App\UI\Screens
screens_path Filesystem path to scan for screens app/UI/Screens
api_url Base URL for internal HTTP calls APP_URL
upload_disk Laravel filesystem disk for uploaded files local (override via UPLOAD_DISK)
users.roles Role metadata and default seeded users admin / user roles scaffold
i18n.default_locale Preferred locale for DB translation lookup APP_LOCALE or en
i18n.fallback_locale Fallback locale for DB translations en

Headless Mode

USIM can run in headless mode, serving API endpoints only without requiring a web renderer.

Configuration

Set the environment variable:

USIM_HEADLESS_MODE=true

Behavior

  • When USIM_HEADLESS_MODE=true: Requests to the web catch-all route return 406 Not Acceptable with JSON error
  • All clients must consume /api/ui/{screen} and /api/ui-event endpoints directly
  • API endpoints function identically in both modes
  • Ideal for backend-driven applications, AI agents, mobile apps, or multi-client architectures

Example (Node.js/JavaScript Client)

// Load a screen
const screenResponse = await fetch('/api/ui/login');
const screen = await screenResponse.json();

console.log(screen);
// {
//   "10": { "type": "container", "parent": "root", ... },
//   "11": { "type": "input", "parent": 10, ... },
//   "agent_context": {
//     "purpose": "User login with email/password",
//     "inputs": ["email", "password"],
//     "outputs": ["redirect", "toast", "abort"]
//   }
// }

// Extract agent context (if present)
if (screen.agent_context) {
    console.log('Screen purpose:', screen.agent_context.purpose);
}

Example (PHP Client)

// Use Laravel HTTP client or GuzzleHttp to consume API directly
$response = Http::get(config('usim.api_url') . '/api/ui/admin/dashboard');
$screen = $response->json();

if (isset($screen['agent_context'])) {
    // Agent access to metadata
    $context = $screen['agent_context'];
}

API Endpoints

USIM registers these routes automatically:

Method URI Description
GET /api/ui/{screen} Load a screen (returns JSON UI tree)
POST /api/ui-event Handle an event (returns JSON diff)
POST /api/upload/temporary Upload a file to temporary storage
DELETE /api/upload/temporary/{id} Remove a temporary upload
GET /files/{path} Serve uploaded files

The client-side JavaScript (ui-renderer.js) handles these calls automatically.

Artisan Commands

Command Description
php artisan usim:discover Scan screens directory and generate manifest cache
php artisan usim:install Scaffold a complete USIM application (screens, auth, admin, tests)
php artisan usim:install --force Overwrite existing files

Octane / RoadRunner Support

USIM is compatible with long-running processes. The service provider automatically resets the component ID generator on each request when laravel/octane is detected:

// Automatic — no configuration needed
$events->listen(\Laravel\Octane\Events\RequestReceived::class, function () {
    UIIdGenerator::reset();
});

The UIChangesCollector is registered as a scoped singleton, ensuring clean state per request.

Directory Structure

After usim:install, your application will have:

app/
├── Http/Controllers/Api/
│   └── AuthController.php        # Auth API endpoints
├── Models/
│   └── User.php                  # With UsimUser, HasRoles, HasApiTokens traits
├── Services/
│   ├── Auth/
│   │   ├── AuthSessionService.php
│   │   ├── LoginService.php
│   │   ├── PasswordService.php
│   │   └── RegisterService.php
│   └── User/
│       └── UserService.php       # Full CRUD, role management, email notifications
└── UI/
    ├── Components/
    │   ├── DataTable/
    │   │   └── UserTableModel.php  # Paginated user table
    │   └── Modals/
    │       ├── EditUserDialog.php
    │       └── RegisterDialog.php
    └── Screens/
        ├── Home.php              # Landing page
        ├── Menu.php              # Navigation menu (links UsersManager for admins)
        ├── Admin/
        │   └── UsersManager.php     # User management (admin only)
        └── Auth/
            ├── Login.php
            ├── ForgotPassword.php
            ├── ResetPassword.php
            ├── EmailVerified.php
            └── Profile.php
config/
└── usim.php                      # USIM configuration (includes users.roles)
database/
├── migrations/
│   ├── *_create_temporary_uploads_table.php
│   ├── *_add_profile_image_to_users_table.php
│   └── *_add_terms_accepted_at_to_users_table.php
└── seeders/
    └── (consumer-defined seeders only; USIM no longer ships package seeders)
providers/
└── EventServiceProvider.php
resources/views/emails/
├── reset-password.blade.php
└── verify-email.blade.php
resources/views/
└── terms.blade.php
routes/
├── api-auth.php                  # Auth API routes
└── web.php                       # + catch-all route for screens

The package itself lives in vendor/idei/usim/ (or packages/idei/usim/ during development).

License

MIT