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

7.6 KiB
Raw Blame History

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)