FacilityPicker

FacilityPicker is how a page asks "which facility?". It is the one component in this section that does not live in packages/azucar/src/ui/. It knows what a facility is and fetches its own list, so it is an app component, not a primitive. Pages mount it and read value. They do not rebuild the popup.

import FacilityPicker, {
type FacilityCode,
type FilterFacilities,
} from "@haloid/azucar/facility-picker";

Anatomy

Nothing here is new drawing. The picker is three documented parts plus the data:

  • Menu gives the trigger pill and the floating popup.
  • FilterList gives the scrolling list of codes, the type-to-filter behavior, and the scroll fades. The picker is the home consumer of that component and uses every prop it has.
  • ButtonWithInput is the search affordance: a magnifying glass that expands into an input and slides the Facility label out of the header.

Open the picker below and type a few letters. The list is 32 facilities in a 136px scroll window. The selected facility is anchored into view on every open, so you do not have to find it.

filter options
function FacilityScope() {
const [facility, setFacility] = useState("TOR5");
return (
  <div className="flex flex-col items-start gap-4">
    <FacilityPicker showFilterFacilities={false} value={facility} onChange={setFacility} />
    <div className="text-[14px] text-fg-2">Scoped to {facility}</div>
  </div>
);
}

render(<FacilityScope />);

Value

The picker is controlled. value is the current selection and onChange reports the next one. There is no internal selection state to fall out of sync.

showFilterFacilities is not a cosmetic flag. It discriminates the props union, so it widens value and onChange together:

// false: the page can only ever hold a facility code
<FacilityPicker showFilterFacilities={false} value={code} onChange={setCode} />
// true: "all" | "usa" | "canada" join the value space
<FacilityPicker showFilterFacilities value={scope} onChange={setScope} />

A page that has not opted in cannot receive "usa". The compiler enforces that, not the reviewer. Turn the control above on and watch the readout: the group options report through the same onChange as a code does, which is why they must be in the type.

FacilityCode is a string alias, not a literal union. Codes are runtime data from the API, so there is no exhaustive list to narrow against. FilterFacilities is a literal union, because those three options are authored in helpers.ts.

The group options render below the separator, outside the FilterList. They are scope actions, not search results. A query filters the facility codes and never touches them, and they never scroll away.

Data

The picker owns its data. useFacilities fetches GET /api/facilities once on mount. Call sites pass no options. Until the request resolves the list is empty, and the items fade in when the data lands. A failed request is swallowed and leaves the list empty. That is the only failure mode of the picker and its main limit today: there is no error state and no way to inject facilities from the outside.

Reopen state

When the popup closes, the picker clears the query and remounts the search input collapsed. It does this through a key bump, not through the blur path of the input, which is unreliable while the value is non-empty. So every open starts the same way: no query, the label visible, and anchor={value} resting the selected facility 20px below the top of the scroll window. The scroll is instant and lands on the first painted frame, because the popup stays mounted and the anchor applies on the hidden-to-visible transition.

Placement

The trigger is a Menu.Trigger pill with px-3 of its own. When it sits at the left edge of a page column, pull that padding back so the label lines up with the content below it:

<div className="-ml-3">
<FacilityPicker showFilterFacilities value={facility} onChange={setFacility} />
</div>

That is the first element of the reconciliation overview, and the pattern to copy.

API

type FacilityCode = string; // runtime data from the API
type FilterFacilities = "all" | "usa" | "canada"; // authored in helpers.ts
type Props =
| {
showFilterFacilities: false;
value: FacilityCode;
onChange: (val: FacilityCode) => void;
}
| {
showFilterFacilities: true;
value: FacilityCode | FilterFacilities;
onChange: (val: FacilityCode | FilterFacilities) => void;
};

showFilterFacilities is required. There is no default, because the flag is what makes the other props typecheck.

Limits

  • Escape inside the search input clears and collapses it, and still bubbles, so it closes the whole popup too. This is inherited from Menu.
  • When you hover an item that then filters out, the shared hover pill goes with it. The pill reappears on the next hover.
  • Matching is a plain substring test over the facility code. Country is not searchable, so "canada" finds nothing. The group options exist for that.