Roly the Pangolin — asset structure & naming
Where every mascot image lives, and what it's called.
Status (16 Jul): the 3D rendered art is the in-product art (team decision) — not a separate flat-SVG tier. 16/16 renders live;
tiers.interim.activestaystruein the manifest so<Mascot>serves them. A flat SVG tier is optional, not required — add one later only if the look calls for it.
Naming convention
One rule, deterministic in both directions:
- Manifest key —
camelCase, matches the product state it serves:streakBroken - Filename —
mascot-+ the key inkebab-case+ extension:mascot-streak-broken.svg
Nothing else. No version suffixes, no _final, no v2. Git holds history; filenames don't.
| Manifest key | Product file | Marketing file |
|---|---|---|
greeting |
mascot-greeting.svg |
mascot-greeting.png |
thinking |
mascot-thinking.svg |
mascot-thinking.png |
tip |
mascot-tip.svg |
mascot-tip.png |
studying |
mascot-studying.svg |
mascot-studying.png |
correcting |
mascot-correcting.svg |
mascot-correcting.png |
celebrating |
mascot-celebrating.svg |
mascot-celebrating.png |
jubilant |
mascot-jubilant.svg |
mascot-jubilant.png |
triumphant |
mascot-triumphant.svg |
mascot-triumphant.png |
cheering |
mascot-cheering.svg |
mascot-cheering.png ⚠️ shares the jubilant pose, confetti stripped |
encouraging |
mascot-encouraging.svg |
mascot-encouraging.png |
grateful |
mascot-grateful.svg |
mascot-grateful.png |
resting |
mascot-resting.svg |
mascot-resting.png |
speaking |
mascot-speaking.svg |
mascot-speaking.png |
offline |
mascot-offline.svg |
mascot-offline.png |
streakBroken |
mascot-streak-broken.svg |
mascot-streak-broken.png |
premium |
mascot-premium.svg |
mascot-premium.png |
Sixteen expressions. That's the ceiling for a while — the next one needs to justify itself against a product state that doesn't exist yet.
Folder tree
ks-design-system/
├── public/assets/mascot/ ← SERVED by Vite at /assets/mascot/…
│ ├── mascot.manifest.json ← source of truth. Edit this, not the component.
│ ├── product/ ← flat neo-brutalist SVG. Target state. EMPTY today.
│ └── marketing/ ← 3D PNG. The in-product art too (team decision).
│
└── assets/mascot/source/ ← archive. NOT served, NOT shipped to the browser.
├── sheet-01-core-8.png
├── sheet-02-celebration-4.png
├── sheet-03-states-4.png
├── hero-greeting-original.png
└── README.md
Why the split: Vite only serves public/. Art that must resolve at runtime lives there.
The source sheets are 1.5 MB each and must never be shipped to a browser, so they stay in
assets/, versioned but unserved.
Old (pre-14 Jul) layout — kept for reference
├── product/ ← flat neo-brutalist SVG.
│ ├── mascot-greeting.svg
│ ├── mascot-thinking.svg
│ ├── mascot-tip.svg
│ ├── mascot-studying.svg
│ ├── mascot-correcting.svg
│ ├── mascot-celebrating.svg
│ ├── mascot-jubilant.svg
│ ├── mascot-triumphant.svg
│ ├── mascot-cheering.svg
│ ├── mascot-encouraging.svg
│ ├── mascot-grateful.svg
│ ├── mascot-resting.svg
│ ├── mascot-speaking.svg
│ ├── mascot-offline.svg
│ ├── mascot-streak-broken.svg
│ └── mascot-premium.svg
│
├── marketing/ ← 3D rendered PNG, transparent, 2048px. The in-product art too (via <Mascot>).
│ ├── mascot-greeting.png
│ ├── … (same 16 names)
│ └── mascot-premium.png
│
└── source/ ← originals. Not shipped, not imported.
├── sheet-01-core-8.png
├── sheet-02-celebration-4.png
├── sheet-03-states-4.png
└── README.md
Why source/ exists
When someone asks "can we get a variant of premium facing left," you need the sheet it came from and the prompt or file that made it. Losing that is how mascots drift over time. source/README.md maps each expression back to its origin.
Product-tier export rules
The product SVG is the pose only. Strip these out before export:
| Element | In the art? | Why |
|---|---|---|
| Held props — mic, calendar, gold card, trophy, book, pom-pom, water bottle, backpack | ✅ Keep | Part of the pose. The mascot is holding them. |
| Particles — sparkles, confetti, music notes, hearts, motion lines | ❌ Strip | Separate animated layer. Declared per-expression in the manifest. |
Live UI — the waveform speech bubble on speaking |
❌ Strip | Must render the user's real audio. A drawn squiggle is a lie. |
System status — the wifi-off badge on offline |
❌ Strip | That's a UI icon (Lucide), not illustration. The app owns it. |
So mascot-speaking.svg is the pangolin holding a mic — no bubble, no waveform. The app composes the real waveform beside it. mascot-offline.svg is the worried pangolin — no wifi glyph. The app pairs it with the Lucide wifi-off icon.
This matters more than it sounds. Bake the waveform in and you ship a recording screen that looks like it's listening whether or not the mic is actually open.
Marketing-tier export rules
Opposite: keep everything. Props, particles, the wifi badge, the waveform bubble — all baked in. Shiva needs a single PNG she can drop into a deck or a story without compositing anything.
- Transparent background
- 2048px on the long edge
- No
@2xvariants — one high-res master, downscale at point of use
Rendering the mascot
Everything goes through <Mascot>, which reads the manifest and serves the 3D renders from
marketing/. Don't hardcode asset paths in product code — import the component, not the PNG, so the
art can be swapped centrally (e.g. if a flat SVG tier is added later).