Skip to main content
Firstlight

Firstlight / Docs

Recipes for the screens a forms app needs, setup for your coding agent, Laravel on native screens, and accessibility.

View source
Media

Media

Media captures exactly one image or one document into Laravel Storage and binds a MediaValue on your native component. Use it when you need a form field with preview, clear, optional crop, and the same error slot as other Firstlight fields.

Complete example

Use FirstlightUI\NativeComponent for the full nonstreaming response-revision contract.

<?php

namespace App\NativeComponents;

use FirstlightUI\Media\MediaStorage;
use FirstlightUI\Media\MediaValue;
use Illuminate\View\View;
use FirstlightUI\NativeComponent;

class Profile extends NativeComponent
{
    public ?MediaValue $avatar = null;

    /** @var array<string, string> */
    protected array $rules = [
        'avatar' => 'required|image|max:2048',
    ];

    public function avatarChosen(string $tempPath): void
    {
        $previous = $this->avatar;
        $this->avatar = MediaStorage::commit(
            $tempPath,
            'mobile_public',
            'avatars',
        );
        MediaStorage::delete($previous);
    }

    public function avatarCleared(): void
    {
        MediaStorage::delete($this->avatar);
        $this->avatar = null;
    }

    public function render(): View
    {
        return view('native.profile');
    }
}
<firstlight:media
    mode="image"
    label="Profile photo"
    helper="Square crop required"
    aspect="1:1"
    disk="mobile_public"
    directory="avatars"
    native:model="avatar"
    @change="avatarChosen"
    @clear="avatarCleared"
/>

Props

API Accepted type Purpose
mode image | document Required. Image allows camera/library and crop; document uses the system file picker only.
value MediaValue | null Committed value. Empty is null.
native:model a ?MediaValue component property Synchronises the accepted value. native:model.live is also accepted.
label string Visible field label.
helper string Supporting text below the control.
error string Validation text; shown instead of helper when non-empty.
required bool Marks the field as required visually and for assistive technology.
disabled bool Prevents pick and clear while retaining the accepted value.
disk string Laravel Storage disk. Defaults to mobile_public.
directory string Directory prefix under the disk. Defaults to media.
aspect ratio string such as 1:1 Image only. Implies required crop to that aspect.
crop optional | required Image only. Freeform crop when aspect is omitted.
a11y-label string Explicit accessible name when needed.
a11y-hint string Additional VoiceOver and TalkBack guidance.
class string External EDGE layout utilities.

Document mode rejects crop and aspect. Unsupported attributes such as multiple and video are rejected before publication.

Crop rules

  • Neither crop nor aspect → store without a crop sheet.
  • aspect → required crop locked to that aspect.
  • crop="optional" without aspect → freeform crop; Skip is allowed.
  • crop="required" without aspect → freeform crop; Confirm is required.

Confirm stores the region the crop sheet shows: the largest rectangle at the locked aspect (or the photo's own aspect when freeform), narrowed by zoom and placed where the person moved it. The region starts centred. Drag the photo to move it and pinch or use the zoom buttons to zoom from 1× to 4×. The region never leaves the photo: it stops at the edges, and zooming out pulls it back inside. With a locked aspect that differs from the photo's, the region can move along the longer side even at 1×; freeform at 1× is the whole photo. A new pick starts centred again. Skip stores the whole photo. Image mode always stores a JPEG.

Clear, Skip, Confirm, Cancel, Crop, zoom, crop-move, and source-chooser copy are package chrome; see Localize chrome.

Events

@change receives the temporary absolute file path after the user confirms a pick (and crop when required). Commit with MediaStorage::commit before assigning MediaValue.

@clear fires when the user clears a present value. Delete the previous object with MediaStorage::delete and set the model to null.

MediaValue

new MediaValue(
    disk: 'mobile_public',
    path: 'avatars/a.jpg',
    mime: 'image/jpeg',
    size: 1200,
    width: 100,
    height: 100,
);

In image mode the field shows a thumbnail of the stored file. It is decoded from the file's local path, so it appears only for disks with a local driver, such as the default mobile_public; other disks show a photo icon. Document mode always shows a document icon and the file name.

To show the image elsewhere in a native view, use the file's local path, Storage::disk($value->disk)->path($value->path). The disk's url() is served only to WebViews.

Validation

Media participates in Validate fields. Public MediaValue properties are adapted to Laravel UploadedFile values so stock image, file, mimes, max, and dimensions rules work. null fails required. Authored error wins over the MessageBag binder.

Accessibility

The field exposes a labelled control with helper or error text. Crop sheets include Confirm, Cancel, optional Skip, and explicit zoom in/out controls meeting 44-point (iOS) and 48-dp (Android) minima. Zoom is not pinch-only, and moving the region is not drag-only: the crop region is one labelled element with Move crop left, right, up, and down actions (VoiceOver actions rotor, TalkBack actions menu). Each action moves a tenth of the region.

Platform behaviour

iOS offers Photo Library (PhotosPicker) and Camera in a confirmation dialog, uses fileImporter for documents, and shows a Firstlight SwiftUI crop sheet. Android offers Photo Library (the system photo picker) and Camera (the system camera app) in a Material bottom sheet, uses the system document picker, and shows a Firstlight Compose crop dialog. Picked files are copied to the app's temporary directory before @change receives the path. Geometry and system pickers stay platform-native; crop semantics stay shared.

Host application setup

On iOS, image mode's Camera option requires a camera usage description, or iOS terminates the app when the camera opens. Firstlight's plugin manifest supplies a generic one ("Used to take a photo for a form field."). Override it in config/nativephp.php with wording specific to your app for App Store review; app entries win over plugin entries:

'permissions' => [
    'NSCameraUsageDescription' => 'Used to take a photo for your profile.',
],

The photo picker and document picker need no permission on either platform. Android needs no manifest change: the system camera app writes through the FileProvider that NativePHP's Android host already declares. If your app adds the CAMERA permission for another feature, grant it before offering Camera.

Compatibility

Media supports the versions listed in the current compatibility reference. The host application must compile both Firstlight-owned native renderers declared by the package manifest.

Screenshots

Platform Light Dark
iOS Media on iOS in light mode Media on iOS in dark mode
Android Media on Android in light mode Media on Android in dark mode

Served from firstlightui/nativephp on GitHub.