Skip to content
txbrendan/ui

Peek Card

beta

The standard content of a peek: who or what, status, facts, the people behind a count, fixes, a note, and next actions.

Preview

person

Josh Tait

Out sick

Engineer · Engineering

Back
Mon 19 Oct · told HR on Fri
Cover
Ruth Gale
Next
Team sync, Thu 10:00
Profile

people

6 have not replied

Workshop · Tue 29 Sep · 19:00

Engineering

  • Josh TaitBackend
  • Tom HaleFrontendReminded at 18:04
  • Leo BrandtMobileAway

Design

  • Mia OkaforProduct
  • Ana RuizResearch
  • Ben MossBrand
See all 21

2 to approve

New members · this week

To approve

  • Priya ShahJoined Mon
  • Cara LyleJoined SunApproved · Design

fixes

Two people on call at once

Clash

  • Move Josh to backupKate stays on call.
  • Swap with KateJosh is on call. Kate goes to backup.
Picked by Sam Carter, Thu 18:20.

glance

Next workshop: . The peek shows a glance: 3 facts and 2 actions.

choices

New members: . Esc puts the choices away first, and the next Esc closes the peek.

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/peek-card

Use it for

  • As a HoverCard's content. Its title names the peek, and in a glance it shows up to 3 facts and 2 actions. People, fixes, and the note always show.
  • Facts are [label, value, tone?] rows. Say who and when, like ['Back', 'Mon 19 Oct · HR'], so people know where a fact came from.
  • A fact value that is a PersonValue, like { person: 'Steve Rowe' }, shows as a Person with a photo.
  • people lists the people behind a count, like the 6 who have not replied, each with a status and an action like Remind.
  • fixes lists ways to fix a problem, best first. The first gets the one-press button. detail says what happens.
  • Give a press a result, like { label: 'Sent' }. The button slips to it in place. With onUndo, coming back shows Undo for 8 seconds, and ⌘Z undoes it too. Send the change in onCommit.
  • Return a promise from onClick or onCommit, and give the result failed words. The button waits while it runs, and turns into Retry if it fails.
  • Other controls that leave a result use useActionResult from tool-button, and resultFaces from this file for their faces. undoable, in tool-button, puts a change with no control, like a saved edit, on the page's ⌘Z stack.
  • Put the key in shortcut. It shows in the button's tooltip only. Wire the key itself with ActionKeys.

Avoid it for

  • A long form or a menu of choices. Use a Popover, a Menu, or a Dialog.
  • A toast to repeat what a pressed button already says. The button carries the result.
  • More than one solid action. The main action is the one solid button.
  • A go action as the main action. Going somewhere is never the main action.

Props

  • person

    Who it is about: a name, or { person, photo }. Shows their avatar.

    string | PersonValue

  • photo

    The person's photo, beside a name.

    string

  • icon

    An icon for what is not a person, in a soft tile.

    ReactNode

  • iconTone

    The icon tile's color role.

    "neutral" | "brand" | "danger" | "success" | "warning" · default "neutral"

  • leading

    Your own avatar or icon, in place of person or icon.

    ReactNode

  • title

    Names the peek. Default: the person's name.

    string

  • subtitle

    A muted line under the title, like "Workshop · Tue 29 Sep · 19:00".

    string

  • status

    Colored status text at the top right. Neutral is muted.

    { label: string; tone?: "neutral" | "brand" | "danger" | "success" | "warning" }

  • facts

    Label and value rows. A glance shows the first 3.

    [label: string, value: ReactNode | PersonValue, tone?: PeekTone][]

  • people

    The people behind a count. group puts them under a heading. done is the record from app state, shown in place of the action. action can ask a question with choices, like a team.

    { person; photo?; meta?; group?; done?; status?; action? }[]

  • peopleAction

    The action for a row that has none, like Remind. A choice joins the result, like Approved · Support.

    { label; choices?; ask?; onClick?; result?: ActionResult }

  • fixes

    Fixes for a problem, best first. cta labels the button, default "Fix". Once pressed, doneDetail replaces detail.

    { label; detail?; doneDetail?; icon?; cta?; onClick?; result?: ActionResult }[]

  • note

    A short muted note.

    ReactNode

  • actions

    Next actions on a thin lip at the foot. The first is the main, solid on the right, unless one sets emphasis. outline sits just left of it, and ghost on the left. all marks every person whose action has that label as done. dismiss closes the peek after the click.

    { label; icon?; shortcut?; emphasis?; go?; all?; dismiss?; href?; iconOnly?; onClick?; result?: ActionResult }[]

  • more

    The way to the full page. A plain button beside the main action, or a link under the actions when it has href.

    { label?; href?; onClick? }

  • className

    Extra classes on the card. Later classes win.

    string

  • ActionResult

    Type, from tool-button. What a press leaves. label shows once pressed and doneLabel once the window ends. failed is what Retry says. seconds is the window, default 8. toast goes to the toast slot, only for what the button can't show.

    { label; doneLabel?; say?; failed?; onUndo?; undone?; onCommit?; seconds?; toast? }

Accessibility

  • In a HoverCard, the title names the peek's dialog. On its own, the card has no role.
  • A press says its result once, through one polite live region for the page. The button's name follows its state: the label, then the result, with "Press to undo." while a press undoes, then the record.
  • A press that fails is named "Retry:" and what failed, so a voice command for Retry finds it.
  • Undo arms only after the pointer leaves, or 1.2 seconds after a key or touch press, so a double press never undoes.
  • ⌘Z or Ctrl+Z undoes the newest change still in its window. In a text field it stays the field's own undo.
  • An icon-only action is named by its label, which shows in its tooltip. A shortcut key shows in the tooltip too.
  • An action with choices sets aria-expanded and moves focus to the first choice. Esc puts the choices away and focus goes back to the action.
  • Rows with a group sit in a list named by the group heading.

Examples

Who is going

<PeekCard
  title="21 going"
  subtitle="Workshop · Tue 29 Sep · 19:00"
  people={[
    { person: "Sam Carter", meta: "Host", status: { label: "Going", tone: "success" } },
    { person: "Josh Tait", meta: "Engineer", status: { label: "No reply" } },
  ]}
  peopleAction={{ label: "Remind", result: { label: "Sent" } }}
  more={{ label: "See all 21", href: "/events/29-sep" }}
/>

A person, in a peek

<HoverCard
  content={
    <PeekCard
      person="Josh Tait"
      subtitle="Engineer · Engineering"
      status={{ label: "Out sick", tone: "danger" }}
      facts={[["Back", "Mon 19 Oct · HR"], ["Next", "Team sync, Thu 10:00"]]}
      actions={[{ label: "Message" }, { label: "Profile", go: true, href: "/people/josh-tait" }]}
    />
  }
>
  Josh Tait
</HoverCard>

Fixes, best first

<PeekCard
  title="Two people in one slot"
  status={{ label: "Clash", tone: "warning" }}
  fixes={[
    { label: "Move Josh to backup", detail: "Kate stays on call.", cta: "Move", result: { label: "Moved" } },
    { label: "Swap with Kate", detail: "Josh is on call, Kate is backup.", cta: "Swap", result: { label: "Swapped" } },
  ]}
/>

A press that can fail

<PeekCard
  title="Josh Tait"
  actions={[
    {
      label: "Remind",
      onClick: () => sendReminder("josh"),
      result: { label: "Reminder sent", failed: "Couldn't send the reminder", onUndo: cancelReminder },
    },
  ]}
/>