BMX Web Components evaluation

<bmx-calendar>

A diary.

The whole diary is one element with a JSON attribute and no script — which is what a Razor page, a Blazor component, a Django template or an htmx swap can actually produce. An all-day event's end is inclusive here: “the 9th to the 11th” is three days, because that is what the person typing it means. The iCalendar convention is exclusive and is right for a machine, which can subtract a day.
Use the keyboard. Tab reaches the grid once, not forty-two times. The arrows move a day, up and down move a week, and Page Up and Page Down move a month — then Enter steps into a day so the events in it are reachable, and Escape comes back out. That two-mode model is what lets a month be a grid at all; without it the events in a month view are either forty-two extra tab stops or unreachable.
The Conference is one bar across three days rather than three pieces that happen to line up — and if it crossed a Sunday it would draw as two bars, each open at the end where it continues. That clipping is weekBars in src/core/diary.ts, which is the same lane-packing function a day column will use for overlapping meetings: a three-day event crossing a week and two meetings overlapping at half past ten look nothing alike and are the same problem.
The hour rail is decoration and is not announced. The obvious markup for a time grid — a row per half hour, a cell per day — is a lie the moment two meetings overlap or one runs from 9:15 to 10:45: there is no honest cell for it to be in, and a reader arrowing through that table hears fragments of meetings in slots they do not start in. So each day is one cell whose name is its events in time order, and Enter steps into it for the detail. Left and right walk the days; up and down move between the all-day strip and the column, because that is what is above and below in a week.
day-start and day-end window the view, because a diary drawn midnight to midnight spends two thirds of its height on hours nobody has meetings in. Tuesday's early call starts at half past six and the grid begins at eight — it is clipped to the top edge and drawn open, and still announced at half past six, because the reader is being told when the meeting is rather than where the box was cut. The Conference has no time of day, so it has no position on a time axis: it is in the strip and never in the column.
The agenda is a different view rather than a stylesheet over the same markup, because it is a different thing to a screen reader: a month is a grid, where the position of a cell means “Tuesday” and “next week”, and an agenda has no second dimension at all. It also leaves empty days out, which is the whole difference between an agenda and a very thin month — somebody asking what is coming up should not have to scroll past twenty empty rows to find out.
+2 more is drawn and never read: what it announces is “2 more: One to one, Retro”. And a limit that would hide exactly one hides none, because a +1 takes the room of the event it replaced and says less — the same rule, and the same function, as the chip set and the avatar group.
editable lets a reader move and resize. The keyboard is the real gesture and the pointer is the second one: Enter steps into the day, Space picks a meeting up, the arrows move it a quarter hour or a day, Shift with them resizes, Enter drops it and Esc puts it back — and every step is announced, because a block that moves on screen and says nothing is a mode with no output. Both routes are the same state machine in src/core/diary.ts, so they cannot disagree. “Payroll” is readonly and refuses to be picked up at all.
Nothing changes until the drop. The moving block is drawn from a proposal and your events array is untouched; on release the component emits bmxEventChange with both the old times and the new ones, and the page reassigns events if it accepts. A page that ignores the event has a calendar whose blocks always snap back, which is the honest behaviour for a page that has not agreed to the change. To refuse a drop before the reader lets go — a day that is closed, a room already taken — assign a dropFilter function from script; the block is then drawn and announced as refused while it is still in their hand.

Every property, event and method: https://docs.binarymission.co.uk/webcomponents/bmx/bmx-calendar.html