feat: Undo/Redo + Main Menu + Global Settings + Global EQ
- 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
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user