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) +}