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
| # | Slice | Norman level | IxDF techniques | Files |
|---|---|---|---|---|
| 1 | Coach voice microcopy pass | Reflective | 4, 5, 10 | ~6 view files + errors |
| 2 | Session complete celebration moment | Reflective | 7, 8 | TimingView.swift finishedView |
| 3 | Loading + saving state animation (phased) | Visceral | 9 | TimingView.swift savingView, SessionReviewView.swift |
| 4 | Login first-impression (signature personality) | Reflective | 1, 2 | LoginView.swift |
| 5 | Coverage 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.
Verifies the plan is targeting the right gaps. Each surface × each Norman level × current rating.
| Surface | Visceral | Behavioral | Reflective | Notes |
|---|---|---|---|---|
TimingView | Strong | Strong | Light | v1.16 micro-interactions landed LAP/START/STOP well; the "session complete" moment is the gap |
SessionReviewView | Medium | Strong | Medium | Filter tabs animate; rows lack reflective voice; Confirm button's wording is functional, not celebratory |
LoginView | Light | Strong | Light | No personality in the first impression |
TodayView | Medium | Strong | Light | Greeting is generic; date section is functional |
SwimmersListView | Medium | Strong | Light | Roster has no voice; + and destructive actions need celebratory/care copy |
WorkoutCard | Medium | Strong | Medium | AM badge breathes; title is clinical |
EmptyStateView | Medium | Strong | Light | Icon breathes; copy is generic |
AuthTextField | Medium | Strong | Light | No 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.
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:
.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.
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.
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.).
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.
TodayView, 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.
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):
| Time | Element | Behavior |
|---|---|---|
| 0.0s | Both | Invisible, scaled to 0.6 |
| 0.0-0.4s | Circle | Stroke draws in (trim(from: 0, to: 1)) |
| 0.2-0.6s | Tick | Stroke draws in (overlapping circle) |
| 0.6-1.1s | Both | Stay visible, gentle breathing pulse |
| 1.1-1.7s | Both | Fade 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 toTodayView. Implementation in
TickMarkCelebrationView.swift+
SessionReviewView.swift(handleConfirmTap + alert "ConfirmAnyway" path both call
triggerCelebration()which centralizesthe show + sleep + dismiss sequence). The TickMarkCelebrationView
overlay sits on top of the navigation bar via
.zIndex(1000)sothe 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.
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.
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:
letter-by-letter (or fades in via a mask) over 0.6s on first appear. Subtle, but the coach notices it.
verify it lands here.
AuthTextFieldis 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.
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.
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.
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.
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+.
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.
Each slice ends with:
swiftc -parse cross-file, zero errors what couldn't be verified, why — see references/working-previously-correction-pattern.md for the format)
decisions are non-obvious
component)
Total: ~5-6 hours, broken across however many sessions the user wants. Per-slice commits keep each session's work reviewable.
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
plans/session-review-design-spec.md + 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.