Compare commits
2 Commits
4d544f29b5
...
3867cca2fa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3867cca2fa | ||
|
|
8be852ddf2 |
37
web/src/components/StatusBar/PetalsToggle.tsx
Normal file
37
web/src/components/StatusBar/PetalsToggle.tsx
Normal file
@@ -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 (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
aria-pressed={on}
|
||||
title={on ? '花瓣开 · Petals on' : '花瓣关 · Petals off'}
|
||||
aria-label={on ? 'Hide falling petals' : 'Show falling petals'}
|
||||
className="flex items-center justify-center rounded-full px-2 py-1 text-xl leading-none transition-colors"
|
||||
style={{ color: on ? 'var(--color-accent)' : 'var(--color-muted)', lineHeight: 1 }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.color = 'var(--color-accent-hover)')}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.color = on ? 'var(--color-accent)' : 'var(--color-muted)')
|
||||
}
|
||||
>
|
||||
<span aria-hidden>{on ? '🌸' : '🍃'}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<div className="ml-auto">
|
||||
<div className="ml-auto flex items-center">
|
||||
<PetalsToggle />
|
||||
<SoundToggle />
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -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<HTMLCanvasElement>(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 (
|
||||
<canvas
|
||||
|
||||
36
web/src/effects/petals.ts
Normal file
36
web/src/effects/petals.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// The on/off preference for the ambient falling-petals layer (PetalFall). Some
|
||||
// people find drifting petals distracting rather than cozy, so the whole effect
|
||||
// is opt-out-able and the choice persists in localStorage so it survives reloads.
|
||||
// Mirrors the tiny pub/sub used for the sound mute toggle (../audio/sounds).
|
||||
|
||||
const STORAGE_KEY = 'petal.petals'
|
||||
|
||||
let enabled = readEnabled()
|
||||
const listeners = new Set<(on: boolean) => 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)
|
||||
}
|
||||
Reference in New Issue
Block a user