Show the rota as a month rather than a list of dates

The Team tab printed the next thirty days as thirty rows of date, name and
a Clear button. That is a rota spelled out one day at a time, and it is the
one shape the question cannot be read in: what anybody wants from a rota is
who holds which stretch, and thirty names down a column hides a handover
between two rows that look the same. It was also the longest thing on the
page by a wide margin, so the escalation ladder and the alert sources sat
below a screen of dates.

It is a month now, Monday to Sunday, one coloured initial per day. A shift
becomes a run of one colour, which is the shape the answer actually has; a
gap becomes a hole you can see. The legend underneath says whose colour is
whose, and one line says how many days are left uncovered, counting only
from today -- an empty Tuesday last week is history, not a hole somebody
still has to fill.

Laid out like the on-call page's week, deliberately: heading and arrows
outside the card, days inside it. It is the same rota, and two pages
showing it two ways would be two things to learn.

Colours come from a person's place in the member list, so they hold still
as you page between months, and six of them repeat -- the initial inside
still tells two people apart, and a legend that has to explain nine hues is
not a legend. They are not the severity palette: nothing on a rota is
critical, and a red Thursday would read as one. --teal and --pink are new
in both themes for the two the palette was short.

The per-row Clear button had nowhere left to live, so a day opens the sheet
the app already uses for confirmations: who holds it, a picker, Assign and
Clear. That assign sends replace=true where the range form still asks
first, and the difference is the point -- the sheet has just named whoever
holds the day, so taking it from them is the thing that was asked for
rather than something to warn about. The range form is unchanged and folded
into a details, since filling a whole shift is what it is for; it opens on
the month above it rather than on today, so paging to March to fill March
does not hand you September.

The server is untouched. The month drawn is the month fetched -- the grid's
Monday overhang and its trailing days are real days and are fetched with
it -- so paging is one GET /api/teams/{id}/schedule per month with from and
to, where it used to be one fixed thirty-day window. No new endpoint, no
change to what the API returns, and terdut-tui is unaffected.

Nobody has looked at this in a browser either. What is checked is the
rendering: team.js's own refresh() was run against a stub fetch and a
pocket DOM for September 2026, and it produces 35 cells for a month whose
1st is a Tuesday, the right from/to on the schedule call, today marked on
the 22nd, three people in the legend with "you" on the viewer, the gap
count over a five-day hole, and -- as a member rather than an owner -- the
same grid as plain divs with no sheet and no range form. How it looks at
phone width, and whether the six colours hold up in dark mode, are not
checked.

Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
This commit is contained in:
Niklas Ye
2026-09-22 09:13:36 +02:00
parent a6fa673e08
commit 67d68ce058
3 changed files with 267 additions and 23 deletions
+67
View File
@@ -31,6 +31,14 @@
--snooze: #6b5bd2;
--snooze-soft: #efedfb;
/* Two hues that mean nothing on their own. The rota needs six colours to
tell six people apart and the palette above only has four that are not
already an alarm. */
--teal: #0f7d8c;
--teal-soft: #e3f4f6;
--pink: #b3427e;
--pink-soft: #fbe8f2;
--radius: 10px;
--radius-sm: 6px;
--shadow: 0 1px 2px rgb(16 24 40 / 6%), 0 1px 3px rgb(16 24 40 / 8%);
@@ -72,6 +80,11 @@
--snooze: #a89bff;
--snooze-soft: #262245;
--teal: #4fc2d4;
--teal-soft: #0f2e33;
--pink: #f07fb8;
--pink-soft: #3a1c2d;
--shadow: 0 1px 2px rgb(0 0 0 / 40%);
--shadow-lg: 0 16px 40px rgb(0 0 0 / 55%);
}
@@ -729,6 +742,60 @@ kbd {
.stacked-form input.wide { min-width: min(420px, 100%); }
.team-picker { margin-top: 8px; max-width: 100%; }
/* The rota, a month at a time. A name is too wide to print thirty times and
too alike down a column to read, so a day carries an initial in that
person's colour and the legend underneath says whose. A shift is then a run
of one colour, which is the shape the question actually has. */
.rota-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; padding: 10px; }
.rota-wd {
padding-bottom: 4px; text-align: center;
color: var(--muted); font-size: 11px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.04em;
}
.rota-day {
display: flex; flex-direction: column; align-items: center; gap: 4px;
min-height: 52px; padding: 6px 0 8px;
border: 0; border-radius: var(--radius-sm); background: none;
font: inherit; color: inherit;
}
button.rota-day { cursor: pointer; }
button.rota-day:hover { background: var(--surface-2); }
.rota-num { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.rota-day.today { background: var(--accent-soft); }
.rota-day.today .rota-num { color: var(--accent); font-weight: 700; }
.rota-day.past { opacity: 0.55; }
/* The days either side of the month are real days and are drawn, but they
belong to the month you are not looking at. */
.rota-day.outside { opacity: 0.35; }
.rota-chip {
display: grid; place-items: center;
width: 26px; height: 26px; border-radius: 50%;
font-size: 12px; font-weight: 750; text-transform: uppercase;
}
/* An empty day is a dot rather than a hole, and keeps the chip's box so the
rows stay on one baseline. */
.rota-chip.none { width: 8px; height: 8px; margin: 9px; background: var(--border-strong); }
/* Six colours, then they repeat; the initial inside still tells two people
apart. Deliberately not the severity palette — nothing here is critical. */
.rc1 { background: var(--accent-soft); color: var(--accent); }
.rc2 { background: var(--ok-soft); color: var(--ok); }
.rc3 { background: var(--snooze-soft); color: var(--snooze); }
.rc4 { background: var(--warn-soft); color: var(--warn); }
.rc5 { background: var(--teal-soft); color: var(--teal); }
.rc6 { background: var(--pink-soft); color: var(--pink); }
.rota-foot { padding: 12px 14px; border-top: 1px solid var(--border); }
.rota-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; font-size: 14px; }
.rota-key { display: inline-flex; align-items: center; gap: 6px; }
.rota-key .rota-chip { width: 22px; height: 22px; font-size: 11px; }
.rota-note { margin: 10px 0 0; color: var(--muted); font-size: 13px; }
.rota-note:first-child { margin-top: 0; }
.rota-bulk { padding: 12px 14px; border-top: 1px solid var(--border); }
.rota-bulk .stacked-form { margin-top: 4px; }
.sheet-pick { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.ladder-level {
border-left: 3px solid var(--border-strong);
padding: 8px 0 8px 12px; margin: 12px 0;