import { useEffect } from 'react';
import { useStore } from '@nanostores/react';
import { TopBar } from './TopBar';
import { MobileMenu } from './MobileMenu';
import { EditorPanel } from './EditorPanel';
import { ShaderLibrary } from './ShaderLibrary';
import { HelpPopup } from './HelpPopup';
import { WelcomePopup } from './WelcomePopup';
import { ShaderCanvas } from './ShaderCanvas';
import { PerformanceWarning } from './PerformanceWarning';
import { uiState, showUI } from '../stores/ui';
import { $appSettings, updateAppSettings, cycleValueMode, cycleRenderMode, handleTapTempo } from '../stores/appSettings';
import { $shader } from '../stores/shader';
import { loadShaders } from '../stores/library';
import { Storage } from '../Storage';
import { LucideIcon } from '../hooks/useLucideIcon';
export function App() {
const ui = useStore(uiState);
const settings = useStore($appSettings);
const shader = useStore($shader);
useEffect(() => {
// Load initial settings from storage
const savedSettings = Storage.getSettings();
$appSettings.set(savedSettings);
// Load saved shaders
loadShaders();
// Set CSS custom property for UI opacity
document.documentElement.style.setProperty(
'--ui-opacity',
(settings.uiOpacity ?? 0.3).toString()
);
}, []);
useEffect(() => {
// Update CSS custom property when opacity changes
document.documentElement.style.setProperty(
'--ui-opacity',
(settings.uiOpacity ?? 0.3).toString()
);
}, [settings.uiOpacity]);
// Keyboard controls for hue shift and value mode when editor not focused
useEffect(() => {
let lastKeyTime = 0;
const DEBOUNCE_DELAY = 150; // ms between key presses
const handleKeyDown = (e: KeyboardEvent) => {
// Only activate if editor is not focused and no control/meta/alt keys are pressed
const editorElement = document.getElementById('editor') as HTMLTextAreaElement;
const isEditorFocused = editorElement && document.activeElement === editorElement;
if (isEditorFocused || e.ctrlKey || e.metaKey || e.altKey) {
return;
}
// Debounce rapid key repeats
const now = Date.now();
if (now - lastKeyTime < DEBOUNCE_DELAY) {
e.preventDefault();
return;
}
lastKeyTime = now;
switch (e.key) {
case 'ArrowLeft':
e.preventDefault();
// Decrease hue shift by 10 degrees (wrapping at 0)
const currentHue = settings.hueShift ?? 0;
const newHueLeft = currentHue - 10;
updateAppSettings({ hueShift: newHueLeft < 0 ? 360 + newHueLeft : newHueLeft });
break;
case 'ArrowRight':
e.preventDefault();
// Increase hue shift by 10 degrees (wrapping at 360)
const currentHueRight = settings.hueShift ?? 0;
const newHueRight = (currentHueRight + 10) % 360;
updateAppSettings({ hueShift: newHueRight });
break;
case 'ArrowUp':
e.preventDefault();
if (e.shiftKey) {
// Shift + Up: Cycle to previous render mode (color palette)
cycleRenderMode('backward');
} else {
// Up: Cycle to previous value mode
cycleValueMode('backward');
}
break;
case 'ArrowDown':
e.preventDefault();
if (e.shiftKey) {
// Shift + Down: Cycle to next render mode (color palette)
cycleRenderMode('forward');
} else {
// Down: Cycle to next value mode
cycleValueMode('forward');
}
break;
case ' ':
e.preventDefault();
// Spacebar: Tap tempo to control time speed
handleTapTempo();
break;
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [settings.hueShift]);
// Save settings changes to localStorage
useEffect(() => {
Storage.saveSettings({
resolution: settings.resolution,
fps: settings.fps,
renderMode: settings.renderMode,
valueMode: settings.valueMode,
uiOpacity: settings.uiOpacity,
hueShift: settings.hueShift,
timeSpeed: settings.timeSpeed,
lastShaderCode: shader.code,
});
}, [settings, shader.code]);
return (
<>
{ui.uiVisible ? (
<>
{!ui.mobileMenuOpen && }
>
) : (
<>
>
)}
>
);
}