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