Skip to content
Brok UI

Loading…

No results

Scheduling

Open source

Time and calendar views — a Gantt schedule, a weekly grid and a year overview.

Preview

resources/views/blocks/scheduling-gantt.blade.php Blade
@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

terminal
php artisan ui:add blocks/scheduling-gantt

Installs these dependencies automatically:

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

card badge

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.

scheduling-gantt.md
# 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:

resources/views/welcome.blade.php Blade
<x-blocks.scheduling-gantt />

Built from primitives

Composes <brok:*> Semantic tokens Re-themeable RTL-ready

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.

resources/views/blocks/scheduling-gantt.blade.php Blade
@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