# Person menu

The signed-in person and what they can do about their account: a header with their initials, name and email; groups of choices (the theme, the app's own) each a radio group with the current one checked and none chosen for them; then Profile, Keyboard shortcuts, Help, the app's own entries and Sign out, each only when the app wired it. A person is drawn as a person: nothing here names an agent.

`form="avatar"` (the default) is the top bar's: the person's initials in a circle, on a button named "{initials}, {name}, account menu" ("PS, Priya Shah, account menu"), so its name starts with what it shows. `form="row"` is a sidebar's foot: the initial, the name, the role (or email) and a chevron, as AppSidebar draws it, whose foot is this menu. The menu opens through `LayerPortal` (DropdownMenu's), so it sits above the app's own dialogs, Radix's included, and focus goes back to the button when it closes.

- Registry name: `person-menu` (`@candor/person-menu`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/person-menu

## Install

```bash
npx candor-ui add person-menu
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/person-menu`.
It brings in: dropdown-menu, initials, locale, motion, theme-setting, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { Download } from "lucide-react"
import { PersonMenu } from "@/components/candor/person-menu"
import type { ThemeChoice } from "@/components/candor/theme-setting"

const sam = { name: "Sam Rivera", email: "sam@acme.example", role: "Admin" }

// Each entry shows only when the app wires it. The avatar form is the top bar's; the row form is a sidebar's foot
// (AppSidebar's foot is this menu).
export default function Example() {
  // The app keeps the theme and puts it on the page; the menu only shows and changes the choice.
  const [theme, setTheme] = React.useState<ThemeChoice>("light")
  return (
    <div className="flex flex-wrap items-start gap-8">
      <div className="flex h-14 w-80 items-center justify-end gap-2 rounded-lg border bg-chrome px-3">
        <span className="me-auto text-xs text-muted-foreground">Top bar</span>
        <PersonMenu
          user={sam}
          onProfile={() => {}}
          onShortcuts={() => {}}
          theme={{ value: theme, onValueChange: setTheme }}
          help={{ href: "#help" }}
          items={[{ id: "export", label: "Export my data", Icon: Download, onSelect: () => {} }]}
          onSignOut={() => {}}
        />
      </div>
      <div className="flex h-40 w-60 flex-col justify-end rounded-lg border bg-sidebar p-2">
        <PersonMenu user={sam} form="row" onProfile={() => {}} onSignOut={() => {}} />
      </div>
    </div>
  )
}
```

## <PersonMenu>

Props:

- `user: PersonMenuUser`: The signed-in person.
- `onProfile?: (() => void) | undefined`: Adds Profile; called when it's chosen.
- `onShortcuts?: (() => void) | undefined`: Adds Keyboard shortcuts; called when it's chosen.
- `onSignOut?: (() => void) | undefined`: Adds Sign out, last; called when it's chosen.
- `help?: PersonMenuHelp | undefined`: Adds Help: a link (`href`, opened as a link) or your own handler (`onOpen`).
- `items?: PersonMenuItem[] | undefined`: The app's own entries, after Help and before Sign out, each drawn as text.
- `theme?: PersonMenuTheme | undefined`: Adds the person's theme as a group of choices (Light, Dark, High contrast), the current one checked.
- `choices?: PersonMenuChoices[] | undefined`: The app's own groups of choices, after the theme, each a radio group with the current one checked.
- `form?: "avatar" | "row" | undefined` (default `"avatar"`): `avatar` (the default): initials in a circle, for a top bar. `row`: the initial, name and role, for a sidebar's foot.
- `compact?: boolean | undefined` (default `false`): In the row form, only the initial shows (a collapsed sidebar); the name and role stay in the button's name.
- `side?: "top" | "bottom" | "left" | "right" | "inline-start" | "inline-end" | undefined`: Which side of the button the menu opens on. Defaults to "bottom" for the avatar and "top" for the row.
- `align?: "start" | "center" | "end" | undefined`: How the menu lines up with the button. Defaults to "end" for the avatar and "start" for the row.
- `labels?: Partial<Record<keyof typeof personMenuStrings, string>> | undefined`: Words for this menu over LocaleProvider's `personMenu`, for an item that keeps its own table (AppSidebar passes its `appSidebar` words, so overrides set there still apply).
- `wrapTrigger?: ((trigger: React.ReactElement) => React.ReactNode) | undefined`: Wraps the button, for an item that adds something round it (AppSidebar's tooltip while it is collapsed). It gets the button and returns what to draw.
- `layerClass?: string | undefined`: Classes for the menu's popup, which is portalled; Candor's own layer class is always kept.
- `className?: string | undefined`: Classes for the button, merged with Candor's.

## Strings (`personMenu`)

The fixed words, English by default. Override any of them under `personMenu` in `<LocaleProvider strings={{ personMenu: { … } }}>`; the table is exported as `personMenuStrings`.

- `accountMenu`: {name}, account menu
- `withInitials`: {initials}, {label}
- `profile`: Profile
- `keyboardShortcuts`: Keyboard shortcuts
- `help`: Help
- `signOut`: Sign out

## Types

### PersonMenuUser

The signed-in person: their name, their email, and their role when the app has one to show in place of the email.

```ts
{
  /** As the person wrote it; its first letters make the avatar's initials. */
  name: string;
  /** Shown under the name in the menu, and in the row form when there is no `role`. */
  email: string;
  /** Shown in the row form in place of the email ("Admin"). */
  role?: string | undefined;
}
```

### PersonMenuItem

One of the app's own entries in the menu, drawn as text.

```ts
{
  /** A key for the entry. */
  id: string;
  /** The entry's words, drawn as text. */
  label: string;
  /** An icon before the words. */
  Icon?: LucideIcon | undefined;
  /** Called when the person chooses it. */
  onSelect: () => void;
}
```

### PersonMenuChoices

A group of choices in the menu, one of them current, drawn as a radio group under its label (a density, a language). Nothing is chosen for the person: with `value` null, none is checked.

```ts
{
  /** A key for the group. */
  id: string;
  /** The group's name, shown above its choices and read as the group's name. */
  label: string;
  /** The current choice's value, as the app last stored it, or null when none is chosen. */
  value: string | null;
  /** The choices, in order. */
  options: {
    value: string;
    label: string;
  }[];
  /** Called with the value the person chose. */
  onValueChange: (value: string) => void;
}
```

### PersonMenuTheme

The person's theme in the menu: ThemeSetting's choices (and its words, under `themeSetting`), as a radio group.

```ts
{
  /** The person's theme as the app last stored it. Left out, none is checked. */
  value?: ThemeChoice | undefined;
  /** Called with the theme the person chose; the app keeps it and puts it on the page, as for ThemeSetting. */
  onValueChange: (next: ThemeChoice) => void;
  /** Which themes to offer, in order. Defaults to light, dark and high contrast; add "system" for "Match my system". */
  options?: ThemeChoice[] | undefined;
}
```

### PersonMenuHelp

Where people get help: a link, or the app's own handler. `label` defaults to "Help".

```ts
{ href?: string | undefined; onOpen?: (() => void) | undefined; label?: string | undefined }
```

## Motion

- **tint** (tint): The avatar button dims a little when the pointer is over it, and a ring fades in on keyboard focus. The menu opens as DropdownMenu's does.

Set with `--cd-motion-person-menu-<moment>-*` in your brand file; the families are in the design reference.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
