Scheduling
Time and calendar views — a Gantt schedule, a weekly grid and a year overview.
Preview
@props([
'heading' => 'Room schedule',
'subheading' => 'A Gantt-style day across rooms — drag the zoom to scale the time axis.',
'hours' => null,
'rooms' => null,
])
@php
// Gantt-style resource timeline: rooms down the side, hours across the top,
// category-accented session blocks on a shared axis. Category accents use
// fixed semantic token utility classes so Tailwind can see them at build
// time and theme changes remain global.
$hours ??= ['9:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00'];
$rooms ??= [
['name' => 'Conference Room A', 'capacity' => 12, 'sessions' => [
['title' => 'Team Standup', 'time' => '09:30', 'people' => 8, 'start' => 0, 'span' => 1, 'category' => 'meeting'],
['title' => 'Product Review', 'time' => '11:00', 'people' => 6, 'start' => 2, 'span' => 2, 'category' => 'review'],
]],
['name' => 'Conference Room B', 'capacity' => 8, 'sessions' => [
['title' => 'Design Workshop', 'time' => '10:00', 'people' => 5, 'start' => 1, 'span' => 2, 'category' => 'workshop'],
['title' => 'Client Meeting', 'time' => '14:00', 'people' => 4, 'start' => 5, 'span' => 1, 'category' => 'meeting'],
]],
['name' => 'Meeting Room 1', 'capacity' => 6, 'sessions' => [
['title' => 'Quick Sync', 'time' => '09:00', 'people' => 3, 'start' => 0, 'span' => 1, 'category' => 'meeting'],
['title' => 'Coffee Break', 'time' => '13:00', 'people' => 10, 'start' => 4, 'span' => 1, 'category' => 'break'],
]],
['name' => 'Meeting Room 2', 'capacity' => 4, 'sessions' => [
['title' => 'Code Review', 'time' => '10:30', 'people' => 4, 'start' => 1, 'span' => 2, 'category' => 'review'],
['title' => '1-on-1', 'time' => '15:00', 'people' => 2, 'start' => 6, 'span' => 1, 'category' => 'meeting'],
]],
['name' => 'Training Room', 'capacity' => 20, 'sessions' => [
['title' => 'React Workshop', 'time' => '09:00', 'people' => 15, 'start' => 0, 'span' => 4, 'category' => 'workshop'],
['title' => 'Security Training', 'time' => '14:30', 'people' => 18, 'start' => 5, 'span' => 3, 'category' => 'workshop'],
]],
];
$columns = max(1, count($hours));
$categories = [
'meeting' => ['label' => 'Meeting', 'dot' => 'bg-info', 'border' => 'border-s-info', 'text' => 'text-info'],
'workshop' => ['label' => 'Workshop', 'dot' => 'bg-success', 'border' => 'border-s-success', 'text' => 'text-success'],
'break' => ['label' => 'Break', 'dot' => 'bg-muted-foreground', 'border' => 'border-s-muted-foreground', 'text' => 'text-foreground'],
'review' => ['label' => 'Review', 'dot' => 'bg-warning', 'border' => 'border-s-warning', 'text' => 'text-warning'],
];
$sessionCount = array_sum(array_map(fn ($r) => count($r['sessions'] ?? []), $rooms));
$gridCols = 'grid-template-columns: minmax(9rem, 13rem) repeat('.$columns.', minmax(3.25rem, 1fr));';
@endphp
<x-ui.card x-data="{ zoom: 1.2 }" {{ $attributes->merge(['class' => 'w-full overflow-hidden']) }}>
<div class="flex flex-col gap-4 border-b border-border p-6">
<div class="flex flex-wrap items-start justify-between gap-2">
<div class="space-y-2">
<h2 class="text-lg font-semibold text-foreground">{{ __($heading) }}</h2>
@if ($subheading)
<p class="max-w-2xl text-sm text-muted-foreground">{{ __($subheading) }}</p>
@endif
</div>
<x-ui.badge variant="outline">{{ __(':count sessions', ['count' => $sessionCount]) }}</x-ui.badge>
</div>
{{-- Zoom control: scales the time axis live --}}
<div class="flex flex-col gap-2">
<label for="timeline-zoom" class="text-xs font-medium text-muted-foreground">{{ __('Zoom level') }}</label>
<input
id="timeline-zoom"
type="range" min="0.8" max="2" step="0.05"
x-model.number="zoom"
class="h-2 w-full max-w-md cursor-pointer appearance-none rounded-full bg-muted accent-primary"
aria-label="{{ __('Zoom level') }}"
/>
</div>
{{-- Category legend --}}
<div class="flex flex-wrap items-center gap-x-4 gap-y-2">
@foreach ($categories as $cat)
<span class="flex items-center gap-2 text-xs font-medium text-foreground">
<span class="size-2 rounded-full {{ $cat['dot'] }}" aria-hidden="true"></span>
{{ __($cat['label']) }}
</span>
@endforeach
</div>
</div>
<div class="relative overflow-x-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" tabindex="0" role="region" aria-label="{{ __('Timeline') }}">
<div class="min-w-[44rem]" :style="`min-width: ${44 * zoom}rem`">
{{-- Axis header --}}
<div class="grid items-center border-b border-border bg-muted/30" style="{{ $gridCols }}">
<div class="px-4 py-4 text-xs font-semibold uppercase tracking-wide text-foreground">{{ __('Rooms') }}</div>
@foreach ($hours as $hour)
<div class="border-s border-border px-2 py-4 text-xs font-medium text-muted-foreground">{{ __($hour) }}</div>
@endforeach
</div>
{{-- Room rows --}}
@foreach ($rooms as $room)
<div class="grid items-stretch border-b border-border last:border-b-0" style="{{ $gridCols }}">
{{-- Room label --}}
<div class="flex flex-col justify-center gap-2 px-4 py-4">
<span class="truncate text-sm font-medium text-foreground">{{ $room['name'] }}</span>
<span class="flex items-center gap-2 text-xs text-muted-foreground">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
{{ $room['capacity'] }}
</span>
</div>
{{-- Track: hour gridlines + placed sessions --}}
<div class="relative grid items-center py-2" style="grid-template-columns: repeat({{ $columns }}, minmax(0, 1fr)); grid-column: 2 / span {{ $columns }};">
@for ($i = 0; $i < $columns; $i++)
<div class="h-full border-s border-border first:border-s-0" style="grid-row: 1; grid-column: {{ $i + 1 }} / span 1;"></div>
@endfor
@foreach ($room['sessions'] ?? [] as $session)
@php
$cat = $categories[$session['category'] ?? 'meeting'] ?? $categories['meeting'];
$start = max(0, (int) ($session['start'] ?? 0));
$span = max(1, min((int) ($session['span'] ?? 1), $columns - $start));
@endphp
<div
class="mx-2 flex min-w-0 flex-col justify-center gap-0.5 overflow-hidden rounded-md border border-s-[3px] border-border bg-muted/40 px-2 py-2 shadow-sm transition-shadow hover:shadow-md {{ $cat['border'] }}"
style="grid-row: 1; grid-column: {{ $start + 1 }} / span {{ $span }};"
>
<span class="truncate text-xs font-semibold {{ $cat['text'] }}">{{ __($session['title']) }}</span>
<span class="flex items-center gap-2 text-xs text-muted-foreground">
<span class="flex items-center gap-2">
<svg class="size-3" 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>
{{ $session['time'] }}
</span>
@if (! empty($session['people']))
<span class="flex items-center gap-2">
<svg class="size-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>
{{ $session['people'] }}
</span>
@endif
</span>
</div>
@endforeach
</div>
</div>
@endforeach
</div>
</div>
</x-ui.card>
Installation
php artisan ui:add blocks/scheduling-gantt
Registry contract
Install confidence
php artisan ui:add blocks/scheduling-gantt
writes only the generated targets below. The CLI validates each file hash before writing and
prompts before replacing local changes unless --force is used.
- Version
- 1.0.5
- License
- open
- Stability
- stable
- Contract
- v1
- Foundation
- ≥ 1.0.0
| Type | Generated target |
|---|---|
| blade | resources/views/blocks/scheduling-gantt.blade.php |
Registry dependencies
Package dependencies
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.
# Brok UI block: Timeline (`scheduling-gantt`)
A Gantt-style resource schedule: rooms down the side, hours across the top, and category-coloured session blocks placed on a shared time axis with a legend.
Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.
## Install
```bash
php artisan ui:add blocks/scheduling-gantt
```
## Render it
```blade
<x-blocks.scheduling-gantt />
```
## Props
- `heading` (string, default `Room schedule`) — Section heading.
- `subheading` (string, default `A Gantt-style day across rooms — drag the zoom to scale the time axis.`) — Supporting copy under the heading.
- `hours` (mixed|null, default `null`) — Hour labels across the top axis; null falls back to a 9am-5pm sample range.
- `rooms` (mixed|null, default `null`) — Rooms with their capacity and scheduled sessions; null falls back to sample data.
## Use when
- Displaying a day's room bookings as a Gantt-style resource schedule with rooms down the side and hours across the top.
- Needing several resources shown with session blocks placed on a shared time axis and a category legend.
## Avoid when
- Wanting a single person's weekly open/unavailable hours instead of multi-room bookings; use scheduling-weekly instead.
## Rules
- Render the installed block with `<x-blocks.scheduling-gantt />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.
## Links
- Docs: https://brokui.dev/blocks/scheduling-gantt
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/scheduling-gantt.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Guidance
Use when
- Displaying a day's room bookings as a Gantt-style resource schedule with rooms down the side and hours across the top.
- Needing several resources shown with session blocks placed on a shared time axis and a category legend.
Avoid when
- Wanting a single person's weekly open/unavailable hours instead of multi-room bookings; use scheduling-weekly instead.
Anti-patterns
- Do not replace semantic props with conflicting utility classes.
- Do not remove labels, focus styles, or state attributes.
Usage
Render the block as a component, passing data where useful:
<x-blocks.scheduling-gantt />
Built from primitives
This block composes installed <brok:*> primitives and semantic design
tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by
editing CSS variables; flip the preview to RTL to confirm it mirrors.
Source
The exact, editable file ui:add writes
into your app. The preview above renders this same source — there are no preview-only blocks.
@props([
'heading' => 'Room schedule',
'subheading' => 'A Gantt-style day across rooms — drag the zoom to scale the time axis.',
'hours' => null,
'rooms' => null,
])
@php
// Gantt-style resource timeline: rooms down the side, hours across the top,
// category-accented session blocks on a shared axis. Category accents use
// fixed semantic token utility classes so Tailwind can see them at build
// time and theme changes remain global.
$hours ??= ['9:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00'];
$rooms ??= [
['name' => 'Conference Room A', 'capacity' => 12, 'sessions' => [
['title' => 'Team Standup', 'time' => '09:30', 'people' => 8, 'start' => 0, 'span' => 1, 'category' => 'meeting'],
['title' => 'Product Review', 'time' => '11:00', 'people' => 6, 'start' => 2, 'span' => 2, 'category' => 'review'],
]],
['name' => 'Conference Room B', 'capacity' => 8, 'sessions' => [
['title' => 'Design Workshop', 'time' => '10:00', 'people' => 5, 'start' => 1, 'span' => 2, 'category' => 'workshop'],
['title' => 'Client Meeting', 'time' => '14:00', 'people' => 4, 'start' => 5, 'span' => 1, 'category' => 'meeting'],
]],
['name' => 'Meeting Room 1', 'capacity' => 6, 'sessions' => [
['title' => 'Quick Sync', 'time' => '09:00', 'people' => 3, 'start' => 0, 'span' => 1, 'category' => 'meeting'],
['title' => 'Coffee Break', 'time' => '13:00', 'people' => 10, 'start' => 4, 'span' => 1, 'category' => 'break'],
]],
['name' => 'Meeting Room 2', 'capacity' => 4, 'sessions' => [
['title' => 'Code Review', 'time' => '10:30', 'people' => 4, 'start' => 1, 'span' => 2, 'category' => 'review'],
['title' => '1-on-1', 'time' => '15:00', 'people' => 2, 'start' => 6, 'span' => 1, 'category' => 'meeting'],
]],
['name' => 'Training Room', 'capacity' => 20, 'sessions' => [
['title' => 'React Workshop', 'time' => '09:00', 'people' => 15, 'start' => 0, 'span' => 4, 'category' => 'workshop'],
['title' => 'Security Training', 'time' => '14:30', 'people' => 18, 'start' => 5, 'span' => 3, 'category' => 'workshop'],
]],
];
$columns = max(1, count($hours));
$categories = [
'meeting' => ['label' => 'Meeting', 'dot' => 'bg-info', 'border' => 'border-s-info', 'text' => 'text-info'],
'workshop' => ['label' => 'Workshop', 'dot' => 'bg-success', 'border' => 'border-s-success', 'text' => 'text-success'],
'break' => ['label' => 'Break', 'dot' => 'bg-muted-foreground', 'border' => 'border-s-muted-foreground', 'text' => 'text-foreground'],
'review' => ['label' => 'Review', 'dot' => 'bg-warning', 'border' => 'border-s-warning', 'text' => 'text-warning'],
];
$sessionCount = array_sum(array_map(fn ($r) => count($r['sessions'] ?? []), $rooms));
$gridCols = 'grid-template-columns: minmax(9rem, 13rem) repeat('.$columns.', minmax(3.25rem, 1fr));';
@endphp
<x-ui.card x-data="{ zoom: 1.2 }" {{ $attributes->merge(['class' => 'w-full overflow-hidden']) }}>
<div class="flex flex-col gap-4 border-b border-border p-6">
<div class="flex flex-wrap items-start justify-between gap-2">
<div class="space-y-2">
<h2 class="text-lg font-semibold text-foreground">{{ __($heading) }}</h2>
@if ($subheading)
<p class="max-w-2xl text-sm text-muted-foreground">{{ __($subheading) }}</p>
@endif
</div>
<x-ui.badge variant="outline">{{ __(':count sessions', ['count' => $sessionCount]) }}</x-ui.badge>
</div>
{{-- Zoom control: scales the time axis live --}}
<div class="flex flex-col gap-2">
<label for="timeline-zoom" class="text-xs font-medium text-muted-foreground">{{ __('Zoom level') }}</label>
<input
id="timeline-zoom"
type="range" min="0.8" max="2" step="0.05"
x-model.number="zoom"
class="h-2 w-full max-w-md cursor-pointer appearance-none rounded-full bg-muted accent-primary"
aria-label="{{ __('Zoom level') }}"
/>
</div>
{{-- Category legend --}}
<div class="flex flex-wrap items-center gap-x-4 gap-y-2">
@foreach ($categories as $cat)
<span class="flex items-center gap-2 text-xs font-medium text-foreground">
<span class="size-2 rounded-full {{ $cat['dot'] }}" aria-hidden="true"></span>
{{ __($cat['label']) }}
</span>
@endforeach
</div>
</div>
<div class="relative overflow-x-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" tabindex="0" role="region" aria-label="{{ __('Timeline') }}">
<div class="min-w-[44rem]" :style="`min-width: ${44 * zoom}rem`">
{{-- Axis header --}}
<div class="grid items-center border-b border-border bg-muted/30" style="{{ $gridCols }}">
<div class="px-4 py-4 text-xs font-semibold uppercase tracking-wide text-foreground">{{ __('Rooms') }}</div>
@foreach ($hours as $hour)
<div class="border-s border-border px-2 py-4 text-xs font-medium text-muted-foreground">{{ __($hour) }}</div>
@endforeach
</div>
{{-- Room rows --}}
@foreach ($rooms as $room)
<div class="grid items-stretch border-b border-border last:border-b-0" style="{{ $gridCols }}">
{{-- Room label --}}
<div class="flex flex-col justify-center gap-2 px-4 py-4">
<span class="truncate text-sm font-medium text-foreground">{{ $room['name'] }}</span>
<span class="flex items-center gap-2 text-xs text-muted-foreground">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
{{ $room['capacity'] }}
</span>
</div>
{{-- Track: hour gridlines + placed sessions --}}
<div class="relative grid items-center py-2" style="grid-template-columns: repeat({{ $columns }}, minmax(0, 1fr)); grid-column: 2 / span {{ $columns }};">
@for ($i = 0; $i < $columns; $i++)
<div class="h-full border-s border-border first:border-s-0" style="grid-row: 1; grid-column: {{ $i + 1 }} / span 1;"></div>
@endfor
@foreach ($room['sessions'] ?? [] as $session)
@php
$cat = $categories[$session['category'] ?? 'meeting'] ?? $categories['meeting'];
$start = max(0, (int) ($session['start'] ?? 0));
$span = max(1, min((int) ($session['span'] ?? 1), $columns - $start));
@endphp
<div
class="mx-2 flex min-w-0 flex-col justify-center gap-0.5 overflow-hidden rounded-md border border-s-[3px] border-border bg-muted/40 px-2 py-2 shadow-sm transition-shadow hover:shadow-md {{ $cat['border'] }}"
style="grid-row: 1; grid-column: {{ $start + 1 }} / span {{ $span }};"
>
<span class="truncate text-xs font-semibold {{ $cat['text'] }}">{{ __($session['title']) }}</span>
<span class="flex items-center gap-2 text-xs text-muted-foreground">
<span class="flex items-center gap-2">
<svg class="size-3" 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>
{{ $session['time'] }}
</span>
@if (! empty($session['people']))
<span class="flex items-center gap-2">
<svg class="size-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>
{{ $session['people'] }}
</span>
@endif
</span>
</div>
@endforeach
</div>
</div>
@endforeach
</div>
</div>
</x-ui.card>
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 - Deprecation
- Not deprecated
- Contract
-
v1 - Foundation
-
≥ 1.0.0