import type { CategoricalIndex } from "@charcuterie/tokens"; import type { ComponentPropsWithRef, ReactNode } from "react"; import type { IntentAppearance } from "../intentStyles.ts"; export type AvatarSize = keyof typeof AVATAR_SIZE_CLASS; export type AvatarProps = Omit, "children"> & { /** * The same three the rest of the categorical family wears, minus * `ghost` — a chip with no fill and no border is a floating * letter, and the whole job here is to be a **shape** the eye * finds in a list. */ appearance?: Exclude; /** * The colour, when somebody has chosen one. Omitted, it is derived * — see `categoricalKey` — so an avatar always has a colour and * never has to be given one. */ categorical?: CategoricalIndex; /** * What the derived colour is hashed from when `categorical` is * absent. Defaults to `name`. * * Pass the **user's id** wherever one exists. `getCategoricalIndex` * does no normalization on purpose, so hashing the name means a * rename — or a trailing space — silently repaints a person the * reader had already learned to recognise. An id does not move. */ categoricalKey?: string; /** * A glyph instead of the initials — a robot for an agent account, * a lock for a service account. The library ships **no icons**, so * this is the app's own * ([decision](../../../docs/decisions/2026-07-29-ship-no-icons-and-no-symbol-glyphs.md)). * It is decoration: the name still travels in `aria-label`, and a * glyph the font cannot render must not be the only thing * carrying who this is. */ icon?: ReactNode; /** * A photo. Optional, and **it is never the only thing** — a * request that 404s, a signed URL that expired, or an app that * simply has no photos falls back to the initials with no empty * box in between. */ imageUrl?: string; /** * The letters, when the derivation is wrong. Derived from `name` * otherwise: the first character of each of the first two * whitespace-separated words, upper-cased. * * Worth passing for a name the split gets wrong — a mononym that * wants two letters, a name whose family part comes first, a * handle with punctuation in it. */ initials?: string; /** * Who this is. Read by assistive technology and shown on hover; * **never printed as visible text**, which is the entire point of * the component. * * Nullable, and that is a decision rather than a loose type — see * the note on the component about what an unassigned row draws. */ name: string | null | undefined; size?: AvatarSize; }; /** * One rule: **the circle is 1.9 x its own type size**, and `size` * picks which step of the type scale that is. * * 1.9 rather than a tighter number because a circle only offers its * full width on the centre line. Two capitals measure about 1.25em * in this face, and a chord taken at the cap height of a 1.75em * circle is barely wider than that — measured, not guessed. * * It is em-based rather than a fixed `size-5` / `size-7` / `size-9`, * and that is the whole of the design. Every length in this token * set moves with the density axis — `--font-size-xs` is 0.88rem * compact, 0.94rem at desktop and 1.11rem on the kiosk — so a chip * pinned to 20px holds two letters at one density and clips them at * the next. `controlStyles.ts` says the same thing about badges and * dots: non-control sizing is type-relative on purpose, because a * chip that stays 20px while the kiosk scales the prose around it * reads as a rendering fault. * * The hand-rolled avatar this replaces is the worked example. It was * `size-5` with `text-[0.625rem]` — a 10px font, which is **not a * step of this type scale at any density**, because 20px is not * enough room for two characters at the smallest size the tokens * actually offer. A component that has to invent a font size to fit * its own box has the wrong box. */ declare const AVATAR_SIZE_CLASS: { readonly sm: "size-[1.9em] text-xs"; readonly md: "size-[1.9em] text-md"; readonly lg: "size-[1.9em] text-xl"; }; /** * A person, as a coloured chip — a glyph or an initial, with the * name attached where only assistive technology and a hover can * reach it. * * It exists to delete a printed name. Docket's board put the word * *"Owner"* on most of its cards, which is a column of identical * text doing the work of a colour; the fleet's boards all want the * same chip, and `Board.stories.tsx` had already hand-rolled one * inside a story file. * * ### Unassigned draws **nothing**, and the component owns that * * `name` accepts `null` and `undefined`, and either renders `null`. * A grey *"None"* placeholder is a decision the design system makes * against the reader: it is a chip that says a fact nobody needed, * repeated down every unassigned row, at the same weight as the * chips that mean something. * * The alternative is a ternary at every call site, and the call * sites are the whole problem — a board, a table, a card footer, a * detail pane, each written on a different day. The one that forgets * is the one that ships the grey circle. Absence is expressed once, * here, which is the same call `Board` makes with `onMove`: no * handler means no affordance, rather than a disabled one. * * A consumer that genuinely wants to *say* "unassigned" still can — * that is text, or an `EmptyState`, and neither of them is an * avatar. * * ### The name is never visible text * * `role="img"` with an `aria-label`, exactly as `Swatch` does, so * `getByRole("img", { name })` is the one handle whether the chip is * showing a photo, a glyph or two letters. `title` carries the same * string for a pointer, which is the only channel a person browsing * with their eyes has — the letters `KG` are not a name. * * That is also why there is no `isNameVisible`: an avatar whose name * is printed beside it is a name with a decoration in front of it, * and the layout that wants one should print the name and skip the * chip. * * ### The colour is derived, never invented * * `categorical` is the ten-wide non-semantic scale — the same one * `Badge` takes — and **not** an `intent`. An intent is a claim the * design system makes; a person is not `danger`. With no * `categorical` the index comes from `getCategoricalIndex`, which is * a pure function of the key, so the same person is the same colour * on every machine, every reload, and in a server render. Ten * colours means collisions past ten people, and that is a property * of the palette rather than a fault in the hash: the colour helps * you *scan*, and the accessible name is what identifies. * * ### There is no `AvatarGroup` * * Deliberately. A stack of overlapping chips with a `+3` needs * answers to which three survive, in what order, and what the * overflow announces — and no consumer has asked the question yet. A * row of `Avatar`s in a flex box is what a multi-assignee list looks * like until one does. */ export declare const Avatar: ({ appearance, categorical, categoricalKey, className, icon, imageUrl, initials, name, size, title, ...spanProps }: AvatarProps) => ReactNode; export {}; //# sourceMappingURL=Avatar.d.ts.map