Skip to content

feat: zen mode — auto-hide chrome during a running breathing session - #33

Draft
abiassi wants to merge 1 commit into
mainfrom
feat/zen-mode-on-play
Draft

feat: zen mode — auto-hide chrome during a running breathing session#33
abiassi wants to merge 1 commit into
mainfrom
feat/zen-mode-on-play

Conversation

@abiassi

@abiassi abiassi commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What this does

Adds zen mode to the breathing experience. When a session is running, the interface chrome fades away so only the orb and its breathing visuals remain. The chrome comes back the moment the user shows intent, then quietly hides again after a few seconds of stillness.

Chrome that fades (all inside the Resonance header): the language switcher (EN), Sign up / account menu, and the settings gear. The orb (<Visualizer>), its phase label (INHALE / HOLD / …), and the ambient particle background stay.

Reveal / auto-hide behavior

  • Enter zen: ~3.5s after the session starts (or after the last activity), the chrome fades out.
  • Reveal on intent:
    • Desktop — mousemove, pointermove, pointerdown, wheel, keydown.
    • Phone — touchstart, plus device motion: a deliberate jolt of the phone reveals the chrome (best-effort; see caveat).
  • Re-hide: after ~3.5s of stillness while the session is still running.
  • Always shown when: the session is paused/stopped, the settings sheet is open, or the account menu is open.

How it reuses / extends the existing immersive mechanism

The codebase already had an immersive?: boolean prop on Resonance and a body[data-resonance-immersive] / --immersive-color background-tint effect keyed off the running session — but it was never wired to any page and never hid chrome. Rather than add a parallel special case, zen mode builds on that same running-session signal and makes chrome-hiding the default in-session behavior, adding the reveal + inactivity-timer layer the original mechanism was missing. The existing background-tint effect is left intact.

Accessibility

  • Fades use opacity + pointer-events, never display:none — controls stay in the tab order and reachable.
  • No focus trap: a keydown (including Tab) reveals the chrome before focus lands, and onFocusCapture on the header re-reveals as a backstop.
  • While hidden, the header is pointer-events: none so it never intercepts clicks meant for the orb.
  • Respects prefers-reduced-motion: the ~1.1s calm fade collapses to a near-instant 200ms transition.

Verification

Verified in a real browser (claude-in-chrome on mbp16, over Tailscale) on /box-breathing-app:

  1. Start → chrome fades: after starting a session, header inline style went to opacity: 0; pointer-events: none; screenshot confirmed orb-only ("HOLD" showing, EN/Sign up/gear gone).
  2. Reveal: a real mousemove returned the header to opacity: 1; pointer-events: auto. touchstart likewise revealed.
  3. Auto-hide: after ~4s of stillness the header re-hid (opacity: 0, data-zen-hidden="true").
  4. Device motion: a tiny gravity-noise reading did not reveal (below threshold); a deliberate jolt did — confirming the jolt threshold works.
  5. Pause: clicking the orb to pause restored the chrome and it stayed shown through inactivity.

tsc --noEmit clean. next build passes (exit 0). No console errors.

Taste-calls for review

  • Landing-page hero copy is intentionally left visible. On pages like /box-breathing-app, a marketing hero heading ("Box breathing app: free online 4-4-4-4 timer") sits over the canvas as a sibling of <Resonance>, outside the component. Zen mode fades only the in-session chrome the request enumerated (header/controls), not that page-level copy. Fading it too would make a more complete "orb-only" moment but means touching ~10 landing pages with differing hero markup (all-or-nothing to stay consistent). Flagging as a follow-up rather than guessing — happy to do it if wanted.
  • Timings: 3.5s inactivity delay, 1.1s fade. Tuned to feel calm; easy to adjust.
  • Device motion on iOS: iOS 13+ gates devicemotion behind an explicit permission prompt. I deliberately do not trigger that prompt (intrusive during a calming session), so on iOS the reveal relies on touch/tap; motion-reveal works on Android/desktop sensors. The MCP browser viewport was locked at desktop width, so the touch/motion paths were verified via dispatched DOM events rather than a true mobile screenshot.

https://claude.ai/code/session_01TqA4N19mpP7BsPxCQbMMH8

When a breathing session is running, fade out the interface chrome (header:
language switcher, sign-up / account menu, settings gear) so only the orb and
its breathing visuals remain. Any sign of intent — mouse move, pointer/scroll,
key press, touch, or a deliberate device-motion jolt — brings the chrome back,
and it auto-hides again after ~3.5s of stillness. Chrome is always shown when
the session is paused/stopped, when the settings sheet is open, or when the
account menu is open.

Reuses the running-session signal that the existing (never-wired) `immersive`
mechanism was built around, but makes zen the default in-session behavior and
adds the reveal/auto-hide layer it was missing.

Accessibility: fades via opacity + pointer-events (never display:none), so
controls stay in the tab order; a keydown reveals before focus lands and
onFocusCapture re-reveals, so keyboard users are never trapped. Respects
prefers-reduced-motion by collapsing the fade to a near-instant transition.

Claude-Session: https://claude.ai/code/session_01TqA4N19mpP7BsPxCQbMMH8
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
deepbreathing-tmmj Ready Ready Preview, Comment Jul 6, 2026 7:07am

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant