Skip to content

Calendar

The calendar package renders one Calendar component with switchable views over a single event source: a month grid with multi-day event spanning, per-day overflow, and keyboard navigation, week and day time grids with hour-precise event blocks, and a resource-planning timeline (“Plantafel”) of resource rows against a day-granular date axis. Every view supports drag-to-reschedule and shares the same adapter, endpoint, and client-side event cache — switching views reuses everything already loaded.

Terminal window
composer require lattice-php/calendar

That is the whole integration: the package ships its React renderer as source, and the lattice() Vite plugin compiles it into your app’s bundle via virtual:lattice/plugins (see Component packages). The PHP classes are picked up by Lattice’s discovery and TypeScript generation automatically. No-build apps use the precompiled module the package also ships: run php artisan lattice:assets after installation.

Implement one CalendarAdapter for the host application. Its single required method returns the events overlapping a requested [$from, $until) window ($until exclusive):

use Carbon\CarbonImmutable;
use Lattice\Calendar\AsCalendar;
use Lattice\Calendar\CalendarAdapter;
use Lattice\Calendar\CalendarDefinition;
use Lattice\Calendar\CalendarEvent;
final class CompanyCalendarAdapter implements CalendarAdapter
{
public function events(CarbonImmutable $from, CarbonImmutable $until): iterable
{
return Meeting::query()
->where('starts_at', '<', $until)
->where('ends_at', '>', $from)
->get()
->map(fn (Meeting $meeting): CalendarEvent => CalendarEvent::make(
(string) $meeting->id,
$meeting->starts_at->format('Y-m-d\TH:i:s'),
$meeting->ends_at->format('Y-m-d\TH:i:s'),
)->label($meeting->title)->color($meeting->color));
}
}
#[AsCalendar('company')]
final class CompanyCalendar extends CalendarDefinition
{
public function adapter(): CalendarAdapter
{
return app(CompanyCalendarAdapter::class);
}
}

The adapter is the only application-facing contract. It owns authorization rules beyond the definition’s authorize(), validation, translations, persistence, and transactions — the calendar package works with any storage model.

Render it with Calendar::use():

use Lattice\Calendar\CalendarView;
use Lattice\Calendar\Components\Calendar;
Calendar::use(CompanyCalendar::class)
->views([CalendarView::Month, CalendarView::Week, CalendarView::Day, CalendarView::Timeline])
->defaultView(CalendarView::Month)
->date('2026-08-01')
->days(90);

->views() picks the enabled views (default: month only); with more than one, the client shows a view switcher. ->defaultView() chooses the initially active view and falls back to the first enabled one. ->date() anchors every view — the month view opens on that date’s month, the week and day views on its week and day, and the timeline window starts on it (default: today). ->days() sets the timeline’s initially rendered window (default 90).

CalendarEvent::make($id, $start, $end) uses half-open bounds — $end is exclusive, the same convention as the events() window. The input format decides the event kind:

  • Plain Y-m-d strings make an all-day event; a one-day event has $end one day after $start.
  • A datetime string or DateTimeInterface makes a timed event, normalized to Y-m-d\TH:i:s. ->allDay() overrides the inference either way.

->label() sets the visible text and ->color() accepts any Lattice color (see Enums reference); uncolored events render in the theme’s primary tone. ->context([...]) attaches data that is merged into the event action’s payload on click, and ->resource($resourceId) binds the event to a timeline row (see below).

The timeline view and rescheduling are opt-in adapter capabilities:

use Illuminate\Http\Request;
use Lattice\Calendar\ProvidesCalendarResources;
use Lattice\Calendar\ReschedulesCalendarEvents;
use Lattice\Calendar\ResourceGroup;
final class ProjectPlanAdapter implements CalendarAdapter, ProvidesCalendarResources, ReschedulesCalendarEvents
{
public function groups(): array
{
return [
ResourceGroup::make('projects', 'Projects')->resources([
['id' => 'website-relaunch', 'label' => 'Website Relaunch'],
]),
ResourceGroup::make('employees', 'Employees')->resources(
fn (): array => Employee::query()->select('id', 'name as label')->get()->toArray(),
),
];
}
public function reschedule(Request $request): CalendarEvent
{
// validate id/resourceId/start/end, persist, return the updated event
}
// ... events()
}
  • ProvidesCalendarResources supplies the timeline’s resource rows and is required as soon as CalendarView::Timeline is enabled — serialization throws a LogicException otherwise. ResourceGroup::make($key, $label)->resources() takes either an inline list of ['id' => ..., 'label' => ...] rows or a closure returning the same shape, evaluated once per render. Timeline lanes lay out the events bound to a resource via ->resource(); the month view shows every event regardless of binding.
  • ReschedulesCalendarEvents enables drag-to-reschedule in every view. Without it the calendar renders read-only and its PATCH endpoint responds 405.

Clicks are wired to regular Lattice actions:

Calendar::use(CompanyCalendar::class)
->eventAction(ShowEventAction::class)
->dayAction(PlanDayAction::class);

