a person defined once: name, handle, picture and colour, the same everywhere
npx shadcn add @ag/personAlready installed? Preview an update
npx shadcn add @ag/person --dry-runYour installed source stays unchanged until you update it. How updates work
Latest changes · 2026-10-10
- New:
short, the name where room is short (a caption's speaker, a chip, a mention in running text), andshortName(person), which falls back toname.
handed to every surface
AAda@ada
the demo above, verbatim
import { Sample } from "@/app/samples"
import type { Person } from "@/registry/base-nova/lib/person"
// #region person
const ADA: Person = {
name: "Ada",
handle: "@ada",
}
// #endregion
/** One person, defined once: the name and handle every surface that shows them reads
* (a chat's header, a Mac's lock screen). Without a picture, a surface draws its own
* placeholder, here the initial. */
export default function PersonDemo() {
return (
<Sample
name="person"
glance
title="one person"
description="handed to every surface"
with="person"
>
<div className="flex items-center gap-3">
<span className="flex size-10 items-center justify-center rounded-full bg-foreground/10 font-semibold">
{ADA.name[0]}
</span>
<span className="leading-tight">
<span className="block font-medium">{ADA.name}</span>
<span className="block text-muted-foreground text-xs">
{ADA.handle}
</span>
</span>
</div>
</Sample>
)
}the source, every file an install writes
// A person, as every surface that shows one needs them: a chat's contact, a Mac's
// lock-screen account. Defined once by the page and handed to each component, so the
// same person wears the same name and picture everywhere. A surface with more to say
// about someone extends this (telegram-chat's `ChatProfile` adds `bot`).
// Framework-free.
export interface Person {
/** The display name, in full: "John F. Kennedy". */
name: string
/** The name where room is short (a caption's speaker, a chip, a mention in running
* text): "Kennedy". Absent, `name`. */
short?: string
/** "@adriangalilea": what a mention types and a sub-line shows. */
handle?: string
/** The picture's URL. Absent, a surface draws its own placeholder. */
avatar?: string
/** An animated picture (mp4) that loops muted over `avatar`, where a surface
* animates one. */
avatarVideo?: string
/** The person's own colour, any CSS colour: what a surface that tells people apart
* by colour tints them with (a transcript's speaker). Absent, the surface picks one
* from its palette by order of appearance, so a page that never sets it still gets
* distinct people, and one that does keeps the same colour on every surface. */
color?: string
}
/** The name to draw where room is short. */
export const shortName = (p: Person): string => p.short ?? p.name
/** Distinct, mid-light hues that read on a light page and a dark one alike: the
* palette a surface picks from for a person who states no `color`. One palette, so a
* chat and a transcript of the same people tint them the same. */
export const PERSON_PALETTE = [
"oklch(0.64 0.15 250)",
"oklch(0.66 0.15 40)",
"oklch(0.64 0.14 150)",
"oklch(0.62 0.17 320)",
"oklch(0.70 0.13 90)",
"oklch(0.64 0.12 200)",
] as const
/** Each key's colour: its person's own, else the palette's by the key's first place in
* `order` (the order people first appear in, so the first always wears the first hue).
* Repeats in `order` are fine; only the first place counts. */
export function personColors(
order: readonly string[],
people: Record<string, Person | undefined>,
): Record<string, string> {
const out: Record<string, string> = {}
;[...new Set(order)].forEach((key, i) => {
out[key] =
people[key]?.color ??
(PERSON_PALETTE[i % PERSON_PALETTE.length] as string)
})
return out
}