Skip to content
Brok UI

Loading…

No results

Cron Schedule

Open source

A human-readable visualiser for a 5-field cron expression — a plain-English summary, a per-field breakdown and upcoming run times.

Version
v1.0.5
Stability
stable
License
MIT
Related
Date Picker
Calendar
API Reference Table

Preview

Daily standup reminder
0 9 * * 1-5

At 09:00 on Mon, Tue, Wed, Thu, Fri

Minute
0
:00
Hour
9
9 AM
Day of month
*
every day
Month
*
every month
Day of week
1-5
Mon, Tue, Wed, Thu, Fri
Next runs
previews.components.cron-schedule.default.blade.php Blade
<div class="w-full max-w-md">
    <x-ui.cron-schedule
        expression="0 9 * * 1-5"
        title="Daily standup reminder"
        :show-next-runs="3"
        reference-date="2026-06-13 08:00:00"
    />
</div>

Installation

terminal
php artisan ui:add cron-schedule

Registry contract

php artisan ui:add cron-schedule 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/cron-schedule.blade.php
Registry dependencies
None — installs on its own.
Packages
composer: jml/brok:^0.2

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.

cron-schedule.md
# Brok UI: Cron Schedule (`cron-schedule`)

A human-readable visualiser for a 5-field cron expression — a plain-English summary, a per-field breakdown and upcoming run times.

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

## Install

```bash
php artisan ui:add cron-schedule
```

## Usage

```blade
<div class="w-full max-w-md">
    <x-ui.cron-schedule
        expression="0 9 * * 1-5"
        title="Daily standup reminder"
        :show-next-runs="3"
        reference-date="2026-06-13 08:00:00"
    />
</div>
```

## Props

- `expression` (string, default `* * * * *`)
- `title` (mixed|null, default `null`)
- `showNextRuns` (int, default `0`)
- `referenceDate` (mixed|null, default `null`)

## Use when

- Use for exact technical detail, code readability, diagnostics, and copyable reference material.
- Explaining what a cron expression actually does, next to the expression itself.
- Confirming a schedule before it is saved, by showing the upcoming run times.

## Avoid when

- Do not surface developer-specific patterns in end-user flows unless the task genuinely requires them.
- Editing a schedule; this is a read-only explainer.
- Parsing a non-standard expression: only the 5-field form is understood.

## Anti-patterns

- Exposing developer tooling in end-user flows without a task need

## Rules

- Use the `<brok:cron-schedule>` tag (or `<x-ui.cron-schedule>`) 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/cron-schedule
- Registry JSON (files, props, contract): https://brokui.dev/r/open/cron-schedule.json

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

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
expression string * * * * *
title mixed | null null
showNextRuns int 0
referenceDate mixed | null null

Slots

  • default —

Data slots

Stable hooks for CSS overrides and browser tests.

cron-schedule

Behavior

  • The expression is summarised in plain English, then broken down field by field.
  • showNextRuns lists that many upcoming runs, computed from referenceDate when one is given so the output is deterministic.
  • An expression that cannot be parsed is reported as invalid rather than guessed at.
  • Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.

Guidance

Developer and documentation tool

Expose exact technical or diagnostic information.

Use when

  • Use for exact technical detail, code readability, diagnostics, and copyable reference material.
  • Explaining what a cron expression actually does, next to the expression itself.
  • Confirming a schedule before it is saved, by showing the upcoming run times.

Avoid when

  • Do not surface developer-specific patterns in end-user flows unless the task genuinely requires them.
  • Editing a schedule; this is a read-only explainer.
  • Parsing a non-standard expression: only the 5-field form is understood.

Use instead

  • Plain-language product UI for nontechnical users

Anti-patterns

  • Exposing developer tooling in end-user flows without a task need
Anatomy
root
Theming hooks
cron-schedule

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
No component-owned keyboard interaction; native element behavior applies.
Focus
none
  • The plain-English summary is text, so the schedule is readable without decoding the five fields.
  • Field labels are associated with their values, and decorative separators are aria-hidden.
  • 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

Safe

Livewire can update this component through forwarded wire:* attributes.

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/cron-schedule.blade.php Blade
@props([
    'expression' => '* * * * *',
    'title' => null,
    'showNextRuns' => 0,
    'referenceDate' => null,
])

