Hydrolyze Emotional Design Implementation Plan

Writing · refreshed Search related

User signal (verbatim, typo preserved): "Lets create an

implementation plan to breath some emotional design into hydrolyze."

(2026-06-20.)

Source framework: Emotional Design (IxDF 2026 +

Norman three-level hierarchy). Continues the work started in

Hydrolyze Feel Direction Playful V1 and

Hydrolyze Micro Interaction System.

The plan at a glance

#SliceNorman levelIxDF techniquesFiles
1Coach voice microcopy passReflective4, 5, 10~6 view files + errors
2Session complete celebration momentReflective7, 8TimingView.swift finishedView
3Loading + saving state animation (phased)Visceral9TimingView.swift savingView, SessionReviewView.swift
4Login first-impression (signature personality)Reflective1, 2LoginView.swift
5Coverage matrix + reflective-layer audit(verification)doc

One commit per slice. swiftc -parse per slice. Real xcodebuild on the Mac Studio when the iCloud dataless issue clears.

Coverage matrix (initial state, BEFORE this plan)

Verifies the plan is targeting the right gaps. Each surface × each Norman level × current rating.

SurfaceVisceralBehavioralReflectiveNotes
TimingViewStrongStrongLightv1.16 micro-interactions landed LAP/START/STOP well; the "session complete" moment is the gap
SessionReviewViewMediumStrongMediumFilter tabs animate; rows lack reflective voice; Confirm button's wording is functional, not celebratory
LoginViewLightStrongLightNo personality in the first impression
TodayViewMediumStrongLightGreeting is generic; date section is functional
SwimmersListViewMediumStrongLightRoster has no voice; + and destructive actions need celebratory/care copy
WorkoutCardMediumStrongMediumAM badge breathes; title is clinical
EmptyStateViewMediumStrongLightIcon breathes; copy is generic
AuthTextFieldMediumStrongLightNo voice in errors

The pattern: behavioral is solid everywhere (the existing information-rich effortlessness design system holds). Visceral is partial (the playful direction work landed the primary CTAs; the secondary surfaces still lag). Reflective is the weakest layer across the board — every surface under-targets it.

Slice 1 — Warmth through motion + interaction (NOT microcopy)

Corrected 2026-06-20 per user (verbatim): "Let's go with 'warm.'

We should try to make it feel welcoming and interactive. We can do

this through the UI animations and how the user interacts with the

app, rather than focusing on the text components." Source:

Warmth Through Motion (the cross-cutting principle).

Direction shift: the original Slice 1 was "coach voice microcopy pass" — replace text everywhere. The user's correction explicitly rejects text as the channel: the warmth comes through motion + interaction, not microcopy. The clinical text conventions stay (HYDROLYZE, REVIEW, LAP, READY, SESSION COMPLETE). The warmth comes from how the app responds.

Targets: the warmth-through-motion layers. Specific motion patterns:

  1. State transition crossfades in TimingView — the `.setup →

.ready → .running → .saving → .finished state machine currently does hard ZStack swaps. Wrap each state change in withAnimation + add .transition(.asymmetric(insertion: .opacity.combined(with: .scale), removal: .opacity))` to each state's view. The transition becomes a smooth crossfade with subtle scale — the app feels like it's responding, not jumping.

  1. Empty state breathing across all surfaces — the v1.16

house-icon breathing pattern in EmptyStateView (TodayView's "no workout" case) is generalized to SwimmersListView's empty state, WorkoutCard when there are no sets, etc. Every "no data" surface breathes.

  1. Reactive button feedback sweep — every primary CTA gets

PressFeedbackStyle or StrongPressFeedbackStyle. Already done in v1.16 for the major surfaces; this is a sweep of the remaining buttons (the inline ✓ accept in SessionReviewView, the +button in SwimmersListView toolbar, etc.).

  1. Login first-impression motion (Slice 4 partial) — the

HYDROLYZE wordmark reveals on first appear (gentle opacity + scale 0.95 → 1.0 over 0.6s). First-impression warmth through motion, NOT through "Welcome back, Coach." text.

  1. Pull-to-refresh with Hydrolyze brandTodayView,

SwimmersListView, SessionReviewView all use the system default pull-to-refresh. Replace with a cyan-tinted custom indicator (or at minimum, set the tint to cyan).

Files touched: ~6 view files for the motion changes. Possibly extract to a PhasedProgressView component if the loading choreography generalizes.

Why this is "warm", not "playful ≠ gamey": all the motion patterns are restrained. No confetti. No bouncing buttons. No playful characters. The motion is welcoming and responsive — the app greets the user by how it responds to their touch, not by what it says. The same principle as the Tick Mark Celebration Pattern: humility in the motion, dignity in the moment.

Out of scope (deferred to a follow-up slice): swipe-gesture personality, matched-geometry transitions between surfaces, custom keyboard animations. These are higher-LOC motion changes that can ship as Slice 6 once Slice 1 establishes the pattern.

Verification: manual preview at each touched surface. The key check: does the surface feel ALIVE without being DISTRACTING? If a coach on a pool deck at 5am would say "the app feels nice" without saying "what's that animation doing," the motion is right.

Slice 2 — Session complete celebration moment

Corrected 2026-06-20 per user (verbatim, typo preserved):

"make sure the celebration is after the coach approves all the

paring engine matches — and maybe don't have any text, just

have a tick mark animation or something."

Source: Tick Mark Celebration Pattern (the reusable

pattern extracted from this correction).

Trigger location (corrected): SessionReviewView.handleConfirmTap() after await viewModel.confirmAll() (or bulkAcceptLikelyCorrect() + confirmAll(), or confirmAll(force: true) from the "Confirm Anyway" alert) resolves successfully. NOT after viewModel.stop() in TimingView. The pairing-engine matches need to be approved first — the celebration should mark the completion of the coach's work, not a mechanical state transition.

Why the trigger matters (the posture-level principle): celebration should mark a data-integrity milestone, not a state transition. Stopping the stopwatch is transient; approving pairing matches is durable ("the data is captured, reviewed, approved, on disk"). The IxDF "memorability" technique (8) only fires when there's something to remember.

The pattern: tick mark animation, no text. Visual sequence (~1.7s total):

TimeElementBehavior
0.0sBothInvisible, scaled to 0.6
0.0-0.4sCircleStroke draws in (trim(from: 0, to: 1))
0.2-0.6sTickStroke draws in (overlapping circle)
0.6-1.1sBothStay visible, gentle breathing pulse
1.1-1.7sBothFade to 0.0 opacity, scale to 0.95

Colors: cyan stroke (#09d4ee) on dark background. Circle and tick both cyan; no fill, just stroke. Stroke width: 3pt.

Haptic: Haptics.success() at the 0.6s mark, when both elements are drawn. The success notification ("ding-ding-rise") is the visceral companion to the visual.

The "no text" decision (user correction): text in a celebration moment competes with the visual for attention. The tick mark is the universal "done" symbol — no language required, reads in any locale, creates a more elegant moment than a coach-voice line. Reserve coach voice for surfaces where the user has time to read (login, empty states, error messages).

Trigger implementation: watch viewModel.hasSavedPairedLaps in SessionReviewView via .onChange(of:). When it becomes true post-confirmAll, set @State var showCelebration = true that overlays the celebration component for ~1.7s, then dismisses the view back to TodayView (or leaves the user on the now-empty review surface — TBD based on user preference).

User decision 2026-06-20: auto-dismiss. The 1.7s tick mark

animation completes, then dismiss() pops the view back to

TodayView. Implementation in TickMarkCelebrationView.swift +

SessionReviewView.swift (handleConfirmTap + alert "Confirm

Anyway" path both call triggerCelebration() which centralizes

the show + sleep + dismiss sequence). The TickMarkCelebrationView

overlay sits on top of the navigation bar via .zIndex(1000) so

the animation reads as full-screen.

Edge case — alert cancel: if the user cancels the "Unconfirmed Rows" alert on the .withFlagged path, NO celebration. The celebration only fires when confirmAll (or confirmAll(force: true)) actually resolves.

Why this is "playful ≠ gamey": a tick mark draw-in is reminiscent of macOS/iOS's universal "completion" indicator. It's not a confetti rain or a jingle — it's a single acknowledgment that "your work is captured and approved." The humility is the point. IxDF technique 10 ("polite, light-hearted messages") in visual form.

Cross-project reuse: the same pattern applies to Felty's Phoenix badge moment in Felt Faction. When a member earns the Phoenix, a tick mark draw-in is the visceral companion to the reflective Felty narration ("the comeback is complete. The Phoenix rises."). Extract the animation to a shared component from day one so both products can import it.

Files touched: SessionReviewView.swift (handleConfirmTap + the onChange + overlay), new TickMarkCelebrationView.swift component.

Verification: manual preview at the end of a real SessionReview flow. Verify the animation completes in 1.7s ± 0.2s, the success haptic fires at the right moment, the celebration doesn't fire on alert cancel.

Slice 3 — Loading + saving state animation (phased)

Targets: Visceral layer (IxDF technique 9 "storytelling" — the saving state has a narrative arc the coach should see).

The work: Today, TimingView.savingView shows a generic ProgressView() plus a "SAVING" label and the recent saveLog lines. The viewmodel already has currentSaveStep (an enum with .fetching, .pairing, .saving phases — used in the v1.x work). Surface those phases visually.

Pattern:

○━━━●━━━●━━━○
   fetching  pairing   saving

A horizontal three-dot indicator. The active dot is cyan and pulsing (phaseAnimator); the completed dots are filled cyan; the future dots are gray. Below the dots: the current phase name in JetBrains Mono + the actual log line for context.

Same pattern for SessionReviewView loading state — fetching the review rows should also be phased, not a generic spinner.

Why this is visceral, not behavioral: the coach FEELS the app is responsive because they can see what it's doing. The engine isn't stuck; it's working through the phases. The visual narration is the visceral feedback that complements the existing haptic/flash system.

Files touched: TimingView.swift savingView (line 438+), SessionReviewView.swift rowsList loading state (line 92+). Possibly extract to a PhasedProgressView component if the pattern generalizes (Charts views might want it too).

Verification: Manual preview during a real save. Verify the phase indicator updates as currentSaveStep changes. Verify the loading state in SessionReviewView matches the same pattern.

Slice 4 — Login first-impression (signature personality)

Targets: Reflective layer (IxDF techniques 1 "signature personality", 2 "engage as a character"). This is the first surface the coach sees — it's the seed for the reflective layer of every subsequent interaction.

The work: the login screen has the wordmark "HYDROLYZE" already in cyan monospaced, but it's static. Add:

  1. Wordmark reveal — the cyan "HYDROLYZE" text reveals

letter-by-letter (or fades in via a mask) over 0.6s on first appear. Subtle, but the coach notices it.

  1. Coach voice subtitle — already covered by Slice 1, but

verify it lands here.

  1. Soft pulse on empty fields — when an AuthTextField

is empty and unfocused, the cyan border is at 0.6 opacity and gently breathes (0.6 → 1.0 → 0.6, 2s cycle). When focused, full opacity (already done in v1.16). When filled, no pulse. This tells the coach "tap here" without being aggressive.

  1. The 5am pool deck test — the login screen needs to

pass the "I'm squinting at my phone under a 5am pool light" test. The cyan-on-dark is good; verify the pulse animation is visible in low light (cyan #09d4ee on dark #060e18 has ~9.5:1 contrast — should be visible).

Files touched: LoginView.swift, AuthTextField.swift (the pulse pattern lives in the field, not the view).

Verification: Manual preview at multiple times of day if possible. Verify the animation is subtle enough to not distract a coach who's trying to sign in fast.

Slice 5 — Coverage matrix + reflective-layer audit

Targets: Verification only. No new UI.

The work: a CSV or markdown table showing every surface × every Norman level × a 1-5 rating × a note on the gap. Produced BEFORE the slices, used to DRIVE the slice prioritization; updated AFTER the slices to verify the gaps closed.

The output lives at: plans/emotional-design-coverage-matrix.md (sibling to the existing plans/session-review-design-spec.md). Becomes a v1.16 spec doc alongside the existing 8 specs.

Verification: Document review. Use as input to v1.17+ plans.

Risks / trade-offs

with a tunable intensity parameter so the user can dial up/down without a recompile.

Block on user sign-off before shipping. The voice decision propagates to every other slice.

additions need to pass the "5am pool deck, splashing, 24 swimmers" test. Coach-voice error message that takes 5 seconds to read = wrong.

doesn't replace. The visceral/breathing work stays.

slice's parse-check is the local verification; the real xcodebuild requires Mac Studio. Surface this as a blocker, don't try to brute-force.

Out of scope for v1.16 emotional design round

iOS 17+ integration work, separate effort.

voice for v1. The user can opt for voice later.

swimmer") — needs the data flywheel work from v1.13.1 to mature. v2.

room.

(Taptic audio). Defer until iOS 18+ makes this easier.

uplift. Defer to v1.17+.

Cross-cutting posture generalization

The "target all three levels explicitly" principle is now the operating system for ALL the user's product work, not just Hydrolyze. Applied to:

reflective layer (the bank has a personality you trust). Live Activities on the wrist + haptics = the missing visceral layer. Universal score across phases = behavioral layer.

Verification approach (per slice)

Each slice ends with:

  1. swiftc -parse cross-file, zero errors
  2. Two-column honest verification report (what was verified,

what couldn't be verified, why — see references/working-previously-correction-pattern.md for the format)

  1. The coverage matrix (Slice 5) updated with the new rating
  2. Manual preview notes captured as inline comments where the

decisions are non-obvious

Estimated scope

component)

Total: ~5-6 hours, broken across however many sessions the user wants. Per-slice commits keep each session's work reviewable.

Source

breath some emotional design into hydrolyze." (2026-06-20, typo "breath" preserved per signal-detector exact-phrasing rule)

Norman three-level hierarchy)

Hydrolyze Micro Interaction System

+ the 7 other design specs in plans/

issue (2934+ dataless files as of 2026-06-20). Per-slice commits will need iCloud rehydration via brctl evict + brctl download — see the apfs-file-recovery skill.

Published and managed by TARS, an AI co-author built on Nathan's gbrain.