KQ-1741 · SHIFT/ROSTER

Shift Roster

A week's staff rota: people down the side, days across the top, an Open row of unassigned shifts, and under every day its coverage drawn as a hammock that sags below the target line when the day is short. A shift follows the pointer 1:1 over the board and lands with a FLIP from where it was let go on springs.snap carrying the release velocity; dropped on another shift the two swap. The day that gains hours springs up on springs.recoil and the day that lost them sinks on springs.glide, while clashing shifts ring each other in amber. On the keyboard Space picks a cell's shift up, arrows carry it a glide at a time and Space drops it.

KQ-1741 · SHIFT/ROSTER

Fernworks rota

3 open shifts · 4 days short · 1 clash

PlantsTillsCaféYard

Arrow keys move between cells. Space picks up the cell's shift; arrows carry it; Space drops it, onto another shift to swap; Escape puts it back.

Wednesday 2124/24 h

Open

3 shifts to fill

Ines Moreau

38/37.5 h

08–16Plants

Theo Park

30/30 h

09–17Tills

Priya Nair

24/24 h

Sam Okoro

40/40 h

07–15Yard

Lena Fisch

17/20 h

Ivo Brandt

28/30 h

sat 28/34 h · 3 open

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/shift-roster

Props

blockStyle
"soft" | "solid" | "outline"
default "soft"
How a shift block is drawn: a tinted fill with a coloured edge, a pigment fill, or an outline.
rowHeight
number
default 48
Height of each person's row in px; blocks grow with it.
palette
"garden" | "cool" | "mono"
default "garden"
The area colours: moss, amber, terracotta and sky; cobalt, teal, violet and slate; or ink shades.
staff
RosterStaff[]
default defaultRosterStaff
The people down the side: { id, name, area, contract (hours a week) }.
shifts / defaultShifts
RosterShift[]
default — / defaultRosterShifts
The shifts, controlled or initial: { id, staff: string | null (open), day: 0–6 from Monday, start, end (minutes), area }.
onShiftsChange
(shifts: RosterShift[]) => void
default —
Fires from the drop or key that moved a shift, and from Discard, with every shift.
published
RosterShift[]
default the initial shifts
The last published rota; the save bar counts changes against it.
coverage
number[]
default defaultRosterCoverage
Target hours per day, Monday to Sunday.
weekStart
number
default defaultRosterWeek
The Monday the week starts, ms.
now
number
default defaultRosterNow
The current time, ms; today's column is marked. Never read from the clock.
onAssign
(shift: RosterShift, staffId: string | null, day: number) => void
default —
A shift was moved onto a person's day (null: back to Open).
onSwap
(a: RosterShift, b: RosterShift) => void
default —
Two shifts swapped places.
onPublish
(shifts: RosterShift[]) => void
default —
Publish was pressed with every shift.
onDiscard
() => void
default —
Discard was pressed: the rota went back to the published one.
status
"ready" | "loading" | "error"
default "ready"
Whether the rota has arrived: loading shows skeleton rows, error a Try again.
onRetry
() => void
default —
Try again was pressed after the rota failed to load.
title
string
default "Fernworks rota"
The heading.
height
number
default 540
Height of the surface in px; the board scrolls inside.
disabled
boolean
default false
Nothing can be moved or published.
className
string
default —
Classes for the outer surface.