Skip to main content

Visual timers — requirements

Epic. MVP-1 requirements breakdown for the Visual timers differentiator (see the feature architecture). The timer is a pure presentation atom backed by a local ticker; it introduces no new domain — it consumes the existing estimateMin field on the chore model. This is the primary time-blindness affordance and a declared neurodiversity differentiator.

TypeFunctional ⭐
Layer (build approach)Frontend — design_system atom + local ticker
RICER 6 × I 2 × C 80% / E 2 = 4.8 · Tier MVP-1
KPI (summary)Timed-chore on-time completion lift — see Success criteria
Traces tofeature visual-timers · C4 design-system
Depends onToday (estimateMin) · Analytics

Success criteria (definitive KPI)

Success = for timed chores, on-time completion lifts by ≥ 10 percentage points vs untimed chores in the same household, OR ≥ 65% of timed chores record a timer_started event — measured across weeks 2–4 post-activation.

  • Metric A (adoption): share of chores with estimateMin > 0 that emit timer_started per week. Target: ≥ 65% by week 4.
  • Metric B (lift): on-time rate for timed chores vs the same household's untimed baseline. On-time = chore_completed arriving within estimateMin of timer_started. Target: ≥ +10 pp.
  • Window: weeks 2–4 post-activation (same cohort anchor as the Today KPI).
  • Guardrail: timer_cancelled ÷ timer_started < 30% — excess cancellation signals the timer is disruptive, not supportive.
  • Baseline: no install base yet; treat these numbers as the launch hypothesis, then re-baseline from first-cohort wk-2 data before declaring success or failure.

Analytics — events to record

All events are internal-BI only: content-free, no child identity, no chore content, never marketing. Emitted through the consent-gated analytics substrate. COPPA: opaque session ref only — never a member name or age-linked identifier (COPPA).

EventWhenKey propertiesFeeds
timer_startedMember taps start on a timed choreduration_bucket (≤5m|≤15m|≤30m|>30m), reduced_motion (bool)adoption numerator (Metric A)
timer_completedTicker reaches zero naturallyduration_bucket, overrun_bucket (on_time|<30s|<2m|>2m)on-time lift (Metric B); completion funnel
timer_cancelledMember dismisses before expiryduration_bucket, pct_elapsed_bucket (<25|<50|<75|<100)guardrail; UX signal

These three events are the minimum instrumentation required for the KPI — both timer_started (adoption denominator) and timer_completed (lift numerator) ship with the surface, not after.

Scope

The visual timer is a design_system atom (DsTimerRing or equivalent) that renders elapsed/remaining time as a shrinking arc or fill — concrete, spatial, not abstract digits. It is wired into the Today surface for any chore where estimateMin > 0. State is local: the ticker lives in a lightweight presentation controller with no Bloc event per tick and no server round-trip. It pauses when the app is backgrounded. Reduced-motion and a11y variants are required from day one.

Functional requirements

FR-TIMER-1 — DS timer atom: animated arc/fill

Priority: P1 · Status: ❌ not built Statement. As a child member with time-blindness, I see a big, concrete visual of time passing (shrinking arc or fill) so I understand how much time remains without reading a clock number. Acceptance

  • Given estimateMin > 0 and a running ticker When the atom renders Then a filled arc or ring shrinks proportionally from 100% → 0% as elapsed time increases.
  • Given elapsed >= total When expiry fires Then the atom reaches its empty/expired state and calls onExpired; it does not auto-change the chore status.
  • Atom is model-agnostic: accepts (Duration total, Duration elapsed) — zero client_sdk import.
  • Animation uses compositor-only properties (arc sweep, opacity); no layout-bound properties animate per frame.

FR-TIMER-2 — Reduced-motion + a11y variant

Priority: P1 · Status: ❌ not built Statement. As a member with reduced-motion enabled or who uses a screen reader, the timer remains fully usable without relying on animation or colour alone. Acceptance

  • Given MediaQuery.of(context).disableAnimations == true When the atom renders Then the continuous sweep is replaced by a discrete percentage label updating at ≤ 10-second intervals.
  • Arc stroke/fill achieves ≥ 3:1 contrast against its background at all fill levels (WCAG 1.4.3 non-text).
  • A Semantics label announces remaining time (e.g. "8 minutes remaining") and updates at ≤ 30-second intervals — not every tick — to avoid screen-reader noise.
  • Expiry is announced once via SemanticsService.announce.

FR-TIMER-3 — Local ticker controller

Priority: P1 · Status: ❌ not built Statement. As a developer, the ticker is a self-contained presentation controller so that timer precision does not inflate Bloc event volume or trigger unnecessary widget rebuilds. Acceptance

  • Given the timer is running When each frame fires Then only the DsTimerRing subtree rebuilds (via ValueListenableBuilder over a ValueNotifier<Duration>); the TodayChoresBloc emits zero events per tick.
  • Controller exposes start(), pause(), resume(), cancel() with immutable state transitions.
  • Elapsed time is derived from wall-clock delta (not accumulated increments) so background jitter does not drift the displayed remaining time.

FR-TIMER-4 — Lifecycle: pause when backgrounded

Priority: P1 · Status: ❌ not built Statement. As a member returning from a background task, I see an accurate remaining time — not a drifted or already-expired timer. Acceptance

  • Given the app transitions to AppLifecycleState.paused When the ticker is active Then it pauses immediately and records a wall-clock snapshot.
  • Given the app resumes When the ticker was paused by lifecycle Then elapsed time is recomputed from the wall-clock snapshot and ticking resumes without a visible gap.
  • No Timer.periodic fires while backgrounded; battery impact from the timer controller is zero during background.

FR-TIMER-5 — Today surface integration

Priority: P1 · Status: ❌ not built Statement. As a member on the Today surface, I can start a visual timer for any timed chore without navigating away from the chore row. Acceptance

  • Given a chore row with estimateMin > 0 When Today renders Then a start-timer affordance (icon or pill) is visible alongside the chore.
  • Given I tap start When the ticker is running Then DsTimerRing renders inline; surrounding chore rows do not reflow.
  • Given the timer expires When onExpired fires Then the row returns to its normal state; an optional local haptic fires (platform-permitted, user-controllable); chore status is not auto-changed.
  • The timer affordance is absent for chores without estimateMin.

FR-TIMER-6 — Analytics instrumentation

Priority: P1 · Status: ❌ not built Statement. As the product team, I can measure timer adoption and on-time lift using the three events in the Analytics table — with zero child PII in any payload. Acceptance

  • Given any timer action When the event is emitted Then the payload matches the schema above: correct duration_bucket, reduced_motion, overrun_bucket, or pct_elapsed_bucket as applicable.
  • No chore title, member name, household ID, or member-linked identifier appears in any timer event.
  • Events are suppressed when analytics consent is absent (consent-gated substrate).

Architecture considerations

  • Pure presentation, no new domain. DsTimerRing lives in design_system with no client_sdk dependency. estimateMin is already on the chore model — no schema change required.
  • One data path unchanged. TodayChoresBloc surfaces estimateMin from the repository; the ticker controller is instantiated in the widget tree, not inside the Bloc.
  • Compositor-friendly animation. Drive with a Flutter AnimationController or CustomPainter arc sweep; no layout invalidation per frame.
  • Reduced-motion. Branch on MediaQuery.of(context).disableAnimations at render time; single atom, single code path, no separate widget class.
  • Battery / lifecycle. WidgetsBindingObserver in the controller; use Ticker (frame-budget-aware) not Timer.periodic; pause Ticker on background.
  • Rebuild scope. ValueNotifier<Duration> + ValueListenableBuilder — zero Bloc events per tick, minimal rebuild surface.

Design work (ahead of build)

  • Timer atom visual (core design artifact): arc style (ring vs fill vs segmented), colour semantics (green → amber → red as time runs low with defined thresholds), expiry state (empty ring + icon or brief pulse). Both light and dark themes; inherits DsTheme tokens — no hardcoded colours.
  • Reduced-motion variant: discrete step layout — percentage label + segmented progress indicator replacing the continuous sweep.
  • On-Today placement: inline affordance in the chore row (start icon/pill) and the active timer state (full ring or compact arc); must not reflow adjacent rows.

Decisions (resolved for MVP-1)

Resolved — see the MVP-1 decisions log for the canonical record, rationale, and status legend (✅ decided · ⚖️ counsel confirms · 🔜 MVP-1.x).

  • D-TIMER-1 — Expiry signal + admin disable (FR-TIMER-5). ✅ Expiry fires a gentle haptic + optional local notification; the household admin can disable the notification (haptic stays unless the device is muted).
  • D-TIMER-2 — Timer in approval view.Yes — show elapsed/estimate on the approval card so a reviewer sees how long the chore took. Reuses the atom.
  • D-TIMER-3 — Estimate adjustment. ✅ Admin-set estimate is the default; a member may adjust their own run's estimate within bounds — not the chore's canonical estimate.
  • D-TIMER-4 — Colour thresholds.Green ≥ 50%, amber < 50%, red < 25% remaining; encode as colour tokens. Under reduced-motion the colour still shifts but does not pulse.

Out of scope (MVP-1)

  • Per-step timers inside a Routine sequence → post-MVP (Routines feature).
  • Auto-overdue status change on timer expiry → open decision; not built for MVP-1.
  • Timer state sync to the server or other devices → purely local for MVP-1.
  • Admin approval view showing elapsed time → post-MVP.
  • Customisable colour themes for the atom beyond DsTheme tokens → deferred.