@php
    use Illuminate\Support\Carbon;

    $showNextRuns = max(0, (int) $showNextRuns);

    // ── Parse the five fields into materialised value sets ───────────────────
    // Each field becomes a sorted list of allowed integers plus an "every" flag
    // (the field was "*"). Supports lists (a,b), ranges (a-b) and steps (*/n,
    // a-b/n). Out-of-range or unparseable tokens are ignored.
    $parseField = function (string $raw, int $lo, int $hi): array {
        $raw = trim($raw);
        $isStar = $raw === '*' || $raw === '?';
        $set = [];
        foreach (explode(',', $raw) as $part) {
            $step = 1;
            if (str_contains($part, '/')) {
                [$part, $stepRaw] = explode('/', $part, 2);
                $step = max(1, (int) $stepRaw);
            }
            if ($part === '*' || $part === '?' || $part === '') {
                $from = $lo;
                $to = $hi;
            } elseif (str_contains($part, '-')) {
                [$a, $b] = array_pad(explode('-', $part, 2), 2, $lo);
                $from = (int) $a;
                $to = (int) $b;
            } else {
                $from = $to = (int) $part;
            }
            for ($v = $from; $v <= $to; $v += $step) {
                if ($v >= $lo && $v <= $hi) {
                    $set[$v] = true;
                }
            }
        }
        $set = array_keys($set);
        sort($set);

        return ['set' => $set ?: range($lo, $hi), 'star' => $isStar];
    };

    $fields = preg_split('/\s+/', trim((string) $expression));
    $valid = count($fields) === 5;
    if (! $valid) {
        $fields = ['*', '*', '*', '*', '*'];
    }

    $minute = $parseField($fields[0], 0, 59);
    $hour = $parseField($fields[1], 0, 23);
    $dom = $parseField($fields[2], 1, 31);
    $month = $parseField($fields[3], 1, 12);
    // Normalise 7 → 0 (both mean Sunday) in the weekday field.
    $dowRaw = str_replace('7', '0', $fields[4]);
    $dow = $parseField($dowRaw, 0, 6);

    $dowNames = [0 => __('Sun'), 1 => __('Mon'), 2 => __('Tue'), 3 => __('Wed'), 4 => __('Thu'), 5 => __('Fri'), 6 => __('Sat')];
    $monthNames = [1 => __('Jan'), 2 => __('Feb'), 3 => __('Mar'), 4 => __('Apr'), 5 => __('May'), 6 => __('Jun'), 7 => __('Jul'), 8 => __('Aug'), 9 => __('Sep'), 10 => __('Oct'), 11 => __('Nov'), 12 => __('Dec')];
    $hour12 = function (int $h): string {
        $suffix = $h < 12 ? 'AM' : 'PM';
        $base = $h % 12;
        if ($base === 0) {
            $base = 12;
        }

        return $base.' '.$suffix;
    };

    // ── Per-field human interpretation (the breakdown rows) ──────────────────
    $rows = [
        ['name' => __('Minute'), 'raw' => $fields[0], 'human' => $minute['star'] ? __('every minute') : implode(', ', array_map(fn ($m) => sprintf(':%02d', $m), $minute['set']))],
        ['name' => __('Hour'), 'raw' => $fields[1], 'human' => $hour['star'] ? __('every hour') : implode(', ', array_map($hour12, $hour['set']))],
        ['name' => __('Day of month'), 'raw' => $fields[2], 'human' => $dom['star'] ? __('every day') : implode(', ', $dom['set'])],
        ['name' => __('Month'), 'raw' => $fields[3], 'human' => $month['star'] ? __('every month') : implode(', ', array_map(fn ($mo) => $monthNames[$mo], $month['set']))],
        ['name' => __('Day of week'), 'raw' => $fields[4], 'human' => $dow['star'] ? __('every weekday') : implode(', ', array_map(fn ($d) => $dowNames[$d], $dow['set']))],
    ];

    // ── Plain-English summary (best-effort for common shapes) ────────────────
    if ($minute['star'] && $hour['star']) {
        $timePart = __('Every minute');
    } elseif (! $minute['star'] && count($minute['set']) === 1 && ! $hour['star'] && count($hour['set']) === 1) {
        $timePart = __('At').' '.sprintf('%02d:%02d', $hour['set'][0], $minute['set'][0]);
    } elseif (! $minute['star'] && count($minute['set']) === 1 && $hour['star']) {
        $timePart = __('At ::minute past every hour', ['minute' => sprintf('%02d', $minute['set'][0])]);
    } else {
        $timePart = __('At multiple times');
    }

    if (! $dow['star']) {
        $dayPart = __('on').' '.implode(', ', array_map(fn ($d) => $dowNames[$d], $dow['set']));
    } elseif (! $dom['star']) {
        $dayPart = __('on day').' '.implode(', ', $dom['set']).' '.__('of the month');
    } else {
        $dayPart = __('every day');
    }

    $monthPart = $month['star'] ? '' : ' '.__('in').' '.implode(', ', array_map(fn ($mo) => $monthNames[$mo], $month['set']));
    $summary = trim($timePart.' '.$dayPart.$monthPart);

    // ── Upcoming runs (day-stepping; bounded) ────────────────────────────────
    $nextRuns = [];
    if ($valid && $showNextRuns > 0) {
        $ref = $referenceDate ? Carbon::parse($referenceDate) : Carbon::now();
        $dayMatches = function (Carbon $day) use ($dom, $month, $dow): bool {
            if (! in_array($day->month, $month['set'], true)) {
                return false;
            }
            $domOk = in_array($day->day, $dom['set'], true);
            $dowOk = in_array($day->dayOfWeek, $dow['set'], true);
            if (! $dom['star'] && ! $dow['star']) {
                return $domOk || $dowOk;   // cron "OR" quirk when both are restricted
            }

            return $domOk && $dowOk;
        };

        $day = $ref->copy()->startOfDay();
        $guard = 0;
        while (count($nextRuns) < $showNextRuns && $guard < 1500) {
            if ($dayMatches($day)) {
                foreach ($hour['set'] as $h) {
                    foreach ($minute['set'] as $mi) {
                        $candidate = $day->copy()->setTime($h, $mi);
                        if ($candidate->gt($ref)) {
                            $nextRuns[] = $candidate;
                            if (count($nextRuns) >= $showNextRuns) {
                                break 2;
                            }
                        }
                    }
                }
            }
            $day->addDay();
            $guard++;
        }
    }
