Components / ScopeAvatar
ScopeAvatar
The mark for something the user OWNS and switches between — a project, an organization. Its real icon when we hold one, otherwise its first letter on a brand pastel. Use PlatformTile for a third-party provider (that is a vendor’s brand, not the customer’s thing) and AccountIdentity for a social account.
import { ScopeAvatar } from '@layers/ui/scope-avatar'Rendered stock
House rules
- The tone is a hash of the NAME, so a project keeps the same colour across sessions, machines and surfaces. That is what lets someone recognise it in the rail and again in the switcher without reading it. Two things sharing a name therefore share a tone — correct, not a collision to design around.
- Colours come from the five brand pastels in PALETTE. This replaced a raw `from-amber-400 to-rose-500` gradient, which was the last raw palette utility in the app and a colour from outside the brand set entirely.
- The letter takes --ink, never white. The pastels are light, and --ink is the pairing theme.css already uses for every bright surface (--success, --warning, --info).
- Squircle, matching the rail chips and app icons: `rounded-2xl [corner-shape:squircle]`, with the radius authored rather than inline-styled (see tokens.md).
- A broken or stale `iconUrl` falls through to the letter rather than rendering a broken image, and the failure resets when the URL changes so switching entities re-tries. A surface that shows a torn image instead of a mark is worse than one that never promised the image.
- An icon is CONTAINED, never covered. A square icon fills the box identically either way, so the difference only shows on the marks that matter: under object-cover a wordmark had its ends cropped off, and an avatar of a brand that cuts the brand’s name in half has stopped doing its job.
- backdrop is the colour painted behind a contained icon, and it comes from storage (media_library.metadata.uiBackdrop) rather than from the component. It is recovered from the mark’s own uniform border, or chosen for contrast against its ink when the mark is transparent. Pass it and a logo whose border was one colour still reads FULL BLEED — the ground matches its own edge, so there is no visible seam. That is also why there is no inset: padding would break the full-bleed case for nothing.
- No backdrop takes the muted ground, and that is deliberate rather than a gap. The resolver returns null when no single colour is honest for a mark, and a component that invented one would defeat the point of it having said so — this colour is drawn edge to edge behind someone’s brand.
- The colour is NOT baked into the stored image, and the repo already learned why. renderOnSquareCanvas composited a sampled background into every logo: a white letterbox on one upload and a black one on the next, and 2,427 of 3,160 production logos still carry that plate where nothing can remove it and every image model reading the logo learns the letterbox is part of the brand. Storage keeps the mark; the UI paints the ground.
- Sizes are sm/md = 24/32px. sm is for list rows, md for a header or rail row.
- fallback="muted" is for a surface that KNOWS an icon is coming — a flat muted block instead of a pastel and a letter. The letter is the permanent mark for a scope that may never have a logo (the rail, the switcher); a saturated stand-in that appears for two seconds and then vanishes is the most jarring thing on a board. Only the surface waiting on a known icon should pass it.