Pattern: schedule calendar
A planner or supervisor scans a month for what is booked — deliveries, maintenance windows, shift cover — several times a day during a busy week, and once a day otherwise. "Done" is picking a day and seeing exactly what is on it without losing the month underneath, the same swap discipline master-detail uses for a list and a record. Not for authoring a schedule — creating, moving or cancelling entries is the admin module's job, not this screen's — and not for one entry's own detail or history, which belongs to record detail, not a month grid. This page only answers "what is on this day".
The screen
Sized to this page's column below — the real screen is full-width. Open it full screen ↗ (new tab, no docs chrome — the same markup below, at real scale).
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
| — non-working day | — non-working day | |||||
| — public holiday, no deliveries | — 2 scheduled | — non-working day | — non-working day | |||
| — 1 scheduled | — non-working day | — non-working day | ||||
| — today, 3 scheduled | — non-working day | — non-working day | ||||
| — currently open, 4 scheduled | — 2 scheduled | — non-working day | — non-working day |
The month stays put while the day-detail list changes — the calendar is never re-rendered on a click, so the reader's place in the month survives. Only days carrying at least one entry are real links; an empty workday is a plain cell, the same "not everything is clickable" discipline the calendar component's own picker uses.
Which day is open is carried the same way master-detail carries
row selection — data-day="selected" on the cell,
which is the calendar component's own state, reused rather than
invented. There is no second "which day is open" mechanism to keep in
sync with it.
Anatomy
- Month grid — calendar; a day with entries is a real link, a day with none is a plain cell.
- Which days carry work — the calendar's own
data-daymarks (closed,holiday,today) plus whether the cell is a link at all; no new attribute for "has entries", the link/no-link distinction already carries it. - Open-day indicator —
data-day="selected"on the chosen cell, the same value the calendar's picker already ships. - Day-detail panel — a card beside the month, holding a compact data table of that day's entries with a status badge per row.
Data contract
| Request | Returns / on failure |
|---|---|
| GET /schedule?month=2026-08 | the month grid, with each day's data-day mark and whether it is a link baked in server-side; the detail panel renders empty until a day is chosen |
| GET /schedule/day/:date | the day-detail fragment only, swapped into #schedule-detail. Never re-render the month — that is what loses the reader's place in it |
| 404 / day now empty | an entry was cancelled after the month rendered — replace the panel with an empty state naming the day, and stop treating that cell as a link on the next month fetch |
| GET /schedule/day/:date on a closed or holiday date | never happens from a click (those cells are plain spans, not links); a bookmarked URL still gets a real answer: an empty state saying the works calendar has no entries possible that day |
No AI-manages/human-monitors split here, stated rather than left silent: this screen is a planner reading and booking dates, not oversight of an automated process — the closed/holiday marks are static business rules, not something an agent manages behind the scenes. There is no monitoring signal to name.
States
| State | What ships |
|---|---|
| Nothing selected | the panel holds a one-line prompt ("Pick a day to see what's on it"), not a blank box |
| Loading the day | aria-busy on the panel only; the month stays interactive so the reader can pick a different day mid-load |
| Empty day | a day that IS a link (server said it has entries) but returns none by the time it's opened — say so, do not show an empty table with headers and nothing under them |
| Non-working day opened via a bookmark | the DIFFERENT empty — explain it is a closed/holiday date, not "nothing scheduled yet" |
| Conflict on one entry | flagged in the row with a badge (see the demo above), not surfaced as a page-level error — the other entries that day are still fine |
| No permission to open a day's detail | the day stays a plain cell rather than a disabled-looking link — a control a reader can see but never use is worse than one that is simply absent |
| Narrow screen | panel and grid stack in source order — the split's own auto-fit grid (see .sched-split below) collapses to one column below ~36rem; the calendar component itself carries no container breakpoint, nothing new to build here |
| No JS | fully functional — this page has no JS dependency; each day is a real <a href="#schedule-detail"> fragment link (native anchor navigation, no script), and the page imports no @busy-office/ui/js behavior |
Components used
complexity 2 of 4
Markup
<div class="bo-calendar">
<table class="bo-calendar__month">
<caption>August 2026 — days with a link carry scheduled work</caption>
<thead><tr><th scope="col"><abbr title="Monday">Mo</abbr></th><th scope="col"><abbr title="Tuesday">Tu</abbr></th><th scope="col"><abbr title="Wednesday">We</abbr></th><th scope="col"><abbr title="Thursday">Th</abbr></th><th scope="col"><abbr title="Friday">Fr</abbr></th><th scope="col"><abbr title="Saturday">Sa</abbr></th><th scope="col"><abbr title="Sunday">Su</abbr></th></tr></thead>
<tbody>
<tr><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-27">27</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-28">28</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-29">29</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-30">30</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-31">31</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-01">1</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-02">2</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day" data-day="holiday"><time datetime="2026-08-03">3</time><span class="bo-visually-hidden"> — public holiday, no deliveries</span></span></td><td><a class="bo-calendar__day" href="#schedule-detail"><time datetime="2026-08-04">4</time><span class="bo-visually-hidden"> — 2 scheduled</span></a></td><td><span class="bo-calendar__day"><time datetime="2026-08-05">5</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-06">6</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-07">7</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-08">8</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-09">9</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-08-10">10</time></span></td><td><a class="bo-calendar__day" href="#schedule-detail"><time datetime="2026-08-11">11</time><span class="bo-visually-hidden"> — 1 scheduled</span></a></td><td><span class="bo-calendar__day"><time datetime="2026-08-12">12</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-13">13</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-14">14</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-15">15</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-16">16</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-08-17">17</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-18">18</time></span></td><td><a class="bo-calendar__day" href="#schedule-detail" data-day="today"><time datetime="2026-08-19">19</time><span class="bo-visually-hidden"> — today, 3 scheduled</span></a></td><td><span class="bo-calendar__day"><time datetime="2026-08-20">20</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-21">21</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-22">22</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-23">23</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><a class="bo-calendar__day" href="#schedule-detail" data-day="selected"><time datetime="2026-08-24">24</time><span class="bo-visually-hidden"> — currently open, 4 scheduled</span></a></td><td><span class="bo-calendar__day"><time datetime="2026-08-25">25</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-26">26</time></span></td><td><a class="bo-calendar__day" href="#schedule-detail"><time datetime="2026-08-27">27</time><span class="bo-visually-hidden"> — 2 scheduled</span></a></td><td><span class="bo-calendar__day"><time datetime="2026-08-28">28</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-29">29</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-30">30</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
</tbody>
</table>
</div>