DsEmojiPicker + Color-Emoji Rendering Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: A calm, low-stim emoji picker (DsEmojiChip + DsEmojiPicker, curated-default → expandable searchable grid) wired into the Setup wizard, plus a bundled color-emoji font so emoji render everywhere — including Flutter goldens.
Architecture: A color-emoji font asset in design_system registered for both runtime and the flow_test golden loader (the foundation, proven first). A data-only categorized emoji dataset + two curated sets. A DsEmojiChip atom + DsEmojiPicker molecule opened via the existing showDsSheet. Wiring into the Setup wizard steps so a chosen emoji reaches the already-emoji-ready SetupBloc.
Tech Stack: Flutter 3.44 / Dart 3.9 (FVM), design_system package (DsSheet, DsTextField, MotionTokens), golden_toolkit, the flow_test harness, flutter_bloc.
Global Constraints
- Use
fvm flutter/fvm dartfor every command. - Capture the TRUE flutter exit code in every test step — run
fvm flutter test <target> > /tmp/t.txt 2>&1; echo "FLUTTER_EXIT=$?"then inspect the file. NEVER pipeflutter testtotail/grepfor the pass/fail decision (it masks the real exit code). A step passes only whenFLUTTER_EXIT=0AND the file says "All tests passed!" with no "Exhausted heap"/"Out of Memory". - All animation honors reduced motion via
MotionTokens.durationOrZero(context, ...)/MediaQuery.disableAnimationsOf(context)— one code path, static when reduced. design_systemstays model-agnostic (noclient_sdkdep). The emoji dataset + picker live indesign_system.- Maestro resolves
id:againstSemantics(identifier: '...'), NOT Keys — put aSemantics(identifier:)on the chip + sheet + a couple of stable emoji. KeepKeys forfind.byKeyin tests. - Codegen (if any) uses a scoped
--build-filter; after it,git diffand restore any unintended.g.dart/.gr.dartfrom HEAD. - Goldens tagged
golden, regenerated with--update-goldens; the galleryflutter build web --releasemust still succeed. - Emoji is OPTIONAL everywhere (null is valid). Commit per task on branch
feat/emoji-picker; do NOT push. - Brand/user-facing copy via i18n where the app already does; DS strings stay in the DS.
File structure
design_system — font + data + components:
- Add
packages/design_system/assets/fonts/NotoColorEmoji.ttf(+ its OFL license) — the color-emoji font asset. - Modify
packages/design_system/pubspec.yaml— declare the emoji font (replacing theTODO(fonts)line). - Modify
packages/design_system/lib/src/tokens/typography_tokens.dart— expose the emoji font family name + wire it as the text-theme emoji fallback. - Create
packages/design_system/lib/src/data/emoji_data.dart— the categorized emoji dataset (data-only:EmojiEntry{char, name, keywords, category}+kAllEmoji) +kCuratedHouseholdEmoji+kCuratedKidEmoji. - Create
packages/design_system/lib/src/atoms/ds_emoji_chip.dart—DsEmojiChip. - Create
packages/design_system/lib/src/molecules/ds_emoji_picker.dart—DsEmojiPicker+showDsEmojiPicker. - Modify
packages/design_system/lib/design_system.dart— exports. - Modify
packages/design_system/gallery/lib/main.dart— aDsEmojiPickerstory. - Tests under
packages/design_system/test/.
flow_test — golden loader:
- Modify
packages/flow_test/lib/src/golden_fonts.dart— register the color-emoji font for goldens; retire the monochrome workaround.
app — wizard wiring:
- Modify
app/lib/inside/routes/authenticated/setup/page.dart— own_householdEmoji/_kidEmojistate, pass to steps, include in dispatched events. - Modify
app/.../setup/widgets/steps/name_step.dart+kid_step.dart— replace the "emoji deferred" slot with aDsEmojiChip. - Modify
app/test/flows/setup_test.dart— pick an emoji, assert it reaches the bloc event.
Phase 1 — Color-emoji font (proof-first foundation)
Task 1: Bundle + register the color-emoji font; PROVE it renders in a golden
Files:
- Create:
packages/design_system/assets/fonts/NotoColorEmoji.ttf+NotoColorEmoji-OFL.txt - Modify:
packages/design_system/pubspec.yaml,packages/design_system/lib/src/tokens/typography_tokens.dart - Modify:
packages/flow_test/lib/src/golden_fonts.dart - Test:
packages/design_system/test/golden/emoji_render_golden_test.dart
Interfaces:
- Produces: the emoji font family constant
TypographyTokens.emojiFamily = 'Twemoji'used as thefontFamilyFallbacktail on the DS text theme; theflow_testgolden loader registers the same family so goldens render it.
PROVEN (no gamble): A pixel-probe experiment (2026-06-21) established that Twemoji COLRv0 renders color in Flutter 3.44 goldens (CBDT + COLRv0 → color; COLRv1 → invisible — the earlier failed attempt). The Twemoji font is already downloaded + staged at /tmp/Twemoji.ttf (Mozilla/jdecked build, CC-BY 4.0, ~1.5 MB). This task wires it in; the color-render golden is now a VERIFICATION, not a gamble.
-
Step 1: Place the font + license.
cp /tmp/Twemoji.ttf packages/design_system/assets/fonts/Twemoji.ttf. Addpackages/design_system/assets/fonts/Twemoji-CCBY.txtwith the CC-BY 4.0 attribution (Twitter/Twemoji, © Twitter Inc., CC-BY 4.0; Mozilla/jdecked COLRv0 build). If/tmp/Twemoji.ttfis missing, re-download the Mozilla Twemoji COLRv0TwemojiMozilla.ttfand note the source. -
Step 2: Write the failing color-render golden + pixel-color probe
packages/design_system/test/golden/emoji_color_render_test.dart — render 🦊🏡⭐ in a Text with fontFamily: 'Twemoji', capture the RepaintBoundary image, and ASSERT a chromatic pixel exists (the proof color rendered, not mono/tofu). Use the probe technique from the experiment:
@Tags(<String>['golden'])
library;
// loads Twemoji via FontLoader, renders Text('🦊🏡⭐', fontFamily: 'Twemoji', size 64),
// boundary.toImage → rawRgba → assert: some opaque non-background pixel has (max-min channel) > 20.
test('Twemoji renders COLOR in the golden pipeline', () async {
// expect(foundColor, isTrue); // chromatic pixel present
});
Keep it self-contained + FAST (await the loader, pump once, read pixels, assert, return — do NOT leave pending timers; the experiment's probe hung 10 min because it didn't). Use a hard per-test timeout.
-
Step 3: Run → fail.
cd packages/design_system && MSYS_NO_PATHCONV=1 fvm flutter test test/golden/emoji_color_render_test.dart > /tmp/t.txt 2>&1; echo "FLUTTER_EXIT=$?"; grep -iE "foundColor|All tests passed|FAIL|Some tests failed" /tmp/t.txt→ FAIL (font not wired / family unresolved). -
Step 4: Register Twemoji (runtime + golden)
pubspec.yaml: replace the TODO(fonts) line with a - family: Twemoji\n fonts:\n - asset: assets/fonts/Twemoji.ttf entry. typography_tokens.dart: set static const emojiFamily = 'Twemoji'; and ensure the DS body/display TextStyles carry fontFamilyFallback: const [emojiFamily]. golden_fonts.dart: the bare-family re-registration loads Twemoji from the design_system pubspec; retire the _loadMonochromeEmojiFont() monochrome workaround (it forced mono Noto Emoji) so emoji now resolve to Twemoji color.
- Step 5: Run → pass (color VERIFIED).
cd packages/design_system && MSYS_NO_PATHCONV=1 fvm flutter test test/golden/emoji_color_render_test.dart > /tmp/t.txt 2>&1; echo "FLUTTER_EXIT=$?"; grep -iE "foundColor|All tests passed" /tmp/t.txt → FLUTTER_EXIT=0, the probe's foundColor=true, "All tests passed!". (The pixel-color assertion IS the proof — no manual PNG inspection needed.)
- Step 6: analyze + format + commit
Run: cd rewhaven && fvm flutter analyze > /tmp/a.txt 2>&1; echo "EXIT=$?"; tail -2 /tmp/a.txt (0/0) · fvm dart format packages/design_system packages/flow_test.
git add packages/design_system/assets/fonts/Twemoji.ttf packages/design_system/assets/fonts/Twemoji-CCBY.txt packages/design_system/pubspec.yaml packages/design_system/lib/src/tokens/typography_tokens.dart packages/flow_test/lib/src/golden_fonts.dart packages/design_system/test/golden/emoji_color_render_test.dart
git commit -m "feat(ds): bundle Twemoji (COLRv0) — color emoji render at runtime + in goldens; retire mono workaround"
Task 2: Regenerate the existing emoji-bearing goldens to true color
Files:
- Modify (regenerate):
app/test/flows/**/screenshots/*.pngare gitignored — but any COMMITTED goldens that contain emoji (DS + app golden PNGs) need regenerating. - Test: the existing golden suites.
Interfaces: Consumes Task 1's color font.
-
Step 1: Find committed goldens that render emoji. Run
cd rewhaven && git ls-files '*goldens*/*.png' | headand identify which DS/app golden images contain emoji (e.g. Today rows, vignettes, any chip). The flow-test screenshots are gitignored (not committed) so they don't need a commit; only COMMITTED goldens matter. -
Step 2: Regenerate the affected committed goldens
Run the owning golden suites with --update-goldens, capturing the true exit:
cd packages/design_system && MSYS_NO_PATHCONV=1 fvm flutter test -t golden --update-goldens > /tmp/t.txt 2>&1; echo "FLUTTER_EXIT=$?" then re-run WITHOUT the flag to confirm pass.
- Step 3: Verify + commit
Run each affected suite without --update-goldens → FLUTTER_EXIT=0. Eyeball one regenerated image to confirm color emoji. Then:
git add packages/design_system/test/golden/goldens
git commit -m "test(ds): regenerate emoji-bearing goldens to true color"
(If NO committed goldens contain emoji, skip the commit and note it — flow-test screenshots are gitignored.)
Phase 2 — Emoji dataset + curated sets
Task 3: The categorized emoji dataset + curated sets
Files:
- Create:
packages/design_system/lib/src/data/emoji_data.dart - Modify:
packages/design_system/lib/design_system.dart(export) - Test:
packages/design_system/test/data/emoji_data_test.dart
Interfaces:
-
Produces:
class EmojiEntry { final String char; final String name; final List<String> keywords; final EmojiCategory category; const EmojiEntry(...); }enum EmojiCategory { smileys, animals, food, activities, travel, objects, symbols, nature }const List<EmojiEntry> kAllEmoji— a curated-but-broad set (a few hundred common emoji with names + keywords; NOT the full 1800 Unicode set — YAGNI for naming a home/kid; enough to feel complete).const List<String> kCuratedHouseholdEmoji(~16–24: 🏡🏠💛⭐🌙🌳🪴🍳🧺🛋️🔆🧸…)const List<String> kCuratedKidEmoji(~16–24: 🦊🐼🐯🦄🐙🐱🦖🌟🚀🐢🦋🐝…)List<EmojiEntry> searchEmoji(String query)— case-insensitive match on name + keywords; empty query →kAllEmoji.
-
Step 1: Write the failing tests
packages/design_system/test/data/emoji_data_test.dart:
test('search matches name and keywords, case-insensitive', () {
expect(searchEmoji('fox').map((e) => e.char), contains('🦊'));
expect(searchEmoji('FOX').map((e) => e.char), contains('🦊'));
expect(searchEmoji('').length, kAllEmoji.length);
});
test('curated sets are non-empty, deduped, and subsets of kAllEmoji chars', () {
for (final set in [kCuratedHouseholdEmoji, kCuratedKidEmoji]) {
expect(set, isNotEmpty);
expect(set.toSet().length, set.length); // no dupes
}
});
test('every emoji entry has a non-empty name', () {
expect(kAllEmoji.every((e) => e.name.trim().isNotEmpty), isTrue);
});
- Step 2: Run to verify it fails
Run: cd packages/design_system && fvm flutter test test/data/emoji_data_test.dart > /tmp/t.txt 2>&1; echo "FLUTTER_EXIT=$?"; tail -5 /tmp/t.txt
Expected: FAIL — undefined.
-
Step 3: Implement
emoji_data.dartwith theEmojiEntry/EmojiCategorytypes, a few-hundred-entrykAllEmoji(chars + names + keywords + category), the two curated lists, andsearchEmoji. Export from the barrel. -
Step 4: Run to verify pass
Run: ... fvm flutter test test/data/emoji_data_test.dart > /tmp/t.txt 2>&1; echo "FLUTTER_EXIT=$?" → 0, "All tests passed!".
- Step 5: analyze + format + commit
Run: cd rewhaven && fvm flutter analyze (0/0) · format.
git add packages/design_system/lib/src/data packages/design_system/lib/design_system.dart packages/design_system/test/data
git commit -m "feat(ds): categorized emoji dataset + curated household/kid sets + search"
Phase 3 — DsEmojiChip + DsEmojiPicker
Task 4: DsEmojiChip atom
Files:
- Create:
packages/design_system/lib/src/atoms/ds_emoji_chip.dart - Modify:
packages/design_system/lib/design_system.dart(export) - Test:
packages/design_system/test/atoms/ds_emoji_chip_test.dart+ a golden intest/golden/
Interfaces:
-
Produces:
class DsEmojiChip extends StatelessWidget { const DsEmojiChip({this.emoji, required this.onTap, this.semanticIdentifier, this.semanticLabel, super.key}); final String? emoji; final VoidCallback onTap; final String? semanticIdentifier; final String? semanticLabel; }— a rounded tappable tile (≥48dp) showingemojiat display size, or a neutral placeholder (a face/+ glyph incolors.inkMuted) when null. Wrapped inSemantics(identifier: semanticIdentifier, label: semanticLabel ?? 'Choose an emoji', button: true). Focus ring + AA per the DS atom floor. -
Step 1: Write the failing widget + golden tests
testWidgets('DsEmojiChip shows the emoji and fires onTap', (tester) async {
var tapped = false;
await tester.pumpWidget(_wrap(DsEmojiChip(emoji: '🦊', onTap: () => tapped = true)));
expect(find.text('🦊'), findsOneWidget);
await tester.tap(find.byType(DsEmojiChip));
expect(tapped, isTrue);
});
testWidgets('DsEmojiChip shows a placeholder when null', (tester) async {
await tester.pumpWidget(_wrap(DsEmojiChip(emoji: null, onTap: () {})));
expect(find.text('🦊'), findsNothing);
});
// + a golden: chip filled (🦊) and chip empty, side by side (real color emoji)
-
Step 2: Run → fail.
cd packages/design_system && fvm flutter test test/atoms/ds_emoji_chip_test.dart > /tmp/t.txt 2>&1; echo "FLUTTER_EXIT=$?"; tail -5 /tmp/t.txt→ FAIL. -
Step 3: Implement
DsEmojiChipper the interface. Export from the barrel. -
Step 4: Run + regenerate golden → pass. Widget test exit 0;
... -t golden --update-goldensthen re-run → exit 0; eyeball the golden shows a color fox. -
Step 5: analyze + format + commit
git add packages/design_system/lib/src/atoms/ds_emoji_chip.dart packages/design_system/lib/design_system.dart packages/design_system/test/atoms packages/design_system/test/golden
git commit -m "feat(ds): DsEmojiChip atom (emoji tile / empty placeholder, a11y)"
Task 5: DsEmojiPicker molecule + showDsEmojiPicker
Files:
- Create:
packages/design_system/lib/src/molecules/ds_emoji_picker.dart - Modify:
packages/design_system/lib/design_system.dart(export) - Modify:
packages/design_system/gallery/lib/main.dart(story) - Test:
packages/design_system/test/molecules/ds_emoji_picker_test.dart+ a golden
Interfaces:
-
Consumes:
kAllEmoji/searchEmoji/kCuratedHouseholdEmoji/kCuratedKidEmoji(Task 3),showDsSheet<String>+DsSheetPanel,DsTextField,MotionTokens. -
Produces:
class DsEmojiPicker extends StatefulWidget { const DsEmojiPicker({required this.curated, this.selected, required this.onSelected, super.key}); final List<String> curated; final String? selected; final ValueChanged<String?> onSelected; }— body: the curated grid (default); a "Search more" expander revealing aDsTextField+ the full grid (searchEmoji(query)), filtered live; a "None" affordance →onSelected(null). The expand animation usesMotionTokens.durationOrZero. Each emoji tile is a button withSemantics(label: <name>); a stable couple carrySemantics(identifier:).Future<String?> showDsEmojiPicker(BuildContext context, {required List<String> curated, String? selected})— opensDsEmojiPickerinsideshowDsSheet<String>and resolves to the chosen emoji (or null).
-
Step 1: Write the failing tests
testWidgets('curated grid shows by default; tapping selects + pops', (tester) async {
String? picked = 'unset';
await tester.pumpWidget(_wrapWithSheetButton((ctx) async {
picked = await showDsEmojiPicker(ctx, curated: kCuratedKidEmoji);
}));
await tester.tap(find.text('Open')); await tester.pumpAndSettle();
expect(find.text(kCuratedKidEmoji.first), findsWidgets); // curated visible
await tester.tap(find.text(kCuratedKidEmoji.first).first); await tester.pumpAndSettle();
expect(picked, kCuratedKidEmoji.first); // selected + popped
});
testWidgets('Search more reveals search + filters the full grid', (tester) async {
// open picker → tap "Search more" → enter "fox" → 🦊 appears
});
testWidgets('None clears the selection', (tester) async {
// open → tap "None" → onSelected(null)
});
-
Step 2: Run → fail.
cd packages/design_system && fvm flutter test test/molecules/ds_emoji_picker_test.dart > /tmp/t.txt 2>&1; echo "FLUTTER_EXIT=$?"; tail -5 /tmp/t.txt→ FAIL. -
Step 3: Implement
DsEmojiPicker+showDsEmojiPicker. Grid ofInkWell/button tiles; the "Search more" toggle animates open with the reduced-motion duration;searchEmojidrives the filtered grid; export from the barrel. -
Step 4: Run + golden → pass. Tests exit 0; add + regenerate a sheet golden (curated state + expanded-search state) showing real color emoji; re-run → exit 0.
-
Step 5: Gallery story + gates + commit. Add a
DsEmojiPickeruse-case to the molecules grouping ingallery/lib/main.dart;cd packages/design_system/gallery && MSYS_NO_PATHCONV=1 fvm flutter build web --release > /tmp/b.txt 2>&1; echo "EXIT=$?"(succeeds);cd rewhaven && fvm flutter analyze(0/0); format.
git add packages/design_system/lib/src/molecules/ds_emoji_picker.dart packages/design_system/lib/design_system.dart packages/design_system/gallery/lib/main.dart packages/design_system/test/molecules packages/design_system/test/golden
git commit -m "feat(ds): DsEmojiPicker (curated grid + expandable search) + showDsEmojiPicker"
Phase 4 — Wire into the Setup wizard
Task 6: Household + kid emoji in the wizard (chip → picker → bloc)
Files:
- Modify:
app/lib/inside/routes/authenticated/setup/page.dart - Modify:
app/.../setup/widgets/steps/name_step.dart,kid_step.dart - Test:
app/test/flows/setup_test.dart
Interfaces:
-
Consumes:
DsEmojiChip,showDsEmojiPicker,kCuratedHouseholdEmoji,kCuratedKidEmoji. The page already dispatchesSetupHouseholdNamed(name, emoji)+SetupKidSubmitted(name, colorIndex, emoji)(the bloc persists emoji already). -
Produces: the Setup page owns
String? _householdEmoji/String? _kidEmoji; the name + kid steps render aDsEmojiChip(onTap→showDsEmojiPickerwith the context curated set → set the state) next to the name field; the dispatched events carry the chosen emoji instead ofnull. -
Step 1: Update the flow test to expect the emoji reaching the bloc
In app/test/flows/setup_test.dart's success scenario, after entering the household name tap the household DsEmojiChip (find.byKey/its Semantics(identifier:)), pick a known curated emoji, then assert the SetupHouseholdNamed event carries that emoji (the flow harness asserts dispatched events / expectedEvents). Same for the kid step + SetupKidSubmitted. Run first → FAIL (chip not present / emoji still null):
cd app && MSYS_NO_PATHCONV=1 fvm flutter test test/flows/setup_test.dart > /tmp/t.txt 2>&1; echo "FLUTTER_EXIT=$?"; tail -5 /tmp/t.txt
- Step 2: Add emoji state + the chip to the steps
In setup/page.dart: add String? _householdEmoji / _kidEmoji; pass each + an onEmojiSelected callback into the steps; include them in the dispatched events (emoji: _householdEmoji / emoji: _kidEmoji). In name_step.dart + kid_step.dart: replace the "emoji deferred" comment/slot with a DsEmojiChip(emoji: emoji, semanticIdentifier: 'SetupPage.householdEmoji' | 'SetupPage.kidEmoji', onTap: () async { final picked = await showDsEmojiPicker(context, curated: kCuratedHouseholdEmoji|kCuratedKidEmoji, selected: emoji); onEmojiSelected(picked); }). Keep the existing name field + color swatch + all other Semantics/Keys.
- Step 3: Run the flow test → pass
cd app && MSYS_NO_PATHCONV=1 fvm flutter test test/flows/setup_test.dart > /tmp/t.txt 2>&1; echo "FLUTTER_EXIT=$?"; tail -5 /tmp/t.txt → FLUTTER_EXIT=0, "All tests passed!".
- Step 4: Regenerate the setup goldens + full suite + gates
Regenerate setup-flow goldens (now with a chosen emoji): cd app && MSYS_NO_PATHCONV=1 fvm flutter test test/flows/setup_test.dart --update-goldens --dart-define=createScreenshots=true > /tmp/t.txt 2>&1; echo "FLUTTER_EXIT=$?". Then the WHOLE app suite with the true exit: cd app && MSYS_NO_PATHCONV=1 fvm flutter test > /tmp/t.txt 2>&1; echo "FLUTTER_EXIT=$?"; tail -2 /tmp/t.txt; grep -cE "Exhausted heap|\[E\]" /tmp/t.txt → exit 0, "All tests passed!", 0 heap/failures. cd rewhaven && fvm flutter analyze (0/0); fvm dart format ..
- Step 5: Commit + graphify
git add app/lib/inside/routes/authenticated/setup app/test/flows/setup_test.dart
git commit -m "feat(app): emoji picker in the Setup wizard (household + kid) → reaches SetupBloc"
Then cd rewhaven && graphify update . (don't commit graphify-out).
Self-review
- Spec coverage: color-emoji font + loaders + retire monochrome (T1, T2), emoji dataset + curated sets (T3),
DsEmojiChip(T4),DsEmojiPicker+showDsEmojiPicker+ sheet + search + Widgetbook (T5), wizard wiring + flow tests (T6), a11ySemantics(label/identifier)(T4/T5/T6), reduced-motion (T5), goldens in true color (T1–T5). The prove-it-first gate is T1 Step 5. ✓ - Placeholders: none — interfaces + test code + exact commands with the true-exit-code pattern. The
kAllEmojicontent is "a few hundred entries" (bounded, YAGNI vs the full 1800) — the implementer authors the list; the SHAPE (EmojiEntry) + tests are concrete. - Type consistency:
EmojiEntry{char,name,keywords,category},searchEmoji,kCuratedHouseholdEmoji/kCuratedKidEmoji,DsEmojiChip({emoji,onTap,semanticIdentifier,semanticLabel}),DsEmojiPicker({curated,selected,onSelected}),showDsEmojiPicker(context,{curated,selected})→Future<String?>,TypographyTokens.emojiFamilyare used consistently across tasks. - Risk gate: T1 is proof-first; if color won't rasterize in goldens, the implementer STOPS and the spec fallback applies — the picker tasks don't silently build on an unproven font.