phpinnacle/tempo provides date and time utilities together with custom Filament pickers, date-range filters, and an extensible calendar widget.
composer require phpinnacle/tempo
php artisan filament:assetsuse PHPinnacle\Tempo\Forms\CronExpression;
use PHPinnacle\Tempo\Forms\DatePicker;
use PHPinnacle\Tempo\Forms\DateRangePicker;
use PHPinnacle\Tempo\Forms\DateTimePicker;
use PHPinnacle\Tempo\Forms\Duration;
use PHPinnacle\Tempo\Forms\TimePicker;
DatePicker::make('published_on');
DateTimePicker::make('published_at');
DateRangePicker::make('period');
TimePicker::make('starts_at');
CronExpression::make('schedule')
->showDescription()
->showDayOfWeek(false)
->gridColumns(['month' => 4])
->defaultMode('expression')
->presets(['Late weekdays' => '0 23 ? * MON-FRI'])
->required();
Duration::make('timeout')->required();
Duration::make('retention')->storeAs('string');
Duration::make('short_timer')
->units(['minute', 'second'])
->quickValues(['minute' => [0, 1, 5, 15, 30, 60]]);The date and time pickers support custom formats, locale, timezone, minimum and maximum values, and optional automatic closing. The package registers its field JavaScript and CSS through Filament assets.
Duration has visual day, hour, minute, and second controls plus one masked text input. Its translated unit suffixes stay fixed within the input; Space and the Left and Right arrow keys move between numbers. When the text input loses focus, its value is redistributed across the enabled units. Months and years are not supported because their length depends on the calendar date. The field stores a non-negative integer number of seconds by default. Use storeAs('string') to store a duration string instead; storeAs('seconds') restores the default. units() limits both editors to the listed units; for example, minutes and seconds can represent 1h 30m as 90m. Values in seconds must be divisible by the smallest enabled unit. The text editor has a clear button and writes normalized values; invalid numbers remain available for validation. storeAs('string') always stores canonical d/h/m/s suffixes regardless of the display language. An empty field stores null, while zero stores 0 or a string with the smallest enabled unit. quickValues() overrides the number shortcuts separately for any unit, leaving unspecified units at their defaults. presets() replaces the built-in quick durations with a label-to-duration map; pass an empty array to hide the preset menu. All options accept Filament callbacks.
CronExpression stores a five-field cron string. Call showDescription() to display a localized plain-language description below the editor; it is hidden by default. It offers presets and visual controls for intervals and selected values in each field. In the Every grid, interval 1 writes *; larger intervals write */N. Consecutive or evenly spaced selections are written as ranges, with a step when needed. The day-of-month grid includes Last, which writes L for the last day of each month; L must stand alone in that field. Use the expression tab for combinations the visual controls cannot represent. Validation accepts numeric values, three-letter English month and weekday names, and ? as an unspecified day of the month or week (but not both).
gridColumns() sets the number of value-grid columns per cron part (minute, hour, day, month, or weekday). Unspecified parts keep their defaults: six for minutes, four for hours and days of the month, three for months, and two for weekdays.
defaultMode() accepts visual (the default) or expression for the initially selected editor. presets() replaces the built-in list with a label-to-expression map; pass an empty array to hide the preset menu.
showDayOfWeek(false) hides the weekday controls in Visual mode. The fifth cron field remains available in Expression mode and keeps its value when other parts are edited visually. Weekday controls are shown by default. All cron field options (gridColumns(), showDescription(), showDayOfWeek(), defaultMode(), and presets()) also accept Filament callbacks. For example, fn (Get $get) => (bool) $get('show_day_of_week') can read a live checkbox elsewhere in the form.
use PHPinnacle\Tempo\Filters\DateRangeFilter;
use PHPinnacle\Tempo\Filters\DateTimeRangeFilter;
use PHPinnacle\Tempo\Filters\TimeRangeFilter;
DateRangeFilter::createdAt();
DateRangeFilter::updatedAt();
DateRangeFilter::make('published_at');
DateTimeRangeFilter::make('published_window')
->column('published_at');
TimeRangeFilter::make('starts_window')
->column('starts_at');The filter name identifies its form state. By default it is also used as the database column; use column() when they differ.
use PHPinnacle\Tempo\Clock;
Clock::now();
Clock::date();
Clock::unix();
Clock::year();CalendarWidget delegates event loading to event sources returned by the widget subclass. Each source is also exposed as a filterable event category. The default record action opens the event URL:
use PHPinnacle\Tempo\Contracts\EventSource;
use PHPinnacle\Tempo\Widgets\CalendarWidget;
final class AppointmentsCalendarWidget extends CalendarWidget
{
/** @return list<EventSource> */
public function getEventSources(): array
{
return [
new AppointmentsEventSource(),
new DeadlinesEventSource(),
];
}
}An event source provides its stable filter key, visible label, icon, color, and events for the requested range:
use Filament\Support\Colors\Color;
use Filament\Support\Icons\Heroicon;
use PHPinnacle\Tempo\Calendar\CalendarEvent;
use PHPinnacle\Tempo\Calendar\CalendarRange;
use PHPinnacle\Tempo\Contracts\EventSource;
final class AppointmentsEventSource implements EventSource
{
public function getKey(): string
{
return 'appointments';
}
public function getLabel(): string
{
return 'Appointments';
}
public function getIcon(): Heroicon
{
return Heroicon::CalendarDays;
}
public function getColor(): array
{
return Color::Blue;
}
/** @return list<CalendarEvent> */
public function getEvents(CalendarRange $range, int $limit): array
{
return [];
}
}Register the configured widget through Filament as usual:
AppointmentsCalendarWidget::make();Tempo requests only the currently selected sources, merges and sorts their events, and applies the global phpinnacle-tempo.calendar.event_limit. It mounts recordAction() when an event has a URL or its viewable flag is enabled. Override recordAction() with CalendarRecordAction when clicks should open a modal or perform another operation. The clicked CalendarEvent is injected into a callback parameter named $event. Event details cross the browser; use the event ID to reload and authorize domain records before reading or changing protected data.
Event sources may expand RFC 5545 recurrence rules without coupling Tempo to their storage model:
use Carbon\CarbonImmutable;
use PHPinnacle\Tempo\Calendar\CalendarRange;
use PHPinnacle\Tempo\Calendar\RecurrenceExpander;
use PHPinnacle\Tempo\Calendar\RecurringEvent;
$range = CalendarRange::create('2026-07-01', '2026-08-01')
?? throw new InvalidArgumentException('Invalid calendar range.');
$occurrences = app(RecurrenceExpander::class)->expand(
new RecurringEvent(
rule: 'FREQ=YEARLY',
start: CarbonImmutable::parse('1990-07-15'),
),
$range,
limit: 100,
);The expander preserves event duration, includes occurrences overlapping the beginning of the requested range, and returns at most the requested limit. Invalid recurrence rules retain the underlying RRULE exception semantics.
The MIT License (MIT). See License File.