@endphp

<div
    data-slot="cron-schedule"
    {{ $attributes->merge(['class' => 'overflow-hidden rounded-lg border border-border bg-card text-foreground']) }}
>
    <div class="border-b border-border px-4 py-3">
        @if ($title !== null)
            <div class="mb-1 text-sm font-semibold text-foreground">{{ $title }}</div>
        @endif
        <div class="flex flex-wrap items-center gap-2 font-mono text-sm" aria-label="{{ __('Cron expression') }}: {{ $expression }}">
            @foreach ($fields as $field)
                <span class="rounded bg-muted px-2 py-0.5 text-foreground">{{ $field }}</span>
            @endforeach
        </div>
        <p class="mt-2 text-sm text-muted-foreground">{{ $summary }}</p>
    </div>

    <dl class="divide-y divide-border">
        @foreach ($rows as $row)
            <div class="grid grid-cols-[8rem_4rem_1fr] items-center gap-2 px-4 py-2 text-sm">
                <dt class="text-muted-foreground">{{ $row['name'] }}</dt>
                <dd class="font-mono text-foreground">{{ $row['raw'] }}</dd>
                <dd class="text-muted-foreground">{{ $row['human'] }}</dd>
            </div>
        @endforeach
    </dl>

    @if ($nextRuns !== [])
        <div class="border-t border-border px-4 py-3">
            <div class="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">{{ __('Next runs') }}</div>
            <ul class="space-y-1">
                @foreach ($nextRuns as $run)
                    <li class="flex items-center gap-2 text-sm">
                        <svg class="size-4 shrink-0 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></svg>
                        <time datetime="{{ $run->toIso8601String() }}" class="font-mono text-foreground">{{ $run->format('D, M j Y, H:i') }}</time>
                    </li>
                @endforeach
            </ul>
        </div>
    @endif
</div>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.0.5
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