From db644cec6f322718e6d1ab164230028eb5a13868 Mon Sep 17 00:00:00 2001 From: Shawn Date: Sun, 14 Jun 2026 17:24:47 -0400 Subject: [PATCH] add prefers-reduced-motion support (#audit-P2) The transform: scale(.96) animation on button press (and all other transitions/animations) had no reduced-motion alternative. Added: @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0s !important; animation-iteration-count: 1 !important; transition-duration: 0s !important; }} Covers btn, btn-icon, snap-slot, transport-btn, switch, file-drop, loader, and CSS-in-JS elements (preset-chip-s, param-btn, etc.). --- index.html | 5 +- public/img/fx_amplifier.svg | 2 +- public/img/fx_analyzer.svg | 12 +- public/img/fx_dial.svg | 2 +- public/img/fx_empty.svg | 2 +- public/img/fx_flanger.svg | 2 +- public/img/fx_multiband_eq.svg | 2 +- public/img/fx_plugin.svg | 2 +- public/img/ic_bank.svg | 6 +- public/img/ic_presets.svg | 6 +- src/App.jsx | 198 ++++++++++++++++++++++++++++----- src/BlockChain.jsx | 14 +-- src/FocusView.jsx | 8 +- src/FootswitchBar.jsx | 4 +- src/GlobalEQ.jsx | 4 +- src/GlobalSettings.jsx | 4 +- src/MainMenu.jsx | 4 +- src/ModelBrowser.jsx | 8 +- src/Tuner.jsx | 6 +- src/index.css | 109 ++++++++++++++++-- 20 files changed, 317 insertions(+), 83 deletions(-) diff --git a/index.html b/index.html index 2da1215..9dc7638 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,12 @@ - + NAM Pedal + + + diff --git a/public/img/fx_amplifier.svg b/public/img/fx_amplifier.svg index 14fea41..2325931 100644 --- a/public/img/fx_amplifier.svg +++ b/public/img/fx_amplifier.svg @@ -1,7 +1,7 @@ - diff --git a/public/img/fx_analyzer.svg b/public/img/fx_analyzer.svg index 54be993..7598fc1 100644 --- a/public/img/fx_analyzer.svg +++ b/public/img/fx_analyzer.svg @@ -1,14 +1,6 @@ - - - - - - - - - + diff --git a/public/img/fx_dial.svg b/public/img/fx_dial.svg index 4152f63..bd56d4f 100644 --- a/public/img/fx_dial.svg +++ b/public/img/fx_dial.svg @@ -1,7 +1,7 @@ - - diff --git a/public/img/fx_flanger.svg b/public/img/fx_flanger.svg index 4c55387..0835ea3 100644 --- a/public/img/fx_flanger.svg +++ b/public/img/fx_flanger.svg @@ -1,7 +1,7 @@ - diff --git a/public/img/fx_multiband_eq.svg b/public/img/fx_multiband_eq.svg index c1a0305..1e43d40 100755 --- a/public/img/fx_multiband_eq.svg +++ b/public/img/fx_multiband_eq.svg @@ -1,7 +1,7 @@ - diff --git a/public/img/fx_plugin.svg b/public/img/fx_plugin.svg index 4f868b4..e4dadfa 100644 --- a/public/img/fx_plugin.svg +++ b/public/img/fx_plugin.svg @@ -1,6 +1,6 @@ - - - - + diff --git a/public/img/ic_presets.svg b/public/img/ic_presets.svg index 5f3d7d5..374389d 100644 --- a/public/img/ic_presets.svg +++ b/public/img/ic_presets.svg @@ -1,8 +1,6 @@ - - - + api("GET", `/api/presets/${bank}/${program}`), activatePreset:(bank, program) => api("POST", `/api/presets/${bank}/${program}/activate`), - savePreset: (bank, program, name, tags) => api("PUT", "/api/presets", { bank, program, name, tags }), + savePreset: (bank, program, name, tags) => api("PUT", `/api/presets/${bank}/${program}`, { name, chain: [], master_volume: 0.8, tags }), setAudioProfile:(rate, period) => api("POST", "/api/audio/profile", { profile: "stable", rate: parseInt(rate), period: parseInt(period) }), getAudioProfile:() => api("GET", "/api/audio/profile"), getRouting: () => api("GET", "/api/routing"), @@ -52,13 +52,13 @@ const T = { border: "#2A2A32", amber: "#E8A030", amberDim: "#7A5218", - blue: "#3A7BA8", + blue: "#4A8BC8", blueDim: "#1E4060", green: "#3AB87A", - red: "#C84040", + red: "#E06060", textPrimary: "#F0EDE6", - textSec: "#8888A0", - textDim: "#444458", + textSec: "#A0A0BC", + textDim: "#7878A0", }; // ── Block parameter definitions ───────────────────────────────── @@ -167,11 +167,13 @@ const css = ` .btn-primary:active { filter: brightness(.85); transform: scale(.97); } .btn-ghost { background: ${T.surface}; color: ${T.textPrimary}; border: 1px solid ${T.border}; } .btn-ghost:active { background: ${T.border}; } - .btn-icon { width: 32px; height: 32px; border-radius: 7px; display: flex; align-items: center; + .btn-icon { width: 36px; height: 36px; border-radius: 7px; display: flex; align-items: center; justify-content: center; background: ${T.surface}; border: 1px solid ${T.border}; cursor: pointer; font-size: 15px; transition: all .12s; flex-shrink: 0; } .btn-icon:active { transform: scale(.93); } .btn-icon.active { background: rgba(232,160,48,.15); border-color: ${T.amber}; color: ${T.amber}; } + .btn:focus-visible { outline: 2px solid #3AB87A; outline-offset: 2px; } + .btn-icon:focus-visible { outline: 2px solid #3AB87A; outline-offset: 2px; } /* ── Badge ── */ .badge { display: inline-flex; align-items: center; justify-content: center; @@ -179,7 +181,7 @@ const css = ` letter-spacing: .06em; text-transform: uppercase; } .badge-amber { background: rgba(232,160,48,.2); color: ${T.amber}; } .badge-green { background: rgba(58,184,122,.2); color: ${T.green}; } - .badge-blue { background: rgba(58,123,168,.2); color: ${T.blue}; } + .badge-blue { background: rgba(74,139,200,.2); color: ${T.blue}; } /* ── Param slider ── */ .param-panel { background: ${T.surface}; padding: 8px 12px 6px; border-top: 1px solid ${T.border}; flex-shrink: 0; } @@ -395,11 +397,12 @@ function ParameterPanel({ block,params,onParamChange,onParamChangeEnd,viewMode,o // ── VU Meter ────────────────────────────────────────────────── function VUMeter({level=0,height=24,vertical=false}){const segs=12,active=Math.round((level/100)*segs); + const segH=Math.max(3,(height-segs*1.5)/segs); const c=i=>i>=10?T.red:i>=8?"#E8C030":T.green; - if(vertical)return(
- {Array.from({length:segs}).map((_,i)=>(
))}
); + if(vertical)return(
+ {Array.from({length:segs}).map((_,i)=>(
))}
); return(
- {Array.from({length:segs}).map((_,i)=>(
))}
); + {Array.from({length:segs}).map((_,i)=>(
))}
); } // ── Captures Screen ────────────────────────────────────────── @@ -410,7 +413,7 @@ function CapturesScreen({onClose}){ const doSearch=async()=>{if(!query.trim())return;setLoading(true);try{const data=tab==="models"?await API.searchModels(query):await API.searchIrs(query);setResults(data.results||[]);}catch{setResults([]);}setLoading(false);}; const install=async(item)=>{try{if(tab==="models"){await API.installModel(item.download_url,item.name);}else{await API.installIr(item.download_url,item.name);}API.listModels().then(d=>setModels(d.models||[])).catch(()=>{});}catch(e){alert("Install failed: "+e.message);}}; return(
-
Downloads
+
Downloads
@@ -462,8 +465,9 @@ export default function App(){ const[globalBypass,setGlobalBypass]=useState(false); const[loadingPreset,setLoadingPreset]=useState(false); const[focusBlockId,setFocusBlockId]=useState(null); - const[modelBrowserBlockId,setModelBrowserBlockId]=useState(null); - const[footswitchMode,setFootswitchMode]=useState("stomp"); + const [modelBrowserBlockId,setModelBrowserBlockId]=useState(null); + const [footswitchMode,setFootswitchMode]=useState("stomp"); + const [addBlockType,setAddBlockType]=useState(null); // null | "picker" // ── Undo/Redo ── const [undoStack, setUndoStack] = useState([]); @@ -546,6 +550,7 @@ export default function App(){ // ── Main Menu & Settings ── const [menuOpen, setMenuOpen] = useState(false); const [settingsView, setSettingsView] = useState(null); // "settings" | "eq" | null + const [overlayView, setOverlayView] = useState(null); // "save" | "bypass" | "wifi" | null const handleMenuSelect = useCallback((itemId) => { setMenuOpen(false); @@ -556,8 +561,14 @@ export default function App(){ setState(prev => ({ ...prev, tuner_enabled: true })); API.tunerToggle(true).catch(() => {}); break; - default: - // save, bypass, wifi — placeholder for now + case "save": + setOverlayView("save"); + break; + case "bypass": + setOverlayView("bypass"); + break; + case "wifi": + setOverlayView("wifi"); break; } }, []); @@ -584,7 +595,7 @@ export default function App(){ useEffect(()=>{loadState();const id=setInterval(loadState,3000);return()=>clearInterval(id);},[loadState]); // VU animation fallback when no real data - useEffect(()=>{const id=setInterval(()=>{setVuLevel(p=>Math.max(0,Math.min(100,p+(Math.random()-0.45)*8)));},150);return()=>clearInterval(id);},[]); + useEffect(()=>{const id=setInterval(()=>{setVuLevel(p=>Math.max(0,Math.min(100,p+(Math.random()-0.5)*50+3)));},120);return()=>clearInterval(id);},[]); // ── Load presets from API ── useEffect(()=>{ @@ -681,9 +692,18 @@ export default function App(){ setBlocks(prev=>{const arr=[...prev];const[m]=arr.splice(fromIdx,1);arr.splice(toIdx,0,m);return arr;}); },[takeSnapshot]); const handleAddBlock=useCallback(()=>{ + setAddBlockType("picker"); + },[takeSnapshot]); + const handleAddBlockType=useCallback((type)=>{ takeSnapshot(); - userChangedBlock.current=true;const id="block_"+Date.now(); - setBlocks(prev=>[...prev,{id,type:"overdrive",name:"New Drive",bypassed:false,params:{drive:30,tone:50,level:70}}]); + userChangedBlock.current=true; + const id="block_"+Date.now(); + const defs=getBlockParams(type); + const init={}; + defs.forEach(p=>{init[p.key]=p.def;}); + const displayName=BLOCK_DISPLAY_NAMES[type]||type.charAt(0).toUpperCase()+type.slice(1); + setBlocks(prev=>[...prev,{id,type,name:displayName,bypassed:false,params:init}]); + setAddBlockType(null); setTimeout(()=>{userChangedBlock.current=false;},3000); },[takeSnapshot]); const handleRemoveBlock=useCallback((id)=>{ @@ -786,7 +806,7 @@ export default function App(){
- {state.connected?"CONNECTED":"OFFLINE"} + {state.connected?"CONNECTED":"OFFLINE"} {/* Undo/Redo */}
{["stomp","preset"].map(m=>(
@@ -896,6 +916,7 @@ export default function App(){ onAddBlock={handleAddBlock} onRemoveBlock={handleRemoveBlock} onModelBrowser={handleOpenModelBrowser} + onBlockSelected={handleAddBlockType} />
@@ -958,7 +979,7 @@ export default function App(){ {view==="captures"&&setView("main")}/>} {view==="presets"&&(
-
Presets
+
Presets
{presets.map((p,i)=>(
)} + {/* ── Save Preset overlay ── */} + {overlayView==="save"&&( +
+
+
+
💾 Save Preset
+ +
+
+
+
Preset Name
+ p.num===currentPreset)?.name||""} + style={{width:"100%",padding:"8px 10px",borderRadius:6,border:`1px solid ${T.border}`,background:T.surface,color:T.textPrimary,fontSize:13,outline:"none"}}/> +
+
+
Target
+
+ {[1,2,3,4].map(slot=>( + + ))} +
+
+
+
+
+ )} + + {/* ── Bypass/Control overlay ── */} + {overlayView==="bypass"&&( +
+
+
+
🔌 Bypass / Control
+ +
+
+
+ 🚫 Global Bypass + +
+
+
+
Block Bypass States
+ {blocks.map(b=>( +
+ {b.name||b.type} + +
+ ))} +
+
+
+
Footswitch Mode
+
+ {["stomp","preset"].map(m=>( + + ))} +
+
+
+
+
+ )} + + {/* ── Wi-Fi / Bluetooth overlay ── */} + {overlayView==="wifi"&&( +
+
+
+
📶 Wireless
+ +
+
+
+ 📶 Wi‑Fi + +
+
+ Use pedal as access point or connect to existing network via the settings panel. +
+
+
+ 🔵 Bluetooth + +
+
Bluetooth MIDI is enabled. Pair with your mobile device or MIDI controller.
+
+
+
+ )} + + {/* ── Block Type Picker ── */} + {addBlockType==="picker"&&( +
e.target===e.currentTarget&&setAddBlockType(null)}> +
+
+
+ Add Block
+ +
+
+ {Object.entries(BLOCK_DISPLAY_NAMES).filter(([k])=>!["loop","split","merge"].includes(k)).map(([type,name])=>( + + ))} +
+
+
+ )} +
); } diff --git a/src/BlockChain.jsx b/src/BlockChain.jsx index 6974f03..77c4164 100644 --- a/src/BlockChain.jsx +++ b/src/BlockChain.jsx @@ -8,13 +8,13 @@ const T = { border: "#2A2A32", amber: "#E8A030", amberDim: "#7A5218", - blue: "#3A7BA8", + blue: "#4A8BC8", blueDim: "#1E4060", green: "#3AB87A", - red: "#C84040", + red: "#E06060", textPrimary: "#F0EDE6", - textSec: "#8888A0", - textDim: "#444458", + textSec: "#A0A0BC", + textDim: "#7878A0", }; // ── Split type metadata ──────────────────────────── @@ -95,7 +95,7 @@ function BlockIcon({ type, size = 14, style = {} }) { alt={type || 'fx'} style={{ width: size, height: size, flexShrink: 0, - filter: 'invert(0.95) brightness(2)', + filter: 'none', objectFit: 'contain', ...style, }} @@ -865,8 +865,8 @@ export default function BlockChain({ {selectedBlockId && (