An event click posts { eventId, ...context } and a day click posts { date } to the sealed action endpoint; the returned effects (toast, modal, redirect, …) dispatch as usual. Events and day cells are only interactive when the matching action is configured.

The month grid follows the viewer’s locale for week start and weekday labels, spans multi-day events across day cells with stable per-week lanes, and marks today. Three event lanes are visible per week; further events collapse into a per-day “+N more” button that opens a popover listing everything on that day.

The grid is keyboard-navigable: arrow keys move the focused day (crossing a month boundary navigates), PageUp/PageDown jump a month, and Enter fires the day action. Prev/next/today controls and the month title round out the header; navigating prefetches the adjacent months’ grids.

With ReschedulesCalendarEvents, dragging an event chip onto another day cell moves the event by whole days: the duration stays intact, all-day events shift their date bounds, and timed events keep their wall-clock times while only the dates move. Dragging any weekly segment of a multi-week event moves the whole event, anchored to the day the drag started on. Keyboard users can focus a chip and press Control+Shift with an arrow key: left/right moves by one day, up/down by one week. The optimistic update, rollback, and announced error handling match the timeline’s rescheduling.

The week and day views share one time grid: a scrollable 24-hour axis (opening at 07:00) with hour-precise event blocks, an all-day row on top, and a now indicator on today’s column. The week view follows the viewer’s locale for its first weekday; the day view is the same grid with a single column. All-day events and events spanning several calendar days render as chips in the all-day row; single-day timed events become blocks positioned by their wall-clock times, and overlapping blocks split the column side by side.

With ReschedulesCalendarEvents, dragging a block moves the event to the day and time under the cursor, snapped to 15 minutes with the duration intact; a dashed preview shows the snapped target while dragging. Dragging the handle at a block’s lower edge changes the event’s end time instead, down to a 15-minute minimum. All-day chips move by whole days, exactly like in the month view. Keyboard users can focus a block and press

Control+Shift with an arrow key — left/right moves by one day, up/down by 15 minutes — or focus the resize handle and press an arrow key alone to adjust the end time. The optimistic update, rollback, and announced error handling match the timeline’s rescheduling.

The timeline draws each resource’s events as bars against a sticky month/calendar-week/day header, with weekend columns striped and a marker line for today. Toolbar controls: / step the visible window by a week, Today recenters on it, and /+ zoom the day column width (10–64px) without changing how many days are loaded. Resource groups collapse independently via their chevron. Overlapping same-resource events stack into lanes automatically, growing the row — events() can simply return whatever overlaps.

Timeline lane layout is day-granular: a timed event occupies the calendar days it touches.

Drag an event to a resource row to change its resource and dates in one operation, or drag either edge to resize. Keyboard users can focus an event and press Control+Shift with an arrow key: left/right moves by one day and up/down moves to the adjacent resource. The client updates immediately while the adapter runs, then either applies the returned event or rolls back and raises the adapter’s translated error message as a toast. Each event represents one resource assignment — if one logical event belongs to several resources, return one event per assignment with a stable, unique ID.

Navigating past the initially loaded window fetches the missing range from the definition’s endpoint, merging it into the shared client-side cache rather than re-fetching already-seen days — the same signed-reference pattern Lattice tables and trees use. The package registers its own route (there is no core routes seam), following Lattice’s group conventions: config('lattice.calendars.middleware', ['web', 'auth']) and config('lattice.calendars.endpoint', 'lattice/calendars/{calendar}').

GET lattice/calendars/{calendar}?from=Y-m-d&to=Y-m-d (to exclusive) re-resolves the definition from its sealed reference and calls adapter()->events($from, $to) again — authorize() on the definition gates both the initial render and every window fetch, mirroring trees and the signing machinery behind them.

PATCH lattice/calendars/{calendar} accepts id, resourceId, start, and end, then calls reschedule($request) on adapters implementing ReschedulesCalendarEvents. resourceId is null for resource-less events, and start/end arrive in the event’s own representation — Y-m-d bounds for all-day events, wall-clock datetimes for timed ones — so each adapter validates exactly the shapes it accepts. The success response contains the updated event; failed Laravel validation responses are forwarded without replacing their translated message.

The component’s strings ship with inline English defaults. With laravel-i18next enabled, the plugin’s calendar namespace is loaded automatically and serves the bundled en/de translations (override them like any Laravel package translation — see Internationalization).

The breakdown below measures the package’s built plugin — React and the framework runtime are external, so this is exactly what the calendar package adds on top of an app that already ships Lattice. It is regenerated on every docs build.

15.0 KB gzipped · 64.0 KB raw JavaScript

DependencyRawGzipShare
@lattice-php/calendar56.4 KB14.2 KB94.9%
Bundler runtime7.7 KB0.8 KB5.1%

Emitted JavaScript files

FileRawGzip
plugin.js64.0 KB15.0 KB

Open the full interactive treemap ↗ · Generated by Sonda 0.14.0 on Sat, 22 Aug 2026 17:50:38 GMT.