Celebration — feature architecture
BUILT (Wave-1 A1, commit 60967b1). The completion-celebration overlay fires reactively on a child's chore SUBMIT directly from the
TodayChoresBlocstate diff — no additional SDK verb, no schema change. Three intensity tiers (full / gentle / reduced-motion) ship with the atom and molecule; audio is deliberately deferred. This reactive signal is the planned hook point for the companion creature and a future token-fade "graduation" mechanic.
The loop in one idea
A child taps Done on the Today surface. The bloc emits an optimistic
submitting flip. The next state carries submitted (awaiting parent) or
done (auto-approved). The host diffs those two state snapshots; if a cell
just crossed submitting → submitted/done it fires a celebration overlay.
Everything that follows — intensity tier, praise copy, floating token text —
flows from that single boolean transition.
TodayChoresBloc
│ state.doneStates[choreId|memberId]
│ previous: submitting
│ current: submitted ──────▶ EFFORT path (manual)
│ done ──────▶ CREDITED path (auto-approved)
│
▼
resolveCelebrationRequest()
│ returns CelebrationRequest { choreId, memberId, credited }
│
▼
TodayCelebrationHost._celebrate()
│ resolves intensity tier (MediaQuery.disableAnimations wins)
│ resolves praise copy (from CelebrationScope preference)
│ resolves tokenText ("+N" only when credited && tokenValue > 0)
│
▼
Overlay.insert(DsCelebrationOverlay) ← IgnorePointer + tap-to-skip
The submitting predecessor guard is load-bearing: chores already approved
on cold load transition absent/notDone → done WITHOUT ever passing through
submitting, so they never trigger a spurious celebration.
1 · Entities & relationships
CelebrationController persists the user's preference (full / gentle)
through the same SharedPrefsEffect seam as AppearanceController. It
never stores reducedMotion — that tier is always derived from
MediaQuery.disableAnimations at render time and always wins over the
persisted preference.
2 · The two celebration paths
Effort path — submitting → submitted
The chore is manually approved; tokens have NOT been credited yet.
CelebrationRequest.credited = false.
- The host sets
tokenText = null. DsCelebrationOverlayrenders praise + particles but NO floating "+N".- The copy praises the action ("Nice work doing that!"), not the result.
Credited path — submitting → done
The chore carries approvalPolicy = auto; tokens land immediately.
CelebrationRequest.credited = true.
- The host looks up
chore.effectiveTokens(now)in the current bloc state. - If
tokenValue > 0,tokenText = Strings.celebrationTokenGain(tokens)(e.g."+5"). - If
tokenValue == 0(an auto-approved expectation),tokenTextstays null — no "+0" is ever shown. DsCelebrationOverlayrenders the full choreography including the floating token text positioned above the check-mark.
3 · Intensity tiers
Three tiers live in DsCelebrationIntensity (the DS atom enum):
| Tier | Particles | Flash | Copy | Dismiss |
|---|---|---|---|---|
full | 26 (varied seed, 750 ms life) | 60 ms white pulse at 30% | rotating pool (Dweck process-praise) | ~1.8 s auto + tap-to-skip |
gentle | 9 (fixed seed, 1100 ms life) | none | single fixed line (predictable-by-design) | ~1.8 s auto + tap-to-skip |
reducedMotion | 0 (inert box) | none | fixed line | 900 ms auto + tap-to-skip |
The gentle tier is designed for users who need predictability over novelty
(ADHD/autism overlap). Both the seed (fixed = same layout every time) and the
copy (one line, never rotating) are locked for consistency. The full tier
varies the burst seed per choreId.hashCode for novelty.
DsCelebrationBurst honors reduced motion two ways:
reducedMotiontier: paints an emptySizedBox(no particles at all).MediaQuery.disableAnimationswith any other tier:MotionTokens.durationOrZeroclamps theAnimationControllertoDuration.zero— the burst paints a single static end-frame and never ticks.
4 · Non-blocking overlay construction
The overlay is a Stack with three layers, all wrapped in IgnorePointer
except the translucent tap-catcher at the bottom of the stack. The tap-catcher
uses HitTestBehavior.translucent so the same tap dismisses the overlay AND
passes through to the UI beneath — the celebration never blocks further
interaction. Only one overlay is in-flight at a time: a second celebration
during an existing one calls _removeOverlay() before inserting.
Auto-dismiss is driven by _life — an AnimationController, not a bare
Timer — so WidgetTester.pumpAndSettle advances it to completion and the
overlay dismisses itself cleanly in widget tests with no pending timers.
5 · Audio (deliberately deferred)
// TODO(celebration-audio): sound is DELIBERATELY deferred — Wave-1 shipped
// under a zero-new-dependencies constraint. The research parameter sheet
// (docs/research/2026-07-14-engagement-expansion-research.md, Brief 1) calls
// for a T+0 sound synced ≤12ms with this haptic, via flutter_soloud
// (Beat-1 fires on a tap gesture, satisfying web autoplay). Adding audio =
// dep review + asset sourcing + a mute/respect-silent-mode story.
Haptic feedback (mediumImpact) fires at T+0 on every tier on mobile; it
silently no-ops on web and swallows platform-channel errors so a celebration
never throws.
6 · Future hook surface
resolveCelebrationRequest is a pure function returning a CelebrationRequest?
from two consecutive doneState maps. Future features can listen to the same
signal without coupling into overlay choreography:
- Companion creature — can react to
creditedvs.submittedto animate different emotions. - Token-fade "graduation" — a planned mechanic where the token economy
fades as intrinsic motivation grows; the same
submitting → donetransition carries the information needed to decide whether to show or suppress the economy signal.
Both features can wire into TodayCelebrationHost._celebrate() (or a sibling
listener on the same bloc) without touching the DS molecule.
Relationship to other features
- Token economy — the
creditedboolean andtokenValueit exposes come from theApprovalPolicy.autopath; manual completions show effort-only copy until the parent approves in the token economy queue. - Member profile — the same
ChoreCompletionrows the celebration is triggered by feed the achievements badge set and weekly streak. - Achievements — planned hook-on feature that can react to the celebration signal for badge unlock animations (deferred to Phase 2).