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-year.blade.php Blade
@props([
    'year' => null,
    'firstDay' => 0,
    'events' => null,
    'today' => null,
])

{{--
    Calendar Year 01 (Phase 16D).
    A static full-year overview: a header showing the year plus 12 lightweight
    mini-month grids in a responsive grid (1 → 2 → 3 → 4 columns). Day grids are
    computed entirely in PHP (no Alpine/JS) so the block is cheap to render. The
    whole year is wrapped in a real <x-ui.card> (data-slot="card") and the header
    surfaces an event count via <x-ui.badge>. Direction mirrors automatically
    because each month uses a CSS grid; weekday initials honour `firstDay`.
--}}
@php
    $year = (int) ($year ?: date('Y'));
    $firstDay = ((int) $firstDay) === 1 ? 1 : 0;
    $today = $today ?: date('Y-m-d');

    $events ??= [
        $year.'-01-01',
        $year.'-02-14',
        $year.'-07-04',
        $year.'-10-31',
        $year.'-12-25',
    ];

    // Index events as a fast lookup, but only those falling within this year.
    $eventSet = [];
    foreach ((array) $events as $event) {
        $iso = is_string($event) ? trim($event) : '';
        if ($iso !== '' && str_starts_with($iso, $year.'-')) {
            $eventSet[$iso] = true;
        }
    }
    $eventCount = count($eventSet);

    // Weekday initials, rotated to the chosen first day. Index 0 = Sunday.
    $weekdayInitials = [
        __('Su'), __('Mo'), __('Tu'), __('We'), __('Th'), __('Fr'), __('Sa'),
    ];
    if ($firstDay === 1) {
        $weekdayInitials[] = array_shift($weekdayInitials);
    }

    $monthNames = [
        1 => __('January'),
        2 => __('February'),
        3 => __('March'),
        4 => __('April'),
        5 => __('May'),
        6 => __('June'),
        7 => __('July'),
        8 => __('August'),
        9 => __('September'),
        10 => __('October'),
        11 => __('November'),
        12 => __('December'),
    ];

    // Pre-compute each month's leading blanks + day numbers in pure PHP.
    $months = [];
    for ($month = 1; $month <= 12; $month++) {
        $first = new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month));
        $daysInMonth = (int) $first->format('t');
        // 0 (Sun) .. 6 (Sat) for the 1st of the month.
        $firstWeekday = (int) $first->format('w');
        // Number of empty leading cells given the chosen first day.
        $leading = ($firstWeekday - $firstDay + 7) % 7;

        $days = [];
        for ($i = 0; $i < $leading; $i++) {
            $days[] = null;
        }
        for ($d = 1; $d <= $daysInMonth; $d++) {
            $iso = sprintf('%04d-%02d-%02d', $year, $month, $d);
            $days[] = [
                'day' => $d,
                'iso' => $iso,
                'isToday' => $iso === $today,
                'isEvent' => isset($eventSet[$iso]),
            ];
        }

        $months[] = [
            'name' => $monthNames[$month],
            'days' => $days,
        ];
    }
@endphp

<x-ui.card {{ $attributes->merge(['class' => 'w-full']) }}>
    <div class="flex flex-wrap items-center justify-between gap-4 border-b border-border p-6">
        <div class="flex items-center gap-4">
            <h2 class="text-2xl font-semibold tracking-tight text-foreground">{{ $year }}</h2>
            @if ($eventCount > 0)
                <x-ui.badge variant="secondary">{{ __(':count events', ['count' => $eventCount]) }}</x-ui.badge>
            @endif
        </div>
        <div class="flex items-center gap-2" aria-hidden="true">
            <span class="inline-flex size-8 items-center justify-center rounded-md border border-border text-muted-foreground">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 rtl:-scale-x-100"><path d="m15 18-6-6 6-6" /></svg>
            </span>
            <span class="inline-flex size-8 items-center justify-center rounded-md border border-border text-muted-foreground">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 rtl:-scale-x-100"><path d="m9 18 6-6-6-6" /></svg>
            </span>
        </div>
    </div>

    <div class="grid grid-cols-1 gap-4 p-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
        @foreach ($months as $monthData)
            <div class="rounded-lg border border-border p-6">
                <h3 class="mb-2 text-sm font-medium text-foreground">{{ $monthData['name'] }}</h3>

                {{-- Weekday initials --}}
                <div class="grid grid-cols-7">
                    @foreach ($weekdayInitials as $initial)
                        <div class="flex h-6 items-center justify-center text-xs font-normal text-muted-foreground">
                            {{ $initial }}
                        </div>
                    @endforeach
                </div>

                {{-- Day cells --}}
                <div class="grid grid-cols-7 gap-y-0.5">
                    @foreach ($monthData['days'] as $cell)
                        @if ($cell === null)
                            <div class="size-7" aria-hidden="true"></div>
                        @else
                            <div class="flex items-center justify-center">
                                <span
                                    @class([
                                        'flex size-7 items-center justify-center rounded-full text-xs transition-colors motion-reduce:transition-none',
                                        'bg-primary font-semibold text-primary-foreground' => $cell['isToday'],
                                        'bg-accent text-accent-foreground' => $cell['isEvent'] && ! $cell['isToday'],
                                        'text-foreground hover:bg-muted' => ! $cell['isToday'] && ! $cell['isEvent'],
                                    ])
                                    @if ($cell['isToday']) aria-current="date" @endif
                                >
                                    {{ $cell['day'] }}
                                </span>
                            </div>
                        @endif
                    @endforeach
                </div>
            </div>
        @endforeach
    </div>
</x-ui.card>

Installation

terminal
php artisan ui:add blocks/scheduling-year

Installs these dependencies automatically:

Registry contract

Install confidence

php artisan ui:add blocks/scheduling-year 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.3
License
open
Stability
stable
Contract
v1
Foundation
≥ 1.0.0
Type Generated target
blade resources/views/blocks/scheduling-year.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-year.md
# Brok UI block: Calendar Year (`scheduling-year`)

A static full-year overview: a header with the year and an event count, plus 12 lightweight mini-month grids in a responsive 1-to-4 column layout. Highlights today and marks event dates.

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-year
```

## Render it

```blade
<x-blocks.scheduling-year />
```

## Props

- `year` (mixed|null, default `null`) — Year rendered; null falls back to the current year.
- `first-day` (int, default `0`) — First weekday of each grid (0 = Sunday, 1 = Monday).
- `events` (mixed|null, default `null`) — ISO date strings marked as events; null falls back to a small sample set.
- `today` (mixed|null, default `null`) — Date treated as today for the highlight; null falls back to the server's current date.

## Use when

- Giving a static full-year overview with 12 mini-month grids that highlight today and mark event dates.
- Wanting a yearly-at-a-glance view rather than a scheduling or booking tool.

## Avoid when

- Needing a day-by-day resource or availability schedule; use scheduling-gantt or scheduling-weekly instead.

## Rules

- Render the installed block with `<x-blocks.scheduling-year />` 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-year
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/scheduling-year.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

  • Giving a static full-year overview with 12 mini-month grids that highlight today and mark event dates.
  • Wanting a yearly-at-a-glance view rather than a scheduling or booking tool.

Avoid when

  • Needing a day-by-day resource or availability schedule; use scheduling-gantt or scheduling-weekly instead.

Anti-patterns

  • Do not replace semantic props with conflicting utility classes.
  • Do not remove labels, focus styles, or state attributes.
Anatomy
root card

Usage

Render the block as a component, passing data where useful:

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

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-year.blade.php Blade
@props([
    'year' => null,
    'firstDay' => 0,
    'events' => null,
    'today' => null,
])

{{--
    Calendar Year 01 (Phase 16D).
    A static full-year overview: a header showing the year plus 12 lightweight
    mini-month grids in a responsive grid (1 → 2 → 3 → 4 columns). Day grids are
    computed entirely in PHP (no Alpine/JS) so the block is cheap to render. The
    whole year is wrapped in a real <x-ui.card> (data-slot="card") and the header
    surfaces an event count via <x-ui.badge>. Direction mirrors automatically
    because each month uses a CSS grid; weekday initials honour `firstDay`.
--}}
@php
    $year = (int) ($year ?: date('Y'));
    $firstDay = ((int) $firstDay) === 1 ? 1 : 0;
    $today = $today ?: date('Y-m-d');

    $events ??= [
        $year.'-01-01',
        $year.'-02-14',
        $year.'-07-04',
        $year.'-10-31',
        $year.'-12-25',
    ];

    // Index events as a fast lookup, but only those falling within this year.
    $eventSet = [];
    foreach ((array) $events as $event) {
        $iso = is_string($event) ? trim($event) : '';
        if ($iso !== '' && str_starts_with($iso, $year.'-')) {
            $eventSet[$iso] = true;
        }
    }
    $eventCount = count($eventSet);

    // Weekday initials, rotated to the chosen first day. Index 0 = Sunday.
    $weekdayInitials = [
        __('Su'), __('Mo'), __('Tu'), __('We'), __('Th'), __('Fr'), __('Sa'),
    ];
    if ($firstDay === 1) {
        $weekdayInitials[] = array_shift($weekdayInitials);
    }

    $monthNames = [
        1 => __('January'),
        2 => __('February'),
        3 => __('March'),
        4 => __('April'),
        5 => __('May'),
        6 => __('June'),
        7 => __('July'),
        8 => __('August'),
        9 => __('September'),
        10 => __('October'),
        11 => __('November'),
        12 => __('December'),
    ];

    // Pre-compute each month's leading blanks + day numbers in pure PHP.
    $months = [];
    for ($month = 1; $month <= 12; $month++) {
        $first = new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month));
        $daysInMonth = (int) $first->format('t');
        // 0 (Sun) .. 6 (Sat) for the 1st of the month.
        $firstWeekday = (int) $first->format('w');
        // Number of empty leading cells given the chosen first day.
        $leading = ($firstWeekday - $firstDay + 7) % 7;

        $days = [];
        for ($i = 0; $i < $leading; $i++) {
            $days[] = null;
        }
        for ($d = 1; $d <= $daysInMonth; $d++) {
            $iso = sprintf('%04d-%02d-%02d', $year, $month, $d);
            $days[] = [
                'day' => $d,
                'iso' => $iso,
                'isToday' => $iso === $today,
                'isEvent' => isset($eventSet[$iso]),
            ];
        }

        $months[] = [
            'name' => $monthNames[$month],
            'days' => $days,
        ];
    }
@endphp

<x-ui.card {{ $attributes->merge(['class' => 'w-full']) }}>
    <div class="flex flex-wrap items-center justify-between gap-4 border-b border-border p-6">
        <div class="flex items-center gap-4">
            <h2 class="text-2xl font-semibold tracking-tight text-foreground">{{ $year }}</h2>
            @if ($eventCount > 0)
                <x-ui.badge variant="secondary">{{ __(':count events', ['count' => $eventCount]) }}</x-ui.badge>
            @endif
        </div>
        <div class="flex items-center gap-2" aria-hidden="true">
            <span class="inline-flex size-8 items-center justify-center rounded-md border border-border text-muted-foreground">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 rtl:-scale-x-100"><path d="m15 18-6-6 6-6" /></svg>
            </span>
            <span class="inline-flex size-8 items-center justify-center rounded-md border border-border text-muted-foreground">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 rtl:-scale-x-100"><path d="m9 18 6-6-6-6" /></svg>
            </span>
        </div>
    </div>

    <div class="grid grid-cols-1 gap-4 p-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
        @foreach ($months as $monthData)
            <div class="rounded-lg border border-border p-6">
                <h3 class="mb-2 text-sm font-medium text-foreground">{{ $monthData['name'] }}</h3>

                {{-- Weekday initials --}}
                <div class="grid grid-cols-7">
                    @foreach ($weekdayInitials as $initial)
                        <div class="flex h-6 items-center justify-center text-xs font-normal text-muted-foreground">
                            {{ $initial }}
                        </div>
                    @endforeach
                </div>

                {{-- Day cells --}}
                <div class="grid grid-cols-7 gap-y-0.5">
                    @foreach ($monthData['days'] as $cell)
                        @if ($cell === null)
                            <div class="size-7" aria-hidden="true"></div>
                        @else
                            <div class="flex items-center justify-center">
                                <span
                                    @class([
                                        'flex size-7 items-center justify-center rounded-full text-xs transition-colors motion-reduce:transition-none',
                                        'bg-primary font-semibold text-primary-foreground' => $cell['isToday'],
                                        'bg-accent text-accent-foreground' => $cell['isEvent'] && ! $cell['isToday'],
                                        'text-foreground hover:bg-muted' => ! $cell['isToday'] && ! $cell['isEvent'],
                                    ])
                                    @if ($cell['isToday']) aria-current="date" @endif
                                >
                                    {{ $cell['day'] }}
                                </span>
                            </div>
                        @endif
                    @endforeach
                </div>
            </div>
        @endforeach
    </div>
</x-ui.card>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.0.3
Status
Stable
License
open
Deprecation
Not deprecated
Contract
v1
Foundation
≥ 1.0.0