Skip to content
txbrendan/ui

Duty Slot

beta

One volunteer duty on one day, as a row in a card: its icon and name, what it involves, who is on it, and where it stands, ending in the quickest fix.

Preview

Thursday 8 October
RefreshmentsMorning break and lunchSue Hale
SetupChairs and screens before 9Tom HaleWaiting for a reply
First aidOn site all day
Front deskSign guests in from 9Nobody yet
PhotosPhotos for the newsletterNobody yet
Front desk this month
Mon 5Front deskSue Hale
Mon 12Front deskDan KerrWaiting for a reply
Mon 19Front desk
Mon 26Front deskNobody yet

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/duty-slot

Use it for

  • One row per duty, in a Card: refreshments, setup, first aid, the front desk, photos.
  • The screen keeps who is on what and passes state. Confirmed is quiet. Waiting for a reply ends in Remind, Needs cover in Find cover, and Nobody yet in Find someone.
  • Pass suggest with the best person to ask and why. Accepting reads Asked at once, with Undo, and onAsk runs once the window ends. Move the duty to asked, with that person, when it lands.
  • Pass out with who dropped out and what they said. It shows in the peek on Needs cover, so the row never repeats it.
  • Pass date for a list across days, like a month of Mondays.

Avoid it for

  • Your own undo timer for the ask, the reminder, or Withdraw. Each waits out its window in the shared result engine, so ⌘Z takes it back.
  • Saying who dropped out in detail as well. The peek already holds it.
  • Touch-only screens with no other way to swap or withdraw. A tap on the row shows its hover actions, but offer them on the duty's own page too.

Props

  • duty

    The duty, like "Refreshments" or "Setup".

    string

  • icon

    The duty's icon, in ink. Default a clipboard.

    ReactNode

  • detail

    What it involves, like "Chairs and screens before 9".

    string

  • date

    A date column, for a list across days, like "Mon 5".

    string

  • person

    Who is on it, or who was asked.

    { person: string; photo?: string } | null

  • state

    Where it stands. Default confirmed with a person, open without one.

    "confirmed" | "asked" | "cover" | "open"

  • out

    Who dropped out and what they said, for the peek on Needs cover.

    { person: string; photo?: string; said?: string }

  • suggest

    The best person to ask, why, and the first message they get. Shows as a Suggestion while the duty needs someone.

    { person: string; photo?: string; reason?: string; message?: string }

  • onAsk

    Asks the suggested person once the undo window ends. A failed promise turns the chip into Retry.

    (person: { person: string; photo?: string; message?: string }) => void | Promise<unknown>

  • onFindCover

    The fix for Needs cover and Nobody yet.

    () => void

  • onRemind

    Reminds the person who has not replied, once the undo window ends.

    () => void | Promise<unknown>

  • onWithdraw

    Withdraws the ask once its undo window ends. Tell the person there.

    () => void | Promise<unknown>

  • onSwap

    A hover action on a confirmed duty.

    () => void

  • divider

    A hairline above the row, inset from the card's edges.

    boolean · default false

  • index

    Place in a list. Rows fade up one after another as they first show.

    number

  • className

    Extra classes. Later classes win.

    string

Accessibility

  • The duty, its detail, who is on it, and the status are all text, in reading order.
  • Each status is words in its tone's soft text, which the theme holds to 4.5:1. The tones share colors, so the words say which.
  • Each fix is a native button named for what it does, like "Remind Tom Hale", with the same words in its tooltip.
  • Needs cover is a peek trigger. Hover, focus, or a tap opens who dropped out, and Esc closes it.
  • The hover actions stay in the Tab order, and focus inside them shows the bar.
  • The ask, the reminder, and Withdraw each say what happened once, through the page's one polite live region.

Examples

Confirmed, waiting for a reply, and needs cover

<Card className="gap-0 py-1.5">
  <DutySlot duty="Refreshments" icon={<Coffee />} detail="Morning break and lunch" person={{ person: "Sue Hale" }} onSwap={swap} />
  <DutySlot
    duty="Setup"
    icon={<Monitor />}
    detail="Chairs and screens before 9"
    person={{ person: "Tom Hale" }}
    state="asked"
    onRemind={() => remind("setup")}
    onWithdraw={() => withdraw("setup")}
    divider
  />
  <DutySlot
    duty="First aid"
    icon={<HeartPulse />}
    state="cover"
    out={{ person: "Mark Clarke", said: "Can't do it · Tue 29 Sep" }}
    suggest={{ person: "Cal Nash", reason: "First aid trained, and free on Thursday" }}
    onAsk={(who) => ask("first-aid", who)}
    onFindCover={() => openCover("first-aid")}
    divider
  />
</Card>