Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | 1x 22x 37x 31x 19x 19x 19x 19x 5x 2x 14x | import { useState } from "react";
import { cn } from "../../lib/utils";
/**
* Avatar — the job book's monogram tile (`.mono`, mockups §7.3).
*
* A studio with a logo shows the logo; a studio without one gets a tile in the
* committed-field colour with its initials printed on it. Square by default
* (2px radius, like every other printed thing); `shape="round"` is for the one
* place roundness means something — a person, in the top bar.
*
* COLOUR: `--primary-600` with `--foreground-inverse`, not the mockup's
* `--action`/`--on-action`. The tile was written while the legacy palette
* override was still in place, and that override themed `primary-600` but not
* `--action`, so `--action` would have painted one laminate-teal tile into an
* otherwise un-reskinned portal. The override is gone and both tokens are now
* the same laminate teal (`primary-600` is #255D52), so this is a free choice
* today rather than a forced one.
*
* Ratios for the pair, text on fill: 6.63:1 light, 7.39:1 dark — AA with no
* `dark:` override. (`primary-500`, which `button.tsx` uses, was only 3.53:1
* on the legacy palette and would not have been.)
*
* ACCESSIBILITY CONTRACT
* - with a `src`: a real `<img>` with `alt={name}`
* - without one (or after the image fails): the tile is `role="img"` with
* `aria-label={name}`, because "AS" read out letter by letter is not the
* name of anything
* - `decorative` drops the name entirely for a tile that sits next to the
* same text, so a screen reader does not say it twice
*/
type AvatarSize = "sm" | "md" | "lg";
/** 1.75 / 2.25 / 3rem — `.mono-sm`, `.mono`, `.mono-lg`. */
const sizeClasses: Record<AvatarSize, string> = {
sm: "size-7 text-xs",
md: "size-9 text-sm",
lg: "size-12 text-base",
};
interface AvatarProps {
/** The name the tile stands for: the label, and the source of the initials. */
name: string;
/** Logo or photo. Falls back to the monogram if it fails to load. */
src?: string | null;
size?: AvatarSize;
shape?: "square" | "round";
/**
* True when the same name is already printed beside the tile. Removes it
* from the accessibility tree rather than repeating it.
*/
decorative?: boolean;
className?: string;
}
/**
* First letters of the first two words, capitalised — "Asha Interiors" → "AI".
*
* Falls back to the first character for a single word, and to nothing at all
* for an empty or punctuation-only name (the tile then prints blank rather
* than a stray glyph).
*/
export function initialsFor(name: string): string {
return name
.split(/\s+/)
.filter((word) => /\p{L}|\p{N}/u.test(word))
.slice(0, 2)
.map((word) => (word.match(/\p{L}|\p{N}/u) ?? [""])[0])
.join("")
.toUpperCase();
}
export function Avatar({
name,
src,
size = "md",
shape = "square",
decorative = false,
className,
}: AvatarProps) {
// The src that failed, not a boolean: a new src is a new chance, and
// storing the URL derives that in render instead of resetting a flag in an
// effect (which would paint the broken image for one frame first).
const [failedSrc, setFailedSrc] = useState<string | null>(null);
const failed = src != null && failedSrc === src;
const shell = cn(
"inline-grid place-items-center overflow-hidden font-semibold tracking-tight select-none bg-primary-600 text-foreground-inverse",
shape === "round" ? "rounded-full" : "rounded-sm",
sizeClasses[size],
className,
);
if (src && !failed) {
return (
<img
src={src}
alt={decorative ? "" : name}
onError={() => setFailedSrc(src)}
className={cn(shell, "object-cover")}
/>
);
}
return (
<span
{...(decorative
? { "aria-hidden": true }
: { role: "img", "aria-label": name })}
className={shell}
>
{initialsFor(name)}
</span>
);
}
|