busy-office-ui
Menu

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).

August 2026 — days with a link carry scheduled work
MoTuWeThFrSaSu
— 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

  1. Month grid — calendar; a day with entries is a real link, a day with none is a plain cell.
  2. Which days carry work — the calendar's own data-day marks (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.
  3. Open-day indicator — data-day="selected" on the chosen cell, the same value the calendar's picker already ships.
  4. 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

RequestReturns / on failure
GET /schedule?month=2026-08the 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/:datethe 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 emptyan 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 datenever 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

StateWhat ships
Nothing selectedthe panel holds a one-line prompt ("Pick a day to see what's on it"), not a blank box
Loading the dayaria-busy on the panel only; the month stays interactive so the reader can pick a different day mid-load
Empty daya 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 bookmarkthe DIFFERENT empty — explain it is a closed/holiday date, not "nothing scheduled yet"
Conflict on one entryflagged 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 detailthe 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 screenpanel 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 JSfully 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>
↑↓ move Enter open Esc close