Skip to content
Brok UI

Loading…

No results

Status History

Open source

A bounded ordered status history with stable state keys, semantic events, absolute timestamps, and explicit empty behavior.

Version
v1.1.2
Stability
stable
License
MIT
Related
Timeline
Relative Time
Badge

Preview

  1. Request created

    by Avery Stone Finance

    Completed
  2. Manager review

    by Priya Nair Finance

    Completed

    Approved without changes.

  3. Stock reserved

    by Warehouse Warehouse

    Completed

    All 12 items were reserved in the Rotterdam warehouse. Two items came from the overflow shelf and were repacked; the pick list was regenerated so the packer sees the new locations, and the carrier booking was moved to the 14:00 pick-up slot to match.

  4. Finance review

    Finance

    Current

    Waiting for one reviewer.

  5. Campaign announcement

    Marketing

    Pending
  6. Payment

    Finance

    Pending
previews.components.status-history.default.blade.php Blade
@php
    $events = [
        ['state' => 'completed', 'label' => __('Request created'), 'at' => now()->subDays(2), 'by' => 'Avery Stone', 'category' => __('Finance')],
        ['state' => 'completed', 'label' => __('Manager review'), 'description' => __('Approved without changes.'), 'at' => now()->subDay(), 'by' => 'Priya Nair', 'category' => __('Finance')],
        ['state' => 'completed', 'label' => __('Stock reserved'), 'description' => __('All 12 items were reserved in the Rotterdam warehouse. Two items came from the overflow shelf and were repacked; the pick list was regenerated so the packer sees the new locations, and the carrier booking was moved to the 14:00 pick-up slot to match.'), 'at' => now()->subHours(20), 'by' => 'Warehouse', 'category' => __('Warehouse')],
        ['state' => 'current', 'label' => __('Finance review'), 'description' => __('Waiting for one reviewer.'), 'at' => now(), 'category' => __('Finance')],
        ['state' => 'pending', 'label' => __('Campaign announcement'), 'category' => __('Marketing')],
        ['state' => 'pending', 'label' => __('Payment'), 'category' => __('Finance')],
    ];
@endphp
<div class="w-full max-w-lg">
    <x-ui.status-history :events="$events" />
</div>

Installation

terminal
php artisan ui:add status-history

Registry contract

php artisan ui:add status-history writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/status-history.blade.php
Registry dependencies
badge relative-time
Packages
composer: jml/brok:^0.2
npm: alpinejs

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

status-history.md
# Brok UI: Status History (`status-history`)

A bounded ordered status history with stable state keys, semantic events, absolute timestamps, and explicit empty behavior.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add status-history
```

## Usage

```blade
@php
    $events = [
        ['state' => 'completed', 'label' => __('Request created'), 'at' => now()->subDays(2), 'by' => 'Avery Stone', 'category' => __('Finance')],
        ['state' => 'completed', 'label' => __('Manager review'), 'description' => __('Approved without changes.'), 'at' => now()->subDay(), 'by' => 'Priya Nair', 'category' => __('Finance')],
        ['state' => 'completed', 'label' => __('Stock reserved'), 'description' => __('All 12 items were reserved in the Rotterdam warehouse. Two items came from the overflow shelf and were repacked; the pick list was regenerated so the packer sees the new locations, and the carrier booking was moved to the 14:00 pick-up slot to match.'), 'at' => now()->subHours(20), 'by' => 'Warehouse', 'category' => __('Warehouse')],
        ['state' => 'current', 'label' => __('Finance review'), 'description' => __('Waiting for one reviewer.'), 'at' => now(), 'category' => __('Finance')],
        ['state' => 'pending', 'label' => __('Campaign announcement'), 'category' => __('Marketing')],
        ['state' => 'pending', 'label' => __('Payment'), 'category' => __('Finance')],
    ];
@endphp
<div class="w-full max-w-lg">
    <x-ui.status-history :events="$events" />
</div>
```

## Props

- `events` (list<StatusEvent>, default `[]`) — Up to 100 events with stable state, label, description, and at values.
- `label` (string, default `Status history`) — Accessible section label.
- `emptyLabel` (string, default `No status changes`) — Localized empty label.
- `filterable` (boolean, default `true`) — Filter chips for the categories the events carry (shown when there are two or more).
- `clamp` (int, default `2`) — Lines a long description shows before it is folded behind Show more.

## Use when

- Use to summarize, sequence, or present data so users can scan it quickly.
- Show the ordered lifecycle of a server-owned record or workflow.

## Avoid when

- Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
- Use timeline for decorative or free-form chronological content.

## Anti-patterns

- Adding display ornament without informational value

## Rules

- Use the `<brok:status-history>` tag (or `<x-ui.status-history>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/status-history
- Registry JSON (files, props, contract): https://brokui.dev/r/open/status-history.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Examples

long-content.blade.php Blade
<div class="max-w-xs">
    <x-ui.status-history :events="[['state' => 'current', 'label' => __('A deliberately long workflow status label'), 'description' => __('Long translated details must wrap without clipping important information.'), 'at' => now()]]" />
</div>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
events list<StatusEvent> [] Up to 100 events with stable state, label, description, and at values.
label string Status history Accessible section label.
emptyLabel string No status changes Localized empty label.
filterable boolean true Filter chips for the categories the events carry (shown when there are two or more).
clamp int 2 Lines a long description shows before it is folded behind Show more.

Slots

Default Blade slot only.

Data slots

Stable hooks for CSS overrides and browser tests.

status-history status-history-filters

Behavior

  • Supports pending, current, completed, failed, and cancelled state keys.
  • Unknown states fail to pending.
  • Declares registry capability flags: a11y, authoredStateFixtures, responsive, rtl, darkMode, localized.

Guidance

Scannable data display

Present data for rapid scanning.

Use when

  • Use to summarize, sequence, or present data so users can scan it quickly.
  • Show the ordered lifecycle of a server-owned record or workflow.

Avoid when

  • Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
  • Use timeline for decorative or free-form chronological content.

Use instead

  • Table for exact comparison
  • Plain text for a single value

Anti-patterns

  • Adding display ornament without informational value
Anatomy
status-history
Theming hooks
Uses semantic borders, text, and badge variants.

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
native
Focus
native
  • Uses a labelled section and ordered list.
  • Each state includes visible text.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<div wire:key="status-history-{{ $record->id }}">
    @php
        $events = [
            ['state' => 'completed', 'label' => __('Request created'), 'at' => now()->subDays(2), 'by' => 'Avery Stone', 'category' => __('Finance')],
            ['state' => 'completed', 'label' => __('Manager review'), 'description' => __('Approved without changes.'), 'at' => now()->subDay(), 'by' => 'Priya Nair', 'category' => __('Finance')],
            ['state' => 'completed', 'label' => __('Stock reserved'), 'description' => __('All 12 items were reserved in the Rotterdam warehouse. Two items came from the overflow shelf and were repacked; the pick list was regenerated so the packer sees the new locations, and the carrier booking was moved to the 14:00 pick-up slot to match.'), 'at' => now()->subHours(20), 'by' => 'Warehouse', 'category' => __('Warehouse')],
            ['state' => 'current', 'label' => __('Finance review'), 'description' => __('Waiting for one reviewer.'), 'at' => now(), 'category' => __('Finance')],
            ['state' => 'pending', 'label' => __('Campaign announcement'), 'category' => __('Marketing')],
            ['state' => 'pending', 'label' => __('Payment'), 'category' => __('Finance')],
        ];
    @endphp
    <div class="w-full max-w-lg">
        <x-ui.status-history :events="$events" />
    </div>
</div>

Source

The exact, editable file ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/status-history.blade.php Blade
@props([
    // [['state' => 'completed|current|failed|cancelled|pending', 'label', 'description', 'at',
    //   'stateLabel' => null, 'category' => 'Finance', 'by' => 'Priya Nair']]
    'events' => [],
    'label' => 'Status history',
    'emptyLabel' => 'No status changes',
    // Filter chips for the categories the events carry (shown when there are two or more).
    'filterable' => true,
    // Lines a description shows before it is folded behind "Show more".
    'clamp' => 2,
])

@php
    $events = array_slice(array_values(array_filter((array) $events, 'is_array')), 0, 100);
    $variants = ['completed' => 'soft-success', 'current' => 'soft-info', 'failed' => 'soft-destructive', 'cancelled' => 'soft-neutral', 'pending' => 'outline'];
    // The dot on the rail: filled in the state's tone, hollow while pending, a
    // pulse ring while current.
    $dots = [
        'completed' => 'bg-success',
        'current' => 'bg-info ring-4 ring-info/20',
        'failed' => 'bg-destructive',
        'cancelled' => 'bg-muted-foreground/60',
        'pending' => 'border-2 border-border bg-background',
    ];
    $categories = array_values(array_unique(array_filter(array_map(static fn (array $e) => $e['category'] ?? null, $events))));
    $showFilters = $filterable && count($categories) >= 2;
    $clamp = max(1, (int) $clamp);
    $clampClass = ['line-clamp-1', 'line-clamp-2', 'line-clamp-3', 'line-clamp-4'][min($clamp, 4) - 1];
@endphp

{{-- Status history: a rail of state changes. The dot sits ON the rail (the
     rail is drawn per item behind the dot), coloured by state; categories fold
     into filter chips; a long description clamps and unfolds. --}}
<section
    data-slot="status-history"
    aria-label="{{ __($label) }}"
    x-data="{ hidden: [], toggle(c) { this.hidden = this.hidden.includes(c) ? this.hidden.filter((x) => x !== c) : [...this.hidden, c]; }, shown(c) { return !c || !this.hidden.includes(c); } }"
    {{ $attributes->merge(['class' => 'min-w-0']) }}
>
    @if ($events === [])
        <p class="text-sm text-muted-foreground">{{ __($emptyLabel) }}</p>
    @else
        @if ($showFilters)
            <div data-slot="status-history-filters" role="group" aria-label="{{ __('Filter by category') }}" class="mb-4 flex flex-wrap items-center gap-2">
                @foreach ($categories as $category)
                    <button
                        type="button"
                        @click="toggle({{ \Illuminate\Support\Js::from($category) }})"
                        :aria-pressed="shown({{ \Illuminate\Support\Js::from($category) }}) ? 'true' : 'false'"
                        class="inline-flex h-7 items-center gap-2 rounded-full border border-border px-3 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground aria-pressed:border-transparent aria-pressed:bg-accent aria-pressed:text-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring motion-reduce:transition-none"
                    >
                        <svg x-show="shown({{ \Illuminate\Support\Js::from($category) }})" class="size-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5L20 7" /></svg>
                        {{ $category }}
                    </button>
                @endforeach
                <button type="button" x-show="hidden.length" x-cloak @click="hidden = []" class="text-xs text-muted-foreground underline-offset-4 hover:text-foreground hover:underline">{{ __('Show all') }}</button>
            </div>
        @endif

        <ol class="relative" role="list">
            @foreach ($events as $event)
                @php
                    $state = array_key_exists($event['state'] ?? '', $variants) ? $event['state'] : 'pending';
                    $category = $event['category'] ?? null;
                    $description = (string) ($event['description'] ?? '');
                    $long = mb_strlen($description) > 140;
                @endphp
                <li
                    class="relative flex min-w-0 gap-4 pb-6 last:pb-0"
                    data-state="{{ $state }}"
                    @if ($category) data-category="{{ $category }}" x-show="shown({{ \Illuminate\Support\Js::from($category) }})" @endif
                    @if ($long) x-data="{ open: false }" @endif
                >
                    {{-- Rail segment and dot: the segment runs from this dot down to the next
                         (none after the last), so the dot is always centred on the line. --}}
                    <span class="relative flex w-3 shrink-0 justify-center" aria-hidden="true">
                        @unless ($loop->last)
                            <span class="absolute top-3 -bottom-1 w-px bg-border" style="inset-inline-start: calc(50% - 0.5px)"></span>
                        @endunless
                        <span class="relative mt-1 size-3 shrink-0 rounded-full {{ $dots[$state] }}"></span>
                    </span>

                    <div class="min-w-0 flex-1">
                        <div class="flex min-w-0 flex-wrap items-start justify-between gap-2">
                            <div class="min-w-0">
                                <p class="break-words text-sm font-medium leading-5 text-foreground">{{ $event['label'] ?? ucfirst($state) }}</p>
                                <p class="mt-px flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
                                    @if (filled($event['at'] ?? null))
                                        <x-ui.relative-time :datetime="$event['at']" mode="datetime" />
                                    @endif
                                    @if (filled($event['by'] ?? null))
                                        <span>{{ __('by :name', ['name' => $event['by']]) }}</span>
                                    @endif
                                    @if ($category)
                                        <span class="inline-flex items-center rounded-full bg-muted px-2 py-px text-2xs font-medium uppercase tracking-wide">{{ $category }}</span>
                                    @endif
                                </p>
                            </div>
                            <x-ui.badge :variant="$variants[$state]" size="sm" class="shrink-0">{{ $event['stateLabel'] ?? ucfirst($state) }}</x-ui.badge>
                        </div>

                        @if ($description !== '')
                            <p class="mt-1 break-words text-sm text-muted-foreground @if ($long) {{ $clampClass }} @endif" @if ($long) :class="open ? '' : '{{ $clampClass }}'" @endif>{{ $description }}</p>
                            @if ($long)
                                <button type="button" @click="open = !open" :aria-expanded="open ? 'true' : 'false'" class="mt-1 text-xs font-medium text-muted-foreground underline-offset-4 hover:text-foreground hover:underline focus-visible:outline-none focus-visible:rounded-sm focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring" x-text="open ? '{{ __('Show less') }}' : '{{ __('Show more') }}'">{{ __('Show more') }}</button>
                            @endif
                        @endif
                    </div>
                </li>
            @endforeach
        </ol>
    @endif
</section>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.1.2
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0