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:
2026-06-12 19:54:53 -04:00
parent a4f7d791bc
commit c7f148341d
4 changed files with 1052 additions and 6 deletions
+140
View File
@@ -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>
);
}