c7f148341d
- Undo/Redo in header bar: snapshot-based tracking for block adds, removes, reorders, parameter changes, and model swaps. 50-level undo stack with debounced param snapshots (400ms cooldown). - Main Menu: slide-out sidebar from left with ☰ button in status bar. Items: Save Preset, Bypass/Control, Global Settings, Global EQ, Tuner, Wi-Fi / Bluetooth. - Global Settings: full-screen overlay with grouped settings — Audio (Master Volume, Input Pad, Impedance), MIDI (Channel, Clock, Thru), Display (Brightness, Auto Dim, Screen Saver), Tempo (Tap Tempo, Division, Mute). - Global EQ: 3-band shelving equalizer (Low 80Hz, Mid 800Hz, High 6.4kHz) with large knobs, canvas-based frequency response graph, and reset button. - New files: src/MainMenu.jsx, src/GlobalSettings.jsx, src/GlobalEQ.jsx
141 lines
4.9 KiB
React
141 lines
4.9 KiB
React
import { useRef, useEffect } from "react";
|
||
|
||
const T = {
|
||
bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32",
|
||
amber: "#E8A030", amberDim: "#7A5218", blue: "#3A7BA8", blueDim: "#1E4060",
|
||
green: "#3AB87A", red: "#C84040", textPrimary: "#F0EDE6", textSec: "#8888A0", textDim: "#444458",
|
||
};
|
||
|
||
const MENU_ITEMS = [
|
||
{ id: "save", label: "Save Preset", icon: "💾", desc: "Save current preset to bank" },
|
||
{ id: "bypass", label: "Bypass/Control", icon: "🔌", desc: "Footswitch & pedal assignment" },
|
||
{ id: "settings", label: "Global Settings",icon: "⚙", desc: "Master volume, input, MIDI, display" },
|
||
{ id: "eq", label: "Global EQ", icon: "📊", desc: "Low, Mid, High shelving EQ" },
|
||
{ id: "tuner", label: "Tuner", icon: "🎵", desc: "Instrument tuner" },
|
||
{ id: "wifi", label: "Wi-Fi / Bluetooth",icon: "📶", desc: "Wireless & network settings" },
|
||
];
|
||
|
||
export default function MainMenu({ onClose, onItemSelect }) {
|
||
const overlayRef = useRef(null);
|
||
|
||
useEffect(() => {
|
||
const el = overlayRef.current;
|
||
if (!el) return;
|
||
// Trigger enter animation
|
||
requestAnimationFrame(() => { el.style.transform = "translateX(0)"; });
|
||
}, []);
|
||
|
||
const handleBackdropClick = (e) => {
|
||
if (e.target === e.currentTarget) onClose?.();
|
||
};
|
||
|
||
return (
|
||
<div
|
||
ref={overlayRef}
|
||
onClick={handleBackdropClick}
|
||
style={{
|
||
position: "fixed", inset: 0, zIndex: 999,
|
||
display: "flex",
|
||
background: "rgba(0,0,0,0.55)",
|
||
transition: "background .2s ease",
|
||
}}
|
||
>
|
||
{/* Sidebar */}
|
||
<div
|
||
style={{
|
||
width: 260, maxWidth: "75vw", height: "100%",
|
||
background: T.panel,
|
||
borderRight: `1px solid ${T.border}`,
|
||
display: "flex", flexDirection: "column",
|
||
boxShadow: "4px 0 24px rgba(0,0,0,0.5)",
|
||
transform: "translateX(-100%)",
|
||
transition: "transform .22s cubic-bezier(.22,.61,.36,1)",
|
||
}}
|
||
>
|
||
{/* Header */}
|
||
<div style={{
|
||
padding: "14px 16px",
|
||
borderBottom: `1px solid ${T.border}`,
|
||
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||
flexShrink: 0,
|
||
}}>
|
||
<div style={{ fontSize: 14, fontWeight: 600, color: T.textPrimary }}>
|
||
☰ Menu
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
style={{
|
||
width: 28, height: 28, borderRadius: 6,
|
||
display: "flex", alignItems: "center", justifyContent: "center",
|
||
background: T.surface, border: `1px solid ${T.border}`,
|
||
color: T.textSec, fontSize: 13, cursor: "pointer",
|
||
}}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
{/* Menu items */}
|
||
<div style={{
|
||
flex: 1, overflowY: "auto", padding: "8px 0",
|
||
}}>
|
||
{MENU_ITEMS.map((item) => (
|
||
<button
|
||
key={item.id}
|
||
onClick={() => onItemSelect?.(item.id)}
|
||
style={{
|
||
width: "100%", display: "flex", alignItems: "center", gap: 12,
|
||
padding: "12px 16px",
|
||
background: "none", border: "none", cursor: "pointer",
|
||
textAlign: "left", color: T.textPrimary,
|
||
transition: "background .1s",
|
||
borderBottom: `1px solid ${T.border}33`,
|
||
}}
|
||
onMouseEnter={(e) => { e.currentTarget.style.background = T.surface; }}
|
||
onMouseLeave={(e) => { e.currentTarget.style.background = "none"; }}
|
||
>
|
||
<div style={{
|
||
width: 34, height: 34, borderRadius: 8,
|
||
display: "flex", alignItems: "center", justifyContent: "center",
|
||
background: T.surface, border: `1px solid ${T.border}`,
|
||
fontSize: 16, flexShrink: 0,
|
||
}}>
|
||
{item.icon}
|
||
</div>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div style={{
|
||
fontSize: 13, fontWeight: 600,
|
||
color: T.textPrimary,
|
||
}}>
|
||
{item.label}
|
||
</div>
|
||
<div style={{
|
||
fontSize: 10, color: T.textDim, marginTop: 1,
|
||
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
|
||
}}>
|
||
{item.desc}
|
||
</div>
|
||
</div>
|
||
<span style={{ color: T.textDim, fontSize: 11 }}>›</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
<div style={{
|
||
padding: "12px 16px",
|
||
borderTop: `1px solid ${T.border}`,
|
||
flexShrink: 0,
|
||
}}>
|
||
<div style={{
|
||
fontSize: 9, color: T.textDim, textAlign: "center",
|
||
letterSpacing: ".06em",
|
||
}}>
|
||
Helix Stadium v0.1
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|