milpa/live

Render-target-agnostic live component primitives for the Milpa PHP framework: contracts, value objects, components, data sources, and the event-driven mount/handle/render lifecycle.

Maintainers

Package info

github.com/getmilpa/live

pkg:composer/milpa/live

Transparency log

Statistics

Installs: 9

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-07-08 20:59 UTC

This package is auto-updated.

Last update: 2026-07-08 21:17:33 UTC


README

Milpa

Milpa Live

Render-target-agnostic live components for the Milpa PHP framework — the same component definition renders to web AND terminal; state, data sources, and an event-driven interception seam, no HTML or ANSI in the component itself.

CI Packagist PHP License Docs

milpa/live is the render-target-agnostic core of Milpa's live component system: a component owns its contract (props/state schema, declared actions), its initial state (mount()), and how it reacts to client-originated actions (handle()) — but never how it turns into markup or terminal output. That's a ComponentRendererInterface's job, paired with the component at the call site. One component, any number of renderers.

Install

composer require milpa/live

Quick example

A minimal component plus two renderers — one for HTML, one for TUI — sharing the exact same mount()/handle() logic:

use Milpa\Live\Contracts\Component\ComponentDefinitionInterface;
use Milpa\Live\Contracts\Rendering\ComponentRendererInterface;
use Milpa\Live\ValueObjects\{
    ComponentContext, ComponentContract, InteractionRequest,
    InteractionResult, RenderRequest, RenderResult, RenderTarget, StateSnapshot,
};

final class CounterComponent implements ComponentDefinitionInterface
{
    public static function contract(): ComponentContract
    {
        return new ComponentContract(name: 'counter', contractVersion: '1.0.0', actions: ['increment' => []]);
    }

    public function mount(array $props, ComponentContext $context): StateSnapshot
    {
        return new StateSnapshot(
            componentId: $context->componentId,
            componentName: 'counter',
            version: '1.0.0',
            data: ['count' => (int) ($props['start'] ?? 0)],
        );
    }

    public function handle(InteractionRequest $request): InteractionResult
    {
        return new InteractionResult(state: new StateSnapshot(
            componentId: $request->state->componentId,
            componentName: $request->state->componentName,
            version: $request->state->version,
            data: ['count' => $request->state->data['count'] + 1],
        ));
    }
}

final class HtmlCounterRenderer implements ComponentRendererInterface
{
    public function supportsTarget(RenderTarget $target): bool
    {
        return $target === RenderTarget::HTML;
    }

    public function render(ComponentDefinitionInterface $component, RenderRequest $request): RenderResult
    {
        $state = $request->state ?? $component->mount($request->props, $request->context);

        return new RenderResult(
            output: sprintf('<button data-count="%d">Count: %d</button>', $state->data['count'], $state->data['count']),
            state: $state,
            format: RenderTarget::HTML,
        );
    }
}

final class TuiCounterRenderer implements ComponentRendererInterface
{
    public function supportsTarget(RenderTarget $target): bool
    {
        return $target === RenderTarget::TUI;
    }

    public function render(ComponentDefinitionInterface $component, RenderRequest $request): RenderResult
    {
        $state = $request->state ?? $component->mount($request->props, $request->context);

        return new RenderResult(output: "[ Count: {$state->data['count']} ]", state: $state, format: RenderTarget::TUI);
    }
}

$component = new CounterComponent();
$context = new ComponentContext('demo-1');

$html = (new HtmlCounterRenderer())->render($component, new RenderRequest(context: $context, target: RenderTarget::HTML));
echo $html->output; // <button data-count="0">Count: 0</button>

$tui = (new TuiCounterRenderer())->render($component, new RenderRequest(context: $context, target: RenderTarget::TUI));
echo $tui->output; // [ Count: 0 ]

CounterComponent never printed a single tag or escape code — both renderers turned the exact same StateSnapshot into their own output, independently.

Web + TUI from one component

That's the thesis this package is built around: a component definition is a pure description of state and behavior; rendering is a separate, swappable concern. A ComponentRendererInterface declares which RenderTarget(s) it supports (HTML, TUI, or the forward-looking ANSI) and turns a mounted StateSnapshot into output for that target — nothing in ComponentDefinitionInterface ever needs to know which renderer, or how many, will consume it.

milpa/live ships the component contracts, the mount/handle lifecycle, data sources, and the event-driven interception seam (component.mounting/mounted, component.handling/handled, component.rendering/rendered — see LiveEventEmitter) — but no HTML and no ANSI renderer. The web surface (AutocompleteHtmlRenderer and friends) lives in milpa/live-web; a TUI renderer is a live candidate in the Milpa lab. This package is the seam both build on, not either surface itself.

Requirements

  • PHP ≥ 8.3
  • milpa/core ^0.5
  • psr/log ^3

Documentation

Full API reference: getmilpa.github.io/live — generated straight from the source DocBlocks and dressed with the Milpa design system.

Contributing

Contributions are welcome — see CONTRIBUTING.md. Please report security issues via SECURITY.md, and note that this project follows a Code of Conduct.

License

Apache-2.0 © TeamX Agency.

Milpa is designed, built, and maintained by TeamX Agency.