Skip to main content

Celebration — feature architecture

BUILT (Wave-1 A1, commit 60967b1). The completion-celebration overlay fires reactively on a child's chore SUBMIT directly from the TodayChoresBloc state 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.
  • DsCelebrationOverlay renders 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), tokenText stays null — no "+0" is ever shown.
  • DsCelebrationOverlay renders 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):

TierParticlesFlashCopyDismiss
full26 (varied seed, 750 ms life)60 ms white pulse at 30%rotating pool (Dweck process-praise)~1.8 s auto + tap-to-skip
gentle9 (fixed seed, 1100 ms life)nonesingle fixed line (predictable-by-design)~1.8 s auto + tap-to-skip
reducedMotion0 (inert box)nonefixed line900 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:

  • reducedMotion tier: paints an empty SizedBox (no particles at all).
  • MediaQuery.disableAnimations with any other tier: MotionTokens.durationOrZero clamps the AnimationController to Duration.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 credited vs. submitted to animate different emotions.
  • Token-fade "graduation" — a planned mechanic where the token economy fades as intrinsic motivation grows; the same submitting → done transition 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 credited boolean and tokenValue it exposes come from the ApprovalPolicy.auto path; manual completions show effort-only copy until the parent approves in the token economy queue.
  • Member profile — the same ChoreCompletion rows 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).