Scheduling
Time and calendar views — a Gantt schedule, a weekly grid and a year overview.
Preview
@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
php artisan ui:add blocks/scheduling-year
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
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: 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
Usage
Render the block as a component, passing data where useful:
<x-blocks.scheduling-year />
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([
'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