Calendar

Event & booking calendar with three views: Month, Week, List. Two intents: browse (see what's happening) and book (pick a slot). Different from Datepicker which is the input control — Calendar is the schedule surface.

a11y (built in): event pills, "+N more" and booking slots are real <button>s — the slots carry an aria-label naming the slot's time range (plus "(unavailable)") and take the native disabled attribute when blocked or past. The view switcher is a role=radiogroup segmented control with roving tabindex and ←/→, Home/End, Enter/Space; prev/next are labelled icon buttons; List view is an Accordion (button[aria-expanded][aria-controls] + role=region panel). Today's cell gets aria-current="date", week numbers and booking counts get spelled-out aria-labels ("Week 22", "3 slots available"), and dots, past-overlays and the grid corner are aria-hidden.

Usage: <div data-lb-calendar></div> — auto-init on page load. Mutate via el._lbCalendar.setEvents([...]), setView('week'), etc. Fires lb-calendar-event-click, lb-calendar-date-click, lb-calendar-slot-click and lb-calendar-view-change — the consumer listens.


Month view

The headline view. ISO week numbers in the left column (toggle off with data-lb-no-week-numbers). Today's date underlined in accent. Past days dim slightly so it's clear what's happened vs. what's ahead. Events use the L2 categorical palette (data.1..8) — pass category: 1..8 per event.

Week numbers — ISO 8601

Each row in the month grid shows its ISO 8601 week number — week 1 is the week containing the first Thursday of the year. Common in Sweden, Germany and other EU contexts; agile/sprint teams reference work by week (e.g. "ship in W22"). Toggle off with data-lb-no-week-numbers if you don't need it.

With week numbers (default)
Without week numbers

Event categories

Events colour by category: 1..8 which maps to the L2 categorical palette (--lb-data-1 … --lb-data-8). Same eight hues used by Bar Chart and other data-viz components, so categorical identity is consistent across surfaces. Light/dark theming is automatic.

Density overflow — "+N more"

Month cells render up to 3 events per day. When more events land on the same day, a "+N more" link replaces the overflow. Click → opens a popover with the full day's list. Click an event in the popover → fires lb-calendar-event-click. Click outside → closes. Capping the list keeps day cells a predictable height, so the month grid stays scannable in dense calendars.

Slot duration & default scroll

Week view's hour grid uses 30-minute slots by default — each slot is a fixed 2.5rem tall, so a 30-min meeting fills one row (40px), a 1-hour meeting fills two (80px). Override per-instance with data-lb-slot-duration="15|30|60". The scroll position lands at 08:00 by default (override with data-lb-scroll-hour). The header + all-day strip stay sticky as you scroll the hours. We deliberately do not auto-track the clock — the view opens at that hour and never re-scrolls itself as time passes.

30-min slots (default)
15-min slots — for booking precision

Booking intent — slot picker

Set data-lb-intent="book" and pass bookingSlots to render a slot picker on the time grid. Each slot is { id, start, end, available, selected? }. Available slots are clickable and fire lb-calendar-slot-click; blocked slots render greyed (the user sees the slot exists but can't pick it); selected slots fill solid. Clamp to working hours via data-lb-min-time / data-lb-max-time — typical for a doctor's office, sales calls, gym classes.

Output: click an available slot

Working hours — data-lb-min-time / data-lb-max-time

Clamp the visible hour range so the calendar stops wasting screen on midnight. Hours outside the range simply don't render — the grid is shorter, no scroll past the configured edges. Useful for booking flows but works for any view. Side-by-side: a wider 06–22 slot vs a tighter 09–17 office-hours slot.

06:00 – 22:00 (extended)
09:00 – 17:00 (office hours)

Booking — Month overview

In Month view, days with available slots get a small accent count badge next to the date showing how many slots are open. Uses the shared .lb-counter component (small size). Click any day → switches to Week view focused on that day so the user can see + pick the slots. Past days don't show the indicator (the slots are gone).

Empty state

When no events are scheduled, all three views read calmly. List view is the most explicit ("No events" per day). Month and Week views just show empty cells.

Programmatic API

Mutate at runtime via el._lbCalendar. Useful for jumping to a specific date, swapping views, or pushing new events as they arrive from a backend.

Output: click an event or date