{
  "schema": "leumas.docs.page/1",
  "id": "pkg:@leumas/avatar",
  "slug": "packages/avatar",
  "kind": "capabilities",
  "bucket": "package",
  "title": "@leumas/avatar — the operator identity, as data",
  "name": "@leumas/avatar",
  "eyebrow": "the operator identity, as data",
  "chip": null,
  "summary": "The operator identity: the renderer-agnostic look contract + slot registry (React-free, shared verbatim by server and client) and the 2.5D operator-card renderers.",
  "keywords": [
    "avatar",
    "renderers",
    "react-free",
    "slot",
    "avatar ui",
    "avatar api",
    "leumas avatar",
    "identity"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# @leumas/avatar — the operator identity, as data\n\nWhat a Leumas user looks like, expressed as a `look` blob that the **server** validates and persists\nand the **client** renders — from one definition, not two. The package ships in two halves to make\nthat literally true: React-free contract modules (`./look`, `./slots`, `./rarity`, `./render`,\n`./frames`, `./auras`) that Node imports byte-identically, and a React barrel a browser only ever\nloads. Zero runtime dependencies; `react`, `react-dom` and `@leumas/ui` are **optional** peers\n(`peerDependenciesMeta`), which is what lets a server package depend on this one and install none of\nthem.\n\nPart of the Operator Deck program — see `ops/todos/dashboard-deck-roadmap.md` (Contracts **C1**, **C2**, **C9**).\n\n## The exports map\n\n| Subpath | Imports | Loaded by |\n|---|---|---|\n| `/slots` · `/rarity` · `/frames` · `/auras` | **nothing** | server + client |\n| `/look` | `./slots.js`, nothing else | server + client |\n| `/render` | `./look.js` + `./slots.js` + `./rarity.js`, and re-exports all three | server + client |\n| `/avatar.css` | — | a bundler, never Node |\n| `.` (the barrel) | React, `@leumas/ui`, every `.jsx` here | **client only** |\n\nEvery consumer in the repo today, and which half it takes:\n\n```js\nimport { normalizeLook, serializeLook, LOOK_MAX_BYTES } from '@leumas/avatar/look';  // engines/progression/src/look.js\nimport { isRenderedSlot } from '@leumas/avatar/slots';                               // engines/progression/src/context.js\nimport { composeLayers } from '@leumas/avatar/render';                               // adapters/domain/avatar/index.js\nimport { RARITY_KEYS } from '@leumas/avatar/rarity';                                 // progression/test/catalog.test.js\nimport { OperatorCard, LookCustomizer, diffLook } from '@leumas/avatar';             // …/dashboard/operator/OperatorStudio.jsx\nimport { OperatorCard } from '@leumas/avatar';                                       // …/dashboard/deck/DeckOperator.jsx\nimport '@leumas/avatar/avatar.css';                                                  // both of the above\n```\n\n**Never delete an entry from that map, and never widen an existing one to reach a `.jsx` file.** Node\nhard-fails an unlisted subpath with `ERR_PACKAGE_PATH_NOT_EXPORTED` the moment an `exports` map\nexists, so dropping `./render` takes the whole `avatar` adapter down: its `index.js` import throws,\n`shared/engines/middleware/src/adapters/registry.js` records the failure, and\n`node ops/infra/scripts/check-adapters.mjs` then fails three ways at once — a non-empty\n`registry.errors`, `avatar` missing from its `EXPECT` list, and the `identicon` probe with nothing to\ncall. Pointing a React-free subpath at JSX is the same failure in slow motion: Node cannot parse\n`.jsx`, so the server dies at import rather than at use.\n\n## The React surface\n\nAll of it behind the barrel, except the last row: those catalogs are plain data and are also exported\nReact-free at `./frames` and `./auras`.\n\n| Export | File | What it is |\n|---|---|---|\n| `AvatarBadge` | `src/AvatarBadge.jsx` | `Avatar` from `@leumas/ui` plus optional decoration. Undecorated it **returns `Avatar` itself**. |\n| `OperatorBust` | `src/OperatorBust.jsx` | Container-sized portrait: aura behind, frame in front, optional ring and rim light. |\n| `OperatorCard` | `src/OperatorCard.jsx` | Bust + nameplate. Composition only — no fetching, no progression maths. |\n| `OperatorFrame` | `src/OperatorFrame.jsx` | The eight frames as inline SVG. `stroke=\"currentColor\"`, `fill=\"none\"`, no colour literal. |\n| `LookCustomizer`, `EDITABLE_SLOTS` | `src/LookCustomizer.jsx` | The controlled picker. Every tile draws the **real** cosmetic. |\n| `FRAMES`, `AURAS`, `frameOf`, `auraOf`, `auraClass` | `src/frames.js`, `src/auras.js` | The catalogs, as plain data. |\n\nThree of those are **delegation, not duplication**, which is the only reason they cannot drift.\n`AvatarBadge` returns the untouched `Avatar` (a copy kept in step by a test drifts the first time\nsomeone edits the kit; `test/badge.test.js` guards the delegation instead), and `LookCustomizer`\ndraws the real cosmetic in every tile, so no preview asset needs keeping in sync. The catalogs sit\noutside their components because the **server** reads them and Node cannot parse `.jsx`:\n`engines/progression/test/catalog.test.js` cross-checks the seeded `cosmetic_definitions` rows\nagainst `FRAME_KEYS`/`AURA_KEYS`, so a row can never name a frame the renderer does not know — which\nwould ship as an equippable cosmetic that visibly does nothing.\n\n`src/avatar.css` owns the entire `lms-op__*` namespace (Contract **C10**) and every custom property\nthe renderers read. Classes are never invented in JS: `auraClass()` returns a class string so the\ncolours and keyframes stay in the stylesheet where the token guard can see them.\n\n## The look, and where its contract lives\n\nA slot value is namespaced. The grammar is Contract **C1** and the parser is `parseSlotValue()` in\n`src/look.js` — do not re-derive either.\n\n- `cat:<cosmeticKey>` — a `cosmetic_definitions` row.\n- `gen:<tool>/<seed>` — a deterministic generator from the **avatar adapter**; allowlist `GEN_TOOLS`.\n- `asset:<ownerId>/<assetId>` — public datacenter storage.\n\nWhich slots accept which namespace is `SLOTS[].accepts` in `src/slots.js`; `acceptsNamespace()` fails\nclosed, and a bare string with no `:` coerces to `cat:`. `normalizeLook()` is pure and idempotent and\n**drops** anything invalid rather than throwing; `parseLook()` never throws at all, so a corrupt or\nhand-edited row renders a default operator instead of blanking a page. Ownership is **not** checked\nhere — the server recomputes it on every equip, and client lock state is presentation only.\n\nThe six 3D slots (`body`, `head`, `outfit`, `prop`, `pose`, `mount`) are already declared, stored and\nnever painted; `validateLook` warns rather than rejecting. That is what makes the deferred 3D\nrenderer a pure addition. **There is no `v: 2`.**\n\n## Four rules you cannot bend\n\n1. **The React-free subpaths import nothing.** No React, no DOM, no CSS, no `@leumas/ui`: `./slots`,\n   `./rarity`, `./frames` and `./auras` import literally nothing and `./look` imports only\n   `./slots.js`. That is what lets `engines/progression` and the `avatar` adapter share one definition\n   with the browser instead of two that agree until they don't.\n2. **`gravatarUrl` is never a valid generator.** It derives a public image from a hash of the user's\n   email, so a rendered avatar becomes an identity oracle — you could confirm whether an address holds\n   an account. It is absent from `GEN_TOOLS` and from the avatar router's own\n   `STYLES = ['boringAvatar', 'identicon', 'blockies']` in `shared/packages/auth/src/routers/avatar.router.js`,\n   and `pnpm smoke:operator` posts `gen:gravatarUrl/x` at the write path to prove it is refused.\n3. **Tint is a security control, not tidiness.** The avatar adapter interpolates palette colours and\n   `options.fontFamily` straight into SVG attributes with no escaping — `escapeXml` guards text\n   content only (`initials`, and `renderCard`'s name/rank/level), never an attribute value — so an\n   arbitrary tint string is an SVG-attribute injection primitive. A tint is a `TINT_TOKENS` member or\n   `/^#[0-9a-fA-F]{6}$/`, and `normalizeLook` drops everything else.\n4. **A cosmetic on disk is `.webp`, never `.svg` and never `.avif`.** `cosmeticUrl()` resolves\n   `cat:<key>` to `<base>/cosmetics/<key>.webp` — the extension is hardcoded — and the library route\n   serves those bytes with no CSP, which makes a same-origin SVG stored XSS. `check-deck-assets.mjs`\n   bans `.svg` outright under `cosmetics/` and `backdrops/`, and holds `cosmetics/` to `.webp` alone:\n   `.avif` stays legal for a backdrop poster, but an `.avif` cosmetic would seed green and then 404 at\n   render. Anything that genuinely needs vectors is **code** here — the eight frames are JSX\n   paths, the auras are CSS rules, and both are seeded with an `assetRef` of `svg:<key>` rather than a\n   file for that guard to resolve. That SVG is emitted **inline**, never as a `data:` URI: custom\n   properties do not cross into a data-URI document, so `currentColor` would resolve to nothing there\n   and the frame would silently vanish.\n\n## Verify\n\n```sh\npnpm --filter @leumas/avatar test         # node --import ./test/register-jsx.mjs --test test/*.test.js — 41 tests\nnode ops/infra/scripts/check-adapters.mjs # the exports map, via the avatar adapter (no root-script alias)\npnpm check:deck                           # bundle purity + the asset/format rules + the built-entry budget\npnpm check:tokens                         # every lms-* class written here must be declared in the corpus\npnpm smoke:operator                       # the look write path end to end, against a running API\n```\n\nThe `--import` flag is not decoration: `test/register-jsx.mjs` registers the loader from\n**`@leumas/test-jsx`** (the package's only devDependency), which transforms `.jsx` with **sucrase**\n(already the transform `@leumas/features` and `@leumas/config-registry` use) and stubs `.css` to an\nempty module. That loader used to be a byte-identical copy in each of the three packages that need\nit — `@leumas/ui`, `@leumas/features` and this one — and is now one workspace package. Without it\nthe contract tests could not import the actual `AvatarBadge` and the actual `@leumas/ui` `Avatar` —\nand a test that re-declares the markup pins itself rather than the component.\n\n`check-deck-bundle.mjs` resolves `@leumas/*` through each package's `exports` map, so it walks in\nhere from the deck: a `three` or `@react-three/*` import anywhere in `src/` fails it. Both it and\n`check-design-tokens.mjs` are repo-wide — read the path in a failure before assuming it is yours.\n`check:deck`'s third script, `check-deck-budget.mjs`, greps the BUILT entry chunk for `lms-op__card`\namong its markers, so it only has anything to say after a production build — with no `dist/` it prints\nwhat is missing and exits 0.\n\n## Caveats\n\n- **`AvatarBadge`, `OperatorBust` and `OperatorFrame` have no consumers outside this package yet.**\n  It was built to replace bare `Avatar` call sites across the dashboard, social and admin over time;\n  that migration has not started. `test/badge.test.js` pins the identity contract in the meantime.\n- **`src/three/OperatorAvatar3D.jsx` does not exist.** `check-deck-bundle.mjs` already exempts that\n  exact path as the one file permitted to reach `three`, behind `await import()` and a quality gate.\n  The exemption is pre-declared for the deferred 3D phase; nothing in `src/` reaches `three` today.\n- **`shared/engines/adapters/domain/avatar` is not this package.** It is a pre-existing deterministic\n  identicon/blockies/initials SVG generator that predates the Operator Deck, it is what the\n  `gen:` namespace resolves through, and it *imports* `@leumas/avatar/render` rather than being it.\n  Any sentence about \"avatar\" has to say which one.\n",
  "source": {
    "path": "shared/packages/avatar/README.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 11181,
    "hash": "30b0212573423d997c9455cf03eafb843c0ff4c0"
  },
  "urls": {
    "html": "/p/packages/avatar",
    "json": "/docs/packages/avatar.json",
    "md": "/docs/packages/avatar.md"
  },
  "links": {
    "composes": [
      "pkg:@leumas/ui"
    ],
    "usedBy": [
      "pkg:@leumas/features",
      "pkg:@leumas/progression",
      "pkg:@leumas/studio"
    ],
    "product": [
      "pkg:@leumas/admin",
      "pkg:@leumas/studio",
      "pkg:@leumas/web"
    ],
    "howTo": [
      "how-to:operator-deck"
    ],
    "skills": []
  },
  "exports": {
    "total": 125,
    "component": 58,
    "hook": 0,
    "helper": 67,
    "names": [
      {
        "n": "AURA_KEYS",
        "k": "component"
      },
      {
        "n": "AURA_KEYS",
        "k": "component"
      },
      {
        "n": "AURAS",
        "k": "component"
      },
      {
        "n": "AURAS",
        "k": "component"
      },
      {
        "n": "AvatarBadge",
        "k": "component"
      },
      {
        "n": "EDITABLE_SLOTS",
        "k": "component"
      },
      {
        "n": "EYE",
        "k": "component"
      },
      {
        "n": "EYE",
        "k": "component"
      },
      {
        "n": "FIELD_BY_KEY",
        "k": "component"
      },
      {
        "n": "FIELDS",
        "k": "component"
      },
      {
        "n": "FRAME_KEYS",
        "k": "component"
      },
      {
        "n": "FRAME_KEYS",
        "k": "component"
      },
      {
        "n": "FRAMES",
        "k": "component"
      },
      {
        "n": "FRAMES",
        "k": "component"
      },
      {
        "n": "GEN_TOOLS",
        "k": "component"
      },
      {
        "n": "GEN_TOOLS",
        "k": "component"
      },
      {
        "n": "GLASS",
        "k": "component"
      },
      {
        "n": "GLASS",
        "k": "component"
      },
      {
        "n": "HAIR",
        "k": "component"
      },
      {
        "n": "HAIR",
        "k": "component"
      },
      {
        "n": "LOOK_MAX_BYTES",
        "k": "component"
      },
      {
        "n": "LOOK_MAX_BYTES",
        "k": "component"
      },
      {
        "n": "LOOK_VERSION",
        "k": "component"
      },
      {
        "n": "LOOK_VERSION",
        "k": "component"
      },
      {
        "n": "LookCustomizer",
        "k": "component"
      },
      {
        "n": "MII_BYTES",
        "k": "component"
      },
      {
        "n": "MII_FIELDS",
        "k": "component"
      },
      {
        "n": "MII_FORMAT",
        "k": "component"
      },
      {
        "n": "MII_KEYS",
        "k": "component"
      },
      {
        "n": "MII_KEYS",
        "k": "component"
      },
      {
        "n": "MII_SEED_LENGTH",
        "k": "component"
      },
      {
        "n": "MII_SEED_LENGTH",
        "k": "component"
      },
      {
        "n": "MiiCustomizer",
        "k": "component"
      },
      {
        "n": "MiiFace",
        "k": "component"
      },
      {
        "n": "MOUTH",
        "k": "component"
      },
      {
        "n": "MOUTH",
        "k": "component"
      },
      {
        "n": "NAMESPACES",
        "k": "component"
      },
      {
        "n": "NAMESPACES",
        "k": "component"
      },
      {
        "n": "OperatorAvatar3D",
        "k": "component"
      },
      {
        "n": "OperatorBust",
        "k": "component"
      },
      {
        "n": "OperatorCard",
        "k": "component"
      },
      {
        "n": "OperatorFrame",
        "k": "component"
      },
      {
        "n": "RARITIES",
        "k": "component"
      },
      {
        "n": "RARITIES",
        "k": "component"
      },
      {
        "n": "RARITY_KEYS",
        "k": "component"
      },
      {
        "n": "RARITY_KEYS",
        "k": "component"
      },
      {
        "n": "SKIN",
        "k": "component"
      },
      {
        "n": "SKIN",
        "k": "component"
      },
      {
        "n": "SKIN_SHADE",
        "k": "component"
      },
      {
        "n": "SKIN_SHADE",
        "k": "component"
      },
      {
        "n": "SLOT_IDS",
        "k": "component"
      },
      {
        "n": "SLOT_IDS",
        "k": "component"
      },
      {
        "n": "SLOT_ORDER",
        "k": "component"
      },
      {
        "n": "SLOT_ORDER",
        "k": "component"
      },
      {
        "n": "SLOTS",
        "k": "component"
      },
      {
        "n": "SLOTS",
        "k": "component"
      },
      {
        "n": "TINT_TOKENS",
        "k": "component"
      },
      {
        "n": "TINT_TOKENS",
        "k": "component"
      },
      {
        "n": "acceptsNamespace",
        "k": "helper"
      },
      {
        "n": "acceptsNamespace",
        "k": "helper"
      },
      {
        "n": "auraClass",
        "k": "helper"
      },
      {
        "n": "auraClass",
        "k": "helper"
      },
      {
        "n": "auraOf",
        "k": "helper"
      },
      {
        "n": "auraOf",
        "k": "helper"
      },
      {
        "n": "buildOperatorMesh",
        "k": "helper"
      },
      {
        "n": "byRarityDesc",
        "k": "helper"
      },
      {
        "n": "byRarityDesc",
        "k": "helper"
      },
      {
        "n": "byteLength",
        "k": "helper"
      },
      {
        "n": "byteLength",
        "k": "helper"
      },
      {
        "n": "catKey",
        "k": "helper"
      },
      {
        "n": "composeLayers",
        "k": "helper"
      },
      {
        "n": "composeLayers",
        "k": "helper"
      },
      {
        "n": "cosmeticUrl",
        "k": "helper"
      },
      {
        "n": "decodeMii",
        "k": "helper"
      },
      {
        "n": "decodeMii",
        "k": "helper"
      },
      {
        "n": "defaultLook",
        "k": "helper"
      },
      {
        "n": "defaultLook",
        "k": "helper"
      },
      {
        "n": "defaultMiiParams",
        "k": "helper"
      },
      {
        "n": "defaultMiiParams",
        "k": "helper"
      },
      {
        "n": "deriveMiiValue",
        "k": "helper"
      },
      {
        "n": "diffLook",
        "k": "helper"
      },
      {
        "n": "diffLook",
        "k": "helper"
      },
      {
        "n": "encodeMii",
        "k": "helper"
      },
      {
        "n": "encodeMii",
        "k": "helper"
      },
      {
        "n": "frameOf",
        "k": "helper"
      },
      {
        "n": "frameOf",
        "k": "helper"
      },
      {
        "n": "getSlot",
        "k": "helper"
      },
      {
        "n": "getSlot",
        "k": "helper"
      },
      {
        "n": "initialsFor",
        "k": "helper"
      },
      {
        "n": "isRenderedSlot",
        "k": "helper"
      },
      {
        "n": "isRenderedSlot",
        "k": "helper"
      },
      {
        "n": "lookSignature",
        "k": "helper"
      },
      {
        "n": "lookSignature",
        "k": "helper"
      },
      {
        "n": "miiDataUri",
        "k": "helper"
      },
      {
        "n": "miiFaceFor",
        "k": "helper"
      },
      {
        "n": "miiFromLook",
        "k": "helper"
      },
      {
        "n": "miiFromLook",
        "k": "helper"
      },
      {
        "n": "miiGeometry",
        "k": "helper"
      },
      {
        "n": "miiGeometry",
        "k": "helper"
      },
      {
        "n": "miiSeedOf",
        "k": "helper"
      },
      {
        "n": "miiShoulders",
        "k": "helper"
      },
      {
        "n": "miiShoulders",
        "k": "helper"
      },
      {
        "n": "miiSlotValue",
        "k": "helper"
      },
      {
        "n": "miiSlotValue",
        "k": "helper"
      },
      {
        "n": "miiSvgString",
        "k": "helper"
      },
      {
        "n": "normalizeLook",
        "k": "helper"
      },
      {
        "n": "normalizeLook",
        "k": "helper"
      },
      {
        "n": "parseLook",
        "k": "helper"
      },
      {
        "n": "parseLook",
        "k": "helper"
      },
      {
        "n": "parseSlotValue",
        "k": "helper"
      },
      {
        "n": "parseSlotValue",
        "k": "helper"
      },
      {
        "n": "pick",
        "k": "helper"
      },
      {
        "n": "randomMiiParams",
        "k": "helper"
      },
      {
        "n": "randomMiiParams",
        "k": "helper"
      },
      {
        "n": "rarityOf",
        "k": "helper"
      },
      {
        "n": "rarityOf",
        "k": "helper"
      },
      {
        "n": "renderPersonaSnapshot",
        "k": "helper"
      },
      {
        "n": "serializeLook",
        "k": "helper"
      },
      {
        "n": "serializeLook",
        "k": "helper"
      },
      {
        "n": "slotsForPhase",
        "k": "helper"
      },
      {
        "n": "slotsForPhase",
        "k": "helper"
      },
      {
        "n": "validateLook",
        "k": "helper"
      },
      {
        "n": "validateLook",
        "k": "helper"
      },
      {
        "n": "variantCount",
        "k": "helper"
      },
      {
        "n": "variantCount",
        "k": "helper"
      }
    ]
  }
}
