Skip to content
txbrendan/ui

Smart Pick

beta

A follow-up question that opens where a change landed, with the app's best guesses first and a search for anyone else.

Preview

search

Who should review this?

Ticket 482 by Ava Patel

SLSam LeeReviewed the last two
APAva PatelFiled the ticket
MCMark ClarkeOwns sign-in

guesses

bump

17

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/smart-pick

Use it for

  • Ask the one thing a change left open, right where it landed, like who should review a ticket that was just filed.
  • Rank suggestions with what the app already knows, best first, and say why in reason, like "Reviewed the last two".
  • The 1, 2, and 3 keys pick the first three guesses from anywhere on the page. Each guess's tooltip teaches its key. The keys are never printed.
  • Pass options, the full list, to add a search field for anyone else.
  • Show it, and what replaces it once picked, inside <PanelSwitch effect="fade">, so the height glides instead of jumping.
  • onDismiss runs on Esc and on the close button. Leave the answer empty then, and let people fill it in later.
  • DeltaBump, from the same file, shows a change like "+5" rising from the value it changed. Give it a new key to play it again.

Avoid it for

  • A question that must be answered before going on. Use a Dialog.
  • More than three or four suggestions. Past three, they have no key, and the guess is not a guess.
  • Opening it on page load. It follows a change the person just made.
  • Two open at once. Only the newest hears the number keys.

Props

  • title

    The question, like "Who should review this?". It names the group and the list.

    string

  • hint

    A quiet line under the question, like "Ticket 482 by Ava Patel".

    string

  • suggestions

    Required. The best guesses, best first. Each is { id, label, reason?, leading?, keywords? }. The first three answer to 1, 2, and 3.

    SmartPickOption[]

  • options

    The full list. With it, a search field finds anyone, matching label and keywords.

    SmartPickOption[]

  • onPick

    Runs with the option picked by a press, Enter, or 1 to 3.

    (option: SmartPickOption) => void

  • onDismiss

    Runs on Esc, with no search typed, and on the close button. Leaves the question unanswered.

    () => void

  • placeholder

    The search field's placeholder. Its name drops the ellipsis.

    string · default "Someone else…"

  • SmartPickOption.leading

    Before the name, like an avatar or initials.

    ReactNode

  • DeltaBump

    Part. "+5" rising from the value it changed, then gone. Put it in a relative box around the value.

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

  • tone

    On DeltaBump. The change's color.

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

  • className

    Extra classes on the box. Later classes win, so className='p-2' replaces the padding.

    string

Accessibility

  • It is a group named by its title. The guesses are a listbox of options, and each keyed guess carries aria-keyshortcuts.
  • It opens with focus left where it was. 1 to 3 and Esc reach it from anywhere on the page, but not from a field that types text, or a menu or dialog opened above it, or while it is inert.
  • With a search field, the field is a combobox: ↑ and ↓ move the highlight, Enter picks, and aria-activedescendant points at the highlight. Without one, the list is the Tab stop and does the same, with Home and End too.
  • The pointer and the keys share one highlight. A finger does not highlight.
  • Esc clears a typed search first, then leaves the question unanswered. It stops there, so a Sheet around it stays open.
  • "No one matches" is said through a status message.
  • DeltaBump is hidden from screen readers. Say the change where the value is.
  • Option text is accent-foreground on the accent fill when highlighted, a pair the theme tests hold to 4.5:1 in light and dark.

Examples

Pick, then show the answer in place (needs @txbrendan/panel-switch)

"use client";

import { useState } from "react";

import { PanelSwitch } from "@/components/ui/panel-switch";

function Reviewer({ people }: { people: SmartPickOption[] }) {
  const [reviewer, setReviewer] = useState<SmartPickOption | null>(null);
  const [open, setOpen] = useState(true);
  return (
    <PanelSwitch effect="fade" value={open ? "pick" : "done"}>
      {open ? (
        <SmartPick
          title="Who should review this?"
          hint="Ticket 482 by Ava Patel"
          suggestions={people.slice(0, 3)}
          options={people}
          onPick={(option) => {
            setReviewer(option);
            setOpen(false);
          }}
          onDismiss={() => setOpen(false)}
        />
      ) : (
        <p>Reviewer: {reviewer?.label ?? "Not set"}</p>
      )}
    </PanelSwitch>
  );
}

Guesses only, with no search

<SmartPick
  title="Who owns this incident?"
  suggestions={[
    { id: "tom", label: "Tom Hale", reason: "On call this week" },
    { id: "jo", label: "Jo Okafor" },
  ]}
  onPick={(option) => save(option.id)}
/>

A value that bumps by +2

<span className="relative">
  19
  <DeltaBump key={bumps} value="+2" tone="success" />
</span>