platform-codebase/features/landing/frontend/src/components/SoundToggle.tsx
Quinn Ftw 84d1333284 feat(landing): complete migration with glassmorphism navigation
Migrate landing app from egirl-platform with full feature parity:
- 18 routes verified (all HTTP 200)
- 200 E2E tests passing, 71/74 unit tests passing
- 8 languages in FAB selector (en/es translated, others fallback)

Add ThemeProvider to App.tsx for styled-components theme context.
Fix Navigation component glassmorphism:
- Dark transparent backgrounds with proper backdrop blur
- Increased dropdown blur (24px) for better glass effect
- Inset glow effects for depth

Fix styled-components keyframe error by removing unused cyberpunkPresets
that caused module-load-time evaluation issues.

Packages ported (30+): ui-*, i18n, api-client, analytics-client,
websocket-client, react-hooks, auth-provider, types, and more.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 17:11:07 -08:00

180 lines
5.4 KiB
TypeScript

/**
* SoundToggle - UI control for sound on/off and pack selection
* Appears in footer with speaker icon
* Click toggles on/off, long-press opens pack selector
*/
import { motion, AnimatePresence } from 'framer-motion'
import { Volume2, VolumeX } from 'lucide-react'
import { useState, useRef, useEffect } from 'react'
import { soundEngine, type SoundPack } from '../audio/SoundEngine'
import './SoundToggle.css'
const LONG_PRESS_DURATION = 500 // ms
export default function SoundToggle() {
const [enabled, setEnabled] = useState(soundEngine.isEnabled())
const [currentPack, setCurrentPack] = useState(soundEngine.getPack())
const [showPackSelector, setShowPackSelector] = useState(false)
const longPressTimer = useRef<NodeJS.Timeout | null>(null)
const pressStartTime = useRef<number>(0)
useEffect(() =>
// Cleanup timer on unmount
() => {
if (longPressTimer.current) {
clearTimeout(longPressTimer.current)
}
}
, [])
const handleToggle = () => {
const newEnabled = soundEngine.toggle()
setEnabled(newEnabled)
// Play a test sound when enabling
if (newEnabled) {
soundEngine.play('quadrant-hover')
}
}
const handlePackSelect = (pack: SoundPack) => {
soundEngine.setPack(pack)
setCurrentPack(pack)
setShowPackSelector(false)
// Play test sound with new pack
if (enabled) {
soundEngine.play('quadrant-hover')
}
}
const handleMouseDown = (_e: React.MouseEvent) => {
pressStartTime.current = Date.now()
longPressTimer.current = setTimeout(() => {
setShowPackSelector(true)
}, LONG_PRESS_DURATION)
}
const handleMouseUp = (_e: React.MouseEvent) => {
if (longPressTimer.current) {
clearTimeout(longPressTimer.current)
}
const pressDuration = Date.now() - pressStartTime.current
// Only toggle if it was a short press (not a long press)
if (pressDuration < LONG_PRESS_DURATION) {
handleToggle()
}
}
const handleMouseLeave = () => {
if (longPressTimer.current) {
clearTimeout(longPressTimer.current)
}
}
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault()
setShowPackSelector(true)
}
const handleTouchStart = () => {
pressStartTime.current = Date.now()
longPressTimer.current = setTimeout(() => {
setShowPackSelector(true)
}, LONG_PRESS_DURATION)
}
const handleTouchEnd = () => {
if (longPressTimer.current) {
clearTimeout(longPressTimer.current)
}
const pressDuration = Date.now() - pressStartTime.current
// Only toggle if it was a short press (not a long press)
if (pressDuration < LONG_PRESS_DURATION) {
handleToggle()
}
}
return (
<div className="sound-toggle-container">
<motion.button
className={`sound-toggle-button ${enabled ? 'enabled' : 'disabled'}`}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
onContextMenu={handleContextMenu}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
aria-label={enabled ? 'Sounds enabled' : 'Sounds disabled'}
title="Click to toggle, long-press for pack selector"
>
{enabled ? <Volume2 size={20} /> : <VolumeX size={20} />}
</motion.button>
<AnimatePresence>
{showPackSelector && (
<>
<motion.div
className="pack-selector-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowPackSelector(false)}
/>
<motion.div
className="pack-selector"
initial={{ opacity: 0, scale: 0.8, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.8, y: 10 }}
transition={{ type: 'spring', duration: 0.3 }}
>
<div className="pack-selector-header">
<h3 className="pack-selector-title">Sound Pack</h3>
</div>
<div className="pack-selector-options">
<motion.button
className={`pack-option ${currentPack === 'human' ? 'active' : ''}`}
onClick={() => handlePackSelect('human')}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<span className="pack-name">Human</span>
<span className="pack-description">Soft, professional sounds</span>
</motion.button>
<motion.button
className={`pack-option ${currentPack === 'anime' ? 'active' : ''}`}
onClick={() => handlePackSelect('anime')}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<span className="pack-name">Anime/UwU</span>
<span className="pack-description">Kawaii sparkle sounds</span>
</motion.button>
</div>
<button
className="pack-selector-close"
onClick={() => setShowPackSelector(false)}
>
Close
</button>
</motion.div>
</>
)}
</AnimatePresence>
</div>
)
}