From 8be852ddf2ee8d1afd393d8a7f7013438cf5122d Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:27:00 -0700 Subject: [PATCH] Add a status-bar toggle to disable the falling petals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some people find the drifting ambient petals distracting rather than cozy, so make the whole PetalFall layer opt-out-able from a ๐ŸŒธ toggle sitting just left of the sound toggle in the status bar. Mirrors the existing sound-mute pattern: a tiny localStorage-backed pub/sub (effects/petals.ts) drives a PetalsToggle twin of SoundToggle. PetalFall subscribes to the setting, stops animating and wipes the canvas when off, and the choice persists across reloads. Works in both day (petals) and night (stars) modes. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd --- web/src/components/StatusBar/PetalsToggle.tsx | 37 +++++++++++++++++++ web/src/components/StatusBar/StatusBar.tsx | 4 +- web/src/effects/PetalFall.tsx | 16 +++++++- web/src/effects/petals.ts | 36 ++++++++++++++++++ 4 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 web/src/components/StatusBar/PetalsToggle.tsx create mode 100644 web/src/effects/petals.ts diff --git a/web/src/components/StatusBar/PetalsToggle.tsx b/web/src/components/StatusBar/PetalsToggle.tsx new file mode 100644 index 0000000..83c72c1 --- /dev/null +++ b/web/src/components/StatusBar/PetalsToggle.tsx @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react' +import { isPetalsEnabled, onPetalsEnabledChange, setPetalsEnabled } from '../../effects/petals' + +// A tiny toggle for Petal's ambient falling-blossom layer, sitting just left of +// the sound toggle in the status bar. Some people find the drifting petals +// distracting, so this turns them off entirely. Bilingual tooltip (she reads +// Mandarin first), and the choice persists across reloads. +export function PetalsToggle() { + const [on, setOn] = useState(isPetalsEnabled) + + // Stay in sync if the setting is flipped elsewhere. + useEffect(() => onPetalsEnabledChange(setOn), []) + + const toggle = () => { + const next = !on + setPetalsEnabled(next) + setOn(next) + } + + return ( + + ) +} diff --git a/web/src/components/StatusBar/StatusBar.tsx b/web/src/components/StatusBar/StatusBar.tsx index c9b2f84..01aac4f 100644 --- a/web/src/components/StatusBar/StatusBar.tsx +++ b/web/src/components/StatusBar/StatusBar.tsx @@ -1,6 +1,7 @@ import { Fragment, useEffect, useRef, useState } from 'react' import type { SaveStatus } from '../../hooks/useAutoSave' import { StatsPanel } from './StatsPanel' +import { PetalsToggle } from './PetalsToggle' import { SoundToggle } from './SoundToggle' interface Props { @@ -145,7 +146,8 @@ export function StatusBar({ wordCount, text, saveStatus, checking, voicing, coll )} -
+
+
diff --git a/web/src/effects/PetalFall.tsx b/web/src/effects/PetalFall.tsx index 8d01d92..8dea48a 100644 --- a/web/src/effects/PetalFall.tsx +++ b/web/src/effects/PetalFall.tsx @@ -1,4 +1,5 @@ -import { useEffect, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' +import { isPetalsEnabled, onPetalsEnabledChange } from './petals' // PetalFall is the cozy ambient layer: a fixed, non-interactive 2D canvas that // drifts soft cherry-blossom petals down the page. Each petal is a real @@ -212,8 +213,19 @@ interface Petal { export function PetalFall({ night = false }: { night?: boolean }) { const canvasRef = useRef(null) + const [enabled, setEnabled] = useState(isPetalsEnabled) + + // Let the status-bar toggle switch the ambient layer on/off live. + useEffect(() => onPetalsEnabledChange(setEnabled), []) useEffect(() => { + if (!enabled) { + // User turned the ambient petals off โ€” wipe any frame left on the canvas. + const el = canvasRef.current + el?.getContext('2d')?.clearRect(0, 0, el.width, el.height) + return + } + const reduce = typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches @@ -347,7 +359,7 @@ export function PetalFall({ night = false }: { night?: boolean }) { document.removeEventListener('visibilitychange', onVisibility) petals = [] } - }, [night]) + }, [night, enabled]) return ( void>() + +function readEnabled(): boolean { + try { + return localStorage.getItem(STORAGE_KEY) !== 'off' + } catch { + return true + } +} + +export function isPetalsEnabled(): boolean { + return enabled +} + +export function setPetalsEnabled(on: boolean): void { + enabled = on + try { + localStorage.setItem(STORAGE_KEY, on ? 'on' : 'off') + } catch { + /* private mode โ€” choice just won't persist */ + } + listeners.forEach((fn) => fn(on)) +} + +export function onPetalsEnabledChange(fn: (on: boolean) => void): () => void { + listeners.add(fn) + return () => listeners.delete(fn) +}