Entity mark
canonThe mark of any record: tone says which world the type belongs to, glyph says the exact type, shape says person / company / thing. One vocabulary for over twenty types.
Replaces seven parallel vocabularies (chip tile, palette glyph, listing tone, nav icons, ad-hoc header glyph, and the two picker maps in MentionInput and EntityPicker, which contradicted the canon — deal as businessplan, product rose in one picker and grey in the next, a tone on people and companies at all). Seven tones cannot cover twenty types, so the tone groups and the glyph identifies. Glyph size keeps the parity of the box — an odd difference puts the drawing on a half pixel.
Used in 21 files · 17 areas · 31× — components/assistant, components/deal, components/dialog, components/entity-preview, components/projects, components/search …
The identity mark of any record, not just a person or a company. React packages/ui/src/components/EntityMark.tsx, primitive .acrm-mark in apps/web/src/ds/entity-mark.css, vocabulary packages/ui/src/lib/entity-mark.ts.
It replaced seven parallel vocabularies the app kept for the same types: the tile inside the reference chip, the bare glyph in the ⌘K palette, the coloured glyph in listings, the navigation icons, whatever a detail header happened to draw, and — found during the cleanup — the pickers: MentionInput and EntityPicker each carried their own type→glyph/tone map, shared by the product picker, the record select and the link field.
They did not merely differ, they contradicted each other: a deal was a green coin in a chip, a grey briefcase in the palette and a businessplan glyph in the @ mention; a product was rose in one picker and grey in the next; and a person or a company was given a tone at all — which the system reserves for things, because a person's colour is the hue of their name. Now there is one answer and every surface reads it from the same place.
Two layers, read in this order. What is this? — shape, tone and glyph. Which one is it? — photo, logo or initials, and only a person or a company has that. The mark adds the first layer; avatar still owns the second.
When to use
- Wherever a record is named: detail header, list row, ⌘K palette row, reference in text, preview card, rail item.
- For a person or a company too — the mark delegates to
<Avatar>, so one call site handles a mixed list without branching on the type.
When not to use
- As a status or a priority marker. The mark says what a record is, never how it is doing.
- As the whole click target — the mark is a mark, the name is the label.
- For a UI action (filter, sort, settings). Those take a plain
<Icon>; a coloured tile would promise a record that is not there.
Anatomy
span.acrm-markplus the size class when it is notsm; inside, one<Icon>from the DS set.- Tone = world, glyph = type. Seven
--ic-*tones cannot cover twenty-seven types, so the tone groups (money · work · time · content · catalogue · property · selection) and the glyph identifies. Someone who learns the worlds knows where they are; the glyph says exactly what. - Shape: circle = person, square = company, tile = thing. Nothing else is encoded in shape.
- Sizes
inline16 ·sm24 (default) ·lg32 — the same ladder as the avatar, to the pixel, so a mixed row never jumps. - The glyph shrinks with the tile (12 · 16 · 20 px) and its stroke thins with it (1.9 · 1.7 · 1.5 in viewBox units = 0.95 · 1.13 · 1.25 px on screen). White drawing on a saturated ground gains weight optically; a constant stroke made the big tile clumsy and the small one mush.
- Glyph size keeps the parity of the box. An odd difference puts the drawing on a half pixel and the stroke blurs asymmetrically — measured at 4.5 px offset before the fix.
- Six Tabler glyphs are not centred inside their own viewBox; deviations of at least one unit are corrected in
GLYPH_NUDGE(vehicle, budget). Smaller ones are left alone on purpose: the correction is expressed in viewBox units and would land off the pixel grid.
Best practices
- Add a new type to
ENTITY_MARK— one row — before drawing it anywhere. Drawing a tile the vocabulary does not know is how the parallel vocabularies were born; a missing type is one line, a private drawing is forever. - Let the mark decide between avatar and tile; do not branch at the call site.
- Pass
seed(andphoto) whenever the call site already holds the user row. The face of a colleague is stored (users.avatar_seed), not derived: without the seed the mark falls back to the hash of the NAME, so a regenerated face would show only to the person who pressed „Regenerate" and everyone else would keep seeing the old one. - Keep the mark and the name together; the mark alone is not a label.
- Re-implement a tile per screen. That is exactly what produced five vocabularies.
- Read
--av-huein your own CSS rule. The hue derived from a name belongs to.acrm-avatarand nothing else;apps/web/src/ds/__tests__/identity-mark-kanon.test.tsfails the build otherwise. The recipe „circle +hsl(var(--av-hue) …)+ initial" spread by copying between CSS files and neither lint nor the visual baseline caught it — a fresh copy looks almost like the canon. - Invent a tone for a new type. Put the type in an existing world, or bring a world to the AD.
- Use a tone to mean anything but the world — not health, not state, not priority.
Content
- An unknown type renders a visible grey question mark. It used to fall back to
dealsilently, which meant a wrong glyph rather than a missing one. - Historical names of the same thing resolve to one mark:
contact= person,organization= company,location= place.
Related
avatar— the identity layer for a person and a company.user-avatar— our own person, drawn as a generated face so a colleague is not a contact.entity-ref,item-list,section-top-bar— the usual hosts.- Stories:
components--entity-mark--default,--variants,--sizes,--states,--composition.
Source: docs/avatar/entity-mark.usage.md · GitLab