Files
collective-lists/docs/history/fase-19-emoji-avatars.md
Oier Bravo Urtasun 9068ac4586 docs(fase-19): history doc + CHANGELOG [Unreleased] entry
Records: shipped 2026-05-19, 359 codepoints across 4 categories,
Unicode 14 baseline (iOS < 17 tofu protection), lazy-render by tab,
bundle delta +5,133 bytes uncompressed JS (well under +12 KB target).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 03:40:59 +02:00

100 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fase 19 — Emoji avatars expansion (food + animals)
**v1.0 · 3/4. Third pass of the v1.0 cycle.**
**Status: Shipped 2026-05-19.**
The avatar picker on `/settings` (and the same control reused on
`/onboarding`, `CreateCollectiveModal` and the `/collective/manage`
rename) used to ship four independent hardcoded arrays totalling 824
glyphs per surface. Fase 19 centralises the catalog (~280 codepoints
across 4 categories) into a shared component and rewires every site.
Plan: `plan/fase-19-emoji-avatars.md`. No DB migration.
## What changed
### 19.1 — Catalog (`$lib/data/emoji-catalog.ts`)
- Hardcoded `Record<EmojiCategory, readonly string[]>` with four categories: `faces` (64), `food` (119), `animals` (140), `objects` (36) = **359 codepoints** total. Plan called for ~280; we landed above because Unicode 14's Food & Drink + Animals & Nature blocks are nearly complete in the catalog.
- **Inclusion rules** (narrow on purpose so every glyph renders consistently on iOS Safari >= 16, Android Chrome, and desktop browsers):
- Unicode 14 baseline only. Excludes Unicode 15+ (e.g. wireless heart 🩷, light-blue heart 🩵, jellyfish 🪼) — iOS < 17 renders these as tofu.
- No ZWJ sequences. No `👨‍👩‍👧` (family), no `🐕‍🦺` (service dog), no `🏳️‍🌈` (rainbow flag). Two reasons: (a) `users.avatar_emoji` stores a text column and ZWJ width is font-dependent, (b) older Android skins render the joined sequence as the component glyphs side-by-side.
- No skin-tone variants (`🏻..🏿`) — parked. The base codepoint already renders.
- Variation selectors kept where the canonical emoji form needs them (`☀️ U+2600 U+FE0F`, `🕊️ U+1F54A U+FE0F`).
- One semantic disambiguation: `🍄` lives in `food` (Animals & Nature in Unicode is the formal home, but the picker context is culinary). Enforced by EC-U-04 which would have surfaced the collision otherwise.
- `CATEGORY_LABELS` keeps the visible tab labels in Spanish (`Caras`, `Comida`, `Animales`, `Objetos`) — mirrors the existing UI style at the integration sites, which never went through Paraglide for `EMOJI_OPTIONS` itself.
### 19.2 — `EmojiPicker.svelte`
- Props: `selected: string | null`, `onSelect: (emoji: string) => void`, optional `onClose?: () => void`. Caller owns the persistence side-effect.
- Layout: tablist header (`role="tablist"`, 4 buttons) + body grid (`role="grid"`, `grid-cols-8 sm:grid-cols-10`, cells `h-9 w-9 text-xl`). Cells match the legacy 9px button so caller layouts don't reflow.
- **Lazy render**: only the active category's grid is mounted at any time. Switching tabs unmounts the previous grid and mounts the new one. Worst case is `animals` at 140 cells — ~40% smaller than mounting all 4 categories at once.
- A11y: cells are `role="gridcell"` with roving `tabindex` (first cell `0`, rest `-1`). Arrow keys / Home / End walk focus cell-by-cell per the WAI-ARIA grid pattern. The tablist receives a single Tab stop.
- The grid itself carries `tabindex="-1"` so it satisfies "interactive role must have a tabindex" without becoming a Tab target.
### 19.3 — Integration
Four sites swapped their inline `EMOJI_OPTIONS` / `EMOJI_GRID` arrays for `<EmojiPicker selected={…} onSelect={…} />`. Surrounding padding / containers preserved so visual layouts on each surface are unchanged.
- `apps/web/src/routes/(app)/settings/+page.svelte` — user avatar (avatar-type=`emoji` branch).
- `apps/web/src/routes/onboarding/+page.svelte` — initial collective emoji.
- `apps/web/src/lib/components/CreateCollectiveModal.svelte` — collective creation from the sidebar.
- `apps/web/src/routes/(app)/collective/manage/+page.svelte` — admin rename (Fase 10.2 surface).
**Backwards-incompatible testid change** on the manage page: the per-cell `collective-emoji-option-{e}` is gone; every cell now lives under the picker's `emoji-picker-cell-{e}` namespace. Two existing specs were updated: `tests/e2e/onboarding.test.ts` O-02 (was selecting `getByRole('button', { name: '🏡' })`) and `tests/e2e/rename-collective.test.ts` MC-01a (was selecting `collective-emoji-option-🏡`). Both now drive the tab → cell flow because 🏡 lives in the `objects` tab and the default is `faces`.
### 19.4 — Test coverage
- `src/lib/data/emoji-catalog.test.ts` — EC-U-01..04 (size bounds on food + animals, no intra-category duplicates, no cross-category overlap) + an integrity check on the 4 category keys + labels.
- `src/lib/components/EmojiPicker.test.ts` — EP-U-01..05 (4 tabs with `role="tab"`, default `faces` selected, click→`onSelect`, tab switch re-renders the grid with the new category's cells, ArrowRight moves focus to the next cell).
- `tests/e2e/emoji-picker.test.ts` — EP-E-01 (Ana on `/settings` selects 🍎 from the Comida tab; DB row updates) + EP-E-02 (Eva on `/onboarding` picks 🐱 from Animales; collective row + sidebar update).
## Test counts (final)
- **pgTAP**: 191 (unchanged)
- **Vitest integration**: 185 (unchanged) + 3 skipped (2 Realtime presence; 1 rate-limit gated)
- **Vitest unit**: 85 (was 75; +10 = 5 EC + 5 EP)
- **Playwright**: 106 (was 104; +2)
- **Gated rate-limit**: 1 (unchanged)
Total: **567 + 3 skipped** (was 555 + 3).
## Bundle-size delta
Measured with `pnpm --filter @colectivo/web build`, comparing the total size of `apps/web/build/client/_app/immutable/chunks/*.js` (uncompressed) before and after:
- Before: 526,835 bytes
- After: 531,968 bytes
- Delta: **+5,133 bytes (~5 KB)** uncompressed JS
Well under the +12 KB target. Compressed delta is even smaller because emoji-catalog is mostly multi-byte UTF-8 strings that gzip efficiently. No need for dynamic-import.
## Decisions worth remembering
- **Catalog is hardcoded, not fetched.** ~3 KB minified — small enough to ship in the JS bundle, big enough that re-fetching on every picker open would be needless network. The mutation procedure when Unicode lifts the floor is: edit the file by hand, re-run the catalog tests.
- **Unicode 14 baseline, not 15.** Until iOS 16 share drops below ~2% of the install base, Unicode 15 codepoints risk tofu rendering on a non-trivial slice of users. Re-evaluate in 2027.
- **Lazy-render by tab, not virtualised.** The largest tab (animals) is 140 cells which the browser handles trivially. Virtualisation would have added complexity and a measurable hydration cost on each tab switch.
- **No skin-tone selector.** Out of scope for the avatar use case; the base codepoint reads fine. Adding skin tones means re-shaping the picker into "base cell + modifier dropdown" — an order-of-magnitude bigger UX change.
- **No search.** With 4 tabs and 36140 cells each, scanning is fast. Search becomes worthwhile around ~500 codepoints or when we add custom-uploaded emoji.
## Out of scope (deliberately)
- Search emoji by name (`unicode-emoji-json` ships the localised names but doubles the JS payload).
- Skin-tone variants (`🏻..🏿`).
- Recently-used tracking (per-user history of last-selected emoji).
- Custom-uploaded emoji (per-collective sticker packs).
- OS-level emoji-keyboard parity (`<input type="text">` plus a `:` autocomplete).
## Files indexed
- `apps/web/src/lib/data/emoji-catalog.ts` + `emoji-catalog.test.ts`
- `apps/web/src/lib/components/EmojiPicker.svelte` + `EmojiPicker.test.ts`
- `apps/web/src/routes/(app)/settings/+page.svelte` (wired)
- `apps/web/src/routes/onboarding/+page.svelte` (wired)
- `apps/web/src/lib/components/CreateCollectiveModal.svelte` (wired)
- `apps/web/src/routes/(app)/collective/manage/+page.svelte` (wired)
- `apps/web/tests/e2e/emoji-picker.test.ts`
- `apps/web/tests/e2e/onboarding.test.ts` (O-02 driver updated)
- `apps/web/tests/e2e/rename-collective.test.ts` (MC-01a driver updated)