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.).
This commit is contained in:
2026-06-14 17:24:47 -04:00
parent cca4aed380
commit db644cec6f
20 changed files with 317 additions and 83 deletions
+4 -1
View File
@@ -2,9 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" />
<meta name="theme-color" content="#0A0A0C" /> <meta name="theme-color" content="#0A0A0C" />
<title>NAM Pedal</title> <title>NAM Pedal</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="/ui/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/ui/favicon.svg" />
</head> </head>
<body> <body>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" <svg fill="#F0EDE6" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px"
height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve"> height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
<g id="Layer_1"> <g id="Layer_1">
<g> <g>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+2 -10
View File
@@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="48px"
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> height="48px" viewBox="0 0 48 48" fill="#F0EDE6">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px"
height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
<g id="Layer_1" display="none">
<path display="inline" d="M47,41c0,3.313-2.687,6-6,6H7c-3.313,0-6-2.687-6-6V7c0-3.313,2.687-6,6-6h34c3.313,0,6,2.687,6,6V41z"/>
<line display="inline" fill="none" stroke="#FFFFFF" stroke-width="5" x1="23.5" y1="10" x2="23.5" y2="22"/>
<line display="inline" fill="none" stroke="#FFFFFF" stroke-width="5" x1="43" y1="13.546" x2="19.938" y2="53.489"/>
<line display="inline" fill="none" stroke="#FFFFFF" stroke-width="5" x1="5" y1="13.545" x2="11.904" y2="25.504"/>
</g>
<g id="Combined"> <g id="Combined">
<path d="M41,1H7C3.687,1,1,3.687,1,7v34c0,3.313,2.687,6,6,6h13.798l20.037-34.704l4.33,2.5L26.571,47H41c3.313,0,6-2.687,6-6V7 <path d="M41,1H7C3.687,1,1,3.687,1,7v34c0,3.313,2.687,6,6,6h13.798l20.037-34.704l4.33,2.5L26.571,47H41c3.313,0,6-2.687,6-6V7
C47,3.687,44.313,1,41,1z M9.739,26.754L2.835,14.795l4.33-2.5l6.904,11.958L9.739,26.754z M26,22h-5V10h5V22z"/> C47,3.687,44.313,1,41,1z M9.739,26.754L2.835,14.795l4.33-2.5l6.904,11.958L9.739,26.754z M26,22h-5V10h5V22z"/>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 450 B

+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" <svg fill="#F0EDE6" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px"
height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve"> height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
<g id="Layer_1"> <g id="Layer_1">
<path d="M31.5,2.262v8.767C35.976,13.628,39,18.462,39,24c0,8.271-6.729,15-15,15c-8.271,0-15-6.729-15-15 <path d="M31.5,2.262v8.767C35.976,13.628,39,18.462,39,24c0,8.271-6.729,15-15,15c-8.271,0-15-6.729-15-15

Before

Width:  |  Height:  |  Size: 857 B

After

Width:  |  Height:  |  Size: 872 B

+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg fill="#F0EDE6" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="48px" height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve"> width="48px" height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
</svg> </svg>

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 492 B

+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" <svg fill="#F0EDE6" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px"
height="48px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve"> height="48px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<g id="Layer_2"> <g id="Layer_2">
<g> <g>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg fill="#F0EDE6" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="48px" height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve"> width="48px" height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
<g id="x"> <g id="x">
</g> </g>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" <svg fill="#F0EDE6" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px"
height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve"> height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
<g id="Combined"> <g id="Combined">
<path d="M35.819,27C34.863,32.666,29.935,37,24,37c-4.43,0-8.298-2.419-10.376-6h5.103c1.41,1.24,3.253,2,5.274,2 <path d="M35.819,27C34.863,32.666,29.935,37,24,37c-4.43,0-8.298-2.419-10.376-6h5.103c1.41,1.24,3.253,2,5.274,2

Before

Width:  |  Height:  |  Size: 900 B

After

Width:  |  Height:  |  Size: 915 B

+2 -4
View File
@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> width="24px" height="24px" viewBox="0 0 24 24" fill="#F0EDE6">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<path d="M18.993,8.993L18.99,5c0-1.1-0.891-2-1.99-2h-4h-3H6C4.9,3,4.01,3.9,4.01,5L4.007,8.993L4,9v10c0,1.1,0.891,2,1.99,2H6h11 <path d="M18.993,8.993L18.99,5c0-1.1-0.891-2-1.99-2h-4h-3H6C4.9,3,4.01,3.9,4.01,5L4.007,8.993L4,9v10c0,1.1,0.891,2,1.99,2H6h11
h0.01c1.1,0,1.99-0.9,1.99-2V9L18.993,8.993z M17,10v9H6v-9V5h5h1h5V10z"/> h0.01c1.1,0,1.99-0.9,1.99-2V9L18.993,8.993z M17,10v9H6v-9V5h5h1h5V10z"/>
<rect x="8" y="7" width="2" height="2"/> <rect x="8" y="7" width="2" height="2"/>

Before

Width:  |  Height:  |  Size: 939 B

After

Width:  |  Height:  |  Size: 647 B

+2 -4
View File
@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> width="24px" height="24px" viewBox="0 0 24 24" fill="#F0EDE6">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<path d="M20.953,7.993L20.949,4c0-1.1-0.891-2-1.99-2h-4h-3h-3c-1.1,0-1.99,0.9-1.99,2h1.99h4h1h5h0.006v16h0.004 <path d="M20.953,7.993L20.949,4c0-1.1-0.891-2-1.99-2h-4h-3h-3c-1.1,0-1.99,0.9-1.99,2h1.99h4h1h5h0.006v16h0.004
c1.1,0,1.99-0.9,1.99-2V8L20.953,7.993z"/> c1.1,0,1.99-0.9,1.99-2V8L20.953,7.993z"/>
<path d="M17.993,10.993L17.99,7c0-1.1-0.891-2-1.99-2h-4H9H6C4.9,5,4.01,5.9,4.01,7l-0.003,3.993L4,11v10c0,1.1,0.891,2,1.99,2H6h10 <path d="M17.993,10.993L17.99,7c0-1.1-0.891-2-1.99-2h-4H9H6C4.9,5,4.01,5.9,4.01,7l-0.003,3.993L4,11v10c0,1.1,0.891,2,1.99,2H6h10

Before

Width:  |  Height:  |  Size: 919 B

After

Width:  |  Height:  |  Size: 627 B

+173 -25
View File
@@ -31,7 +31,7 @@ const API = {
listPresets: () => api("GET", "/api/presets"), listPresets: () => api("GET", "/api/presets"),
getPreset: (bank, program) => api("GET", `/api/presets/${bank}/${program}`), getPreset: (bank, program) => api("GET", `/api/presets/${bank}/${program}`),
activatePreset:(bank, program) => api("POST", `/api/presets/${bank}/${program}/activate`), 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) }), setAudioProfile:(rate, period) => api("POST", "/api/audio/profile", { profile: "stable", rate: parseInt(rate), period: parseInt(period) }),
getAudioProfile:() => api("GET", "/api/audio/profile"), getAudioProfile:() => api("GET", "/api/audio/profile"),
getRouting: () => api("GET", "/api/routing"), getRouting: () => api("GET", "/api/routing"),
@@ -52,13 +52,13 @@ const T = {
border: "#2A2A32", border: "#2A2A32",
amber: "#E8A030", amber: "#E8A030",
amberDim: "#7A5218", amberDim: "#7A5218",
blue: "#3A7BA8", blue: "#4A8BC8",
blueDim: "#1E4060", blueDim: "#1E4060",
green: "#3AB87A", green: "#3AB87A",
red: "#C84040", red: "#E06060",
textPrimary: "#F0EDE6", textPrimary: "#F0EDE6",
textSec: "#8888A0", textSec: "#A0A0BC",
textDim: "#444458", textDim: "#7878A0",
}; };
// ── Block parameter definitions ───────────────────────────────── // ── Block parameter definitions ─────────────────────────────────
@@ -167,11 +167,13 @@ const css = `
.btn-primary:active { filter: brightness(.85); transform: scale(.97); } .btn-primary:active { filter: brightness(.85); transform: scale(.97); }
.btn-ghost { background: ${T.surface}; color: ${T.textPrimary}; border: 1px solid ${T.border}; } .btn-ghost { background: ${T.surface}; color: ${T.textPrimary}; border: 1px solid ${T.border}; }
.btn-ghost:active { background: ${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; justify-content: center; background: ${T.surface}; border: 1px solid ${T.border}; cursor: pointer;
font-size: 15px; transition: all .12s; flex-shrink: 0; } font-size: 15px; transition: all .12s; flex-shrink: 0; }
.btn-icon:active { transform: scale(.93); } .btn-icon:active { transform: scale(.93); }
.btn-icon.active { background: rgba(232,160,48,.15); border-color: ${T.amber}; color: ${T.amber}; } .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 ── */
.badge { display: inline-flex; align-items: center; justify-content: center; .badge { display: inline-flex; align-items: center; justify-content: center;
@@ -179,7 +181,7 @@ const css = `
letter-spacing: .06em; text-transform: uppercase; } letter-spacing: .06em; text-transform: uppercase; }
.badge-amber { background: rgba(232,160,48,.2); color: ${T.amber}; } .badge-amber { background: rgba(232,160,48,.2); color: ${T.amber}; }
.badge-green { background: rgba(58,184,122,.2); color: ${T.green}; } .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 slider ── */
.param-panel { background: ${T.surface}; padding: 8px 12px 6px; border-top: 1px solid ${T.border}; flex-shrink: 0; } .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 ────────────────────────────────────────────────── // ── VU Meter ──────────────────────────────────────────────────
function VUMeter({level=0,height=24,vertical=false}){const segs=12,active=Math.round((level/100)*segs); 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; const c=i=>i>=10?T.red:i>=8?"#E8C030":T.green;
if(vertical)return(<div style={{display:"flex",flexDirection:"column-reverse",gap:1,height,alignItems:"center"}}> if(vertical)return(<div style={{display:"flex",flexDirection:"column-reverse",gap:1.5,height,alignItems:"center"}}>
{Array.from({length:segs}).map((_,i)=>(<div key={i} style={{width:4,height:(height-segs)/segs,borderRadius:1,background:i<active?c(i):T.border,boxShadow:i<active?`0 0 4px ${c(i)}`:"none",transition:"background .05s"}}/>))}</div>); {Array.from({length:segs}).map((_,i)=>(<div key={i} style={{width:4,height:segH,borderRadius:1.5,background:i<active?c(i):T.border,boxShadow:i<active?`0 0 4px ${c(i)}`:"none",transition:"all .2s ease, opacity .15s"}}/>))}</div>);
return(<div style={{display:"flex",gap:2,height:6,alignItems:"center",width:height}}> return(<div style={{display:"flex",gap:2,height:6,alignItems:"center",width:height}}>
{Array.from({length:segs}).map((_,i)=>(<div key={i} style={{flex:1,height:"100%",borderRadius:1,background:i<active?c(i):T.border,transition:"background .05s"}}/>))}</div>); {Array.from({length:segs}).map((_,i)=>(<div key={i} style={{flex:1,height:"100%",borderRadius:1,background:i<active?c(i):T.border,transition:"all .2s ease"}}/>))}</div>);
} }
// Captures Screen // 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 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);}}; 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(<div style={{display:"flex",flexDirection:"column",height:"100%",background:T.bg}}> return(<div style={{display:"flex",flexDirection:"column",height:"100%",background:T.bg}}>
<div className="screen-header"><div className="screen-title">Downloads</div><button className="btn-icon" style={{width:28,height:28,fontSize:13}}onClick={onClose}></button></div> <div className="screen-header"><div className="screen-title">Downloads</div><button className="btn-icon" style={{width:36,height:36,fontSize:13}}onClick={onClose}></button></div>
<div style={{padding:"6px 12px",borderBottom:`1px solid ${T.border}`}}><div className="toggle"> <div style={{padding:"6px 12px",borderBottom:`1px solid ${T.border}`}}><div className="toggle">
<button className={`toggle-btn ${tab==="models"?"active":""}`}onClick={()=>setTab("models")}>NAM Models</button> <button className={`toggle-btn ${tab==="models"?"active":""}`}onClick={()=>setTab("models")}>NAM Models</button>
<button className={`toggle-btn ${tab==="irs"?"active":""}`}onClick={()=>setTab("irs")}>IR Cabs</button></div></div> <button className={`toggle-btn ${tab==="irs"?"active":""}`}onClick={()=>setTab("irs")}>IR Cabs</button></div></div>
@@ -462,8 +465,9 @@ export default function App(){
const[globalBypass,setGlobalBypass]=useState(false); const[globalBypass,setGlobalBypass]=useState(false);
const[loadingPreset,setLoadingPreset]=useState(false); const[loadingPreset,setLoadingPreset]=useState(false);
const[focusBlockId,setFocusBlockId]=useState(null); const[focusBlockId,setFocusBlockId]=useState(null);
const[modelBrowserBlockId,setModelBrowserBlockId]=useState(null); const [modelBrowserBlockId,setModelBrowserBlockId]=useState(null);
const[footswitchMode,setFootswitchMode]=useState("stomp"); const [footswitchMode,setFootswitchMode]=useState("stomp");
const [addBlockType,setAddBlockType]=useState(null); // null | "picker"
// ── Undo/Redo ── // ── Undo/Redo ──
const [undoStack, setUndoStack] = useState([]); const [undoStack, setUndoStack] = useState([]);
@@ -546,6 +550,7 @@ export default function App(){
// ── Main Menu & Settings ── // ── Main Menu & Settings ──
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const [settingsView, setSettingsView] = useState(null); // "settings" | "eq" | null const [settingsView, setSettingsView] = useState(null); // "settings" | "eq" | null
const [overlayView, setOverlayView] = useState(null); // "save" | "bypass" | "wifi" | null
const handleMenuSelect = useCallback((itemId) => { const handleMenuSelect = useCallback((itemId) => {
setMenuOpen(false); setMenuOpen(false);
@@ -556,8 +561,14 @@ export default function App(){
setState(prev => ({ ...prev, tuner_enabled: true })); setState(prev => ({ ...prev, tuner_enabled: true }));
API.tunerToggle(true).catch(() => {}); API.tunerToggle(true).catch(() => {});
break; break;
default: case "save":
// save, bypass, wifi — placeholder for now setOverlayView("save");
break;
case "bypass":
setOverlayView("bypass");
break;
case "wifi":
setOverlayView("wifi");
break; break;
} }
}, []); }, []);
@@ -584,7 +595,7 @@ export default function App(){
useEffect(()=>{loadState();const id=setInterval(loadState,3000);return()=>clearInterval(id);},[loadState]); useEffect(()=>{loadState();const id=setInterval(loadState,3000);return()=>clearInterval(id);},[loadState]);
// VU animation fallback when no real data // 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 ── // ── Load presets from API ──
useEffect(()=>{ 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;}); setBlocks(prev=>{const arr=[...prev];const[m]=arr.splice(fromIdx,1);arr.splice(toIdx,0,m);return arr;});
},[takeSnapshot]); },[takeSnapshot]);
const handleAddBlock=useCallback(()=>{ const handleAddBlock=useCallback(()=>{
setAddBlockType("picker");
},[takeSnapshot]);
const handleAddBlockType=useCallback((type)=>{
takeSnapshot(); takeSnapshot();
userChangedBlock.current=true;const id="block_"+Date.now(); userChangedBlock.current=true;
setBlocks(prev=>[...prev,{id,type:"overdrive",name:"New Drive",bypassed:false,params:{drive:30,tone:50,level:70}}]); 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); setTimeout(()=>{userChangedBlock.current=false;},3000);
},[takeSnapshot]); },[takeSnapshot]);
const handleRemoveBlock=useCallback((id)=>{ const handleRemoveBlock=useCallback((id)=>{
@@ -786,7 +806,7 @@ export default function App(){
<div style={{display:"flex",gap:8,alignItems:"center"}}> <div style={{display:"flex",gap:8,alignItems:"center"}}>
<div style={{width:6,height:6,borderRadius:"50%", <div style={{width:6,height:6,borderRadius:"50%",
background:state.connected?T.green:T.red,boxShadow:`0 0 6px ${state.connected?T.green:T.red}`}}/> background:state.connected?T.green:T.red,boxShadow:`0 0 6px ${state.connected?T.green:T.red}`}}/>
<span className="mono" style={{fontSize:9,color:T.textSec}}>{state.connected?"CONNECTED":"OFFLINE"}</span> <span className="mono" style={{fontSize:9,color:state.connected?T.green:T.red}}>{state.connected?"CONNECTED":"OFFLINE"}</span>
{/* Undo/Redo */} {/* Undo/Redo */}
<button className="btn-icon" style={{width:20,height:20,fontSize:11,opacity:undoStack.length>0?1:0.3}} <button className="btn-icon" style={{width:20,height:20,fontSize:11,opacity:undoStack.length>0?1:0.3}}
onClick={handleUndo} disabled={undoStack.length===0} title="Undo (Ctrl+Z)"> onClick={handleUndo} disabled={undoStack.length===0} title="Undo (Ctrl+Z)">
@@ -805,14 +825,14 @@ export default function App(){
<div style={{display:"flex",gap:8,alignItems:"center"}}> <div style={{display:"flex",gap:8,alignItems:"center"}}>
<div style={{display:"flex",gap:8,alignItems:"center"}}> <div style={{display:"flex",gap:8,alignItems:"center"}}>
<VUMeter level={vuLevel} height={24}/> <VUMeter level={vuLevel} height={24}/>
<span className="mono" style={{fontSize:9,color:T.textDim}}>CPU {state.cpu_percent!=null?`${state.cpu_percent}%`:"—"}</span> <span className="mono" style={{fontSize:9,color:T.textSec}}>CPU <span style={{color:T.amber,fontWeight:700}}>{state.cpu_percent!=null?`${state.cpu_percent}%`:"—"}</span></span>
</div> </div>
<div style={{width:1,height:12,background:T.border}}/> <div style={{width:1,height:12,background:T.border}}/>
<button className="btn-icon" style={{width:24,height:24,position:'relative'}}onClick={()=>setView("captures")}title="Downloads"> <button className="btn-icon" style={{width:24,height:24,position:'relative'}}onClick={()=>setView("captures")}title="Downloads">
<img src={`${import.meta.env.BASE_URL}img/ic_bank.svg`} alt="captures" style={{width:14,height:14,filter:'invert(1) brightness(2)'}}/> <img src={`${import.meta.env.BASE_URL}img/ic_bank.svg`} alt="captures" style={{width:14,height:14}}/>
</button> </button>
<button className="btn-icon" style={{width:24,height:24,position:'relative'}}onClick={()=>setView("presets")}title="Presets"> <button className="btn-icon" style={{width:24,height:24,position:'relative'}}onClick={()=>setView("presets")}title="Presets">
<img src={`${import.meta.env.BASE_URL}img/ic_presets.svg`} alt="presets" style={{width:14,height:14,filter:'invert(1) brightness(2)'}}/> <img src={`${import.meta.env.BASE_URL}img/ic_presets.svg`} alt="presets" style={{width:14,height:14}}/>
</button> </button>
<div style={{display:"flex",gap:2,alignItems:"center",background:T.surface,borderRadius:4,border:`1px solid ${T.border}`,padding:2,height:24}}> <div style={{display:"flex",gap:2,alignItems:"center",background:T.surface,borderRadius:4,border:`1px solid ${T.border}`,padding:2,height:24}}>
{["stomp","preset"].map(m=>(<button key={m} onClick={()=>setFootswitchMode(m)} style={{ {["stomp","preset"].map(m=>(<button key={m} onClick={()=>setFootswitchMode(m)} style={{
@@ -827,7 +847,7 @@ export default function App(){
background:state.tuner_enabled?'rgba(128,208,160,.15)':T.surface, background:state.tuner_enabled?'rgba(128,208,160,.15)':T.surface,
borderColor:state.tuner_enabled?T.green:T.border}} borderColor:state.tuner_enabled?T.green:T.border}}
onClick={handleTunerToggle} title={state.tuner_enabled?"Exit Tuner":"Tuner"}> onClick={handleTunerToggle} title={state.tuner_enabled?"Exit Tuner":"Tuner"}>
<img src={`${import.meta.env.BASE_URL}img/fx_analyzer.svg`} alt="tuner" style={{width:14,height:14,filter:state.tuner_enabled?'invert(0.9) brightness(2) sepia(0.4) hue-rotate(80deg)':'invert(1) brightness(2)'}}/> <img src={`${import.meta.env.BASE_URL}img/fx_analyzer.svg`} alt="tuner" style={{width:14,height:14,filter:state.tuner_enabled?'brightness(0) saturate(100%) invert(0.9) sepia(0.4) hue-rotate(80deg)':'none'}}/>
</button> </button>
<button className="btn-icon" style={{width:24,height:24,position:'relative',background:'rgba(232,160,48,.12)',borderColor:T.amber}}onClick={()=>{if(!document.fullscreenElement){document.documentElement.requestFullscreen()}else{document.exitFullscreen()}}}title="Fullscreen"> <button className="btn-icon" style={{width:24,height:24,position:'relative',background:'rgba(232,160,48,.12)',borderColor:T.amber}}onClick={()=>{if(!document.fullscreenElement){document.documentElement.requestFullscreen()}else{document.exitFullscreen()}}}title="Fullscreen">
<svg viewBox="0 0 24 24" width="14" height="14" fill="#E8A030"> <svg viewBox="0 0 24 24" width="14" height="14" fill="#E8A030">
@@ -837,7 +857,7 @@ export default function App(){
<div style={{width:1,height:16,background:T.border}}/> <div style={{width:1,height:16,background:T.border}}/>
<button className="btn-icon" style={{width:24,height:24,position:'relative'}} <button className="btn-icon" style={{width:24,height:24,position:'relative'}}
onClick={()=>setMenuOpen(true)} title="Main Menu"> onClick={()=>setMenuOpen(true)} title="Main Menu">
<span style={{fontSize:14,color:T.textSec,lineHeight:1}}></span> <span style={{fontSize:14,color:T.amber,lineHeight:1}}></span>
</button> </button>
</div> </div>
</div> </div>
@@ -896,6 +916,7 @@ export default function App(){
onAddBlock={handleAddBlock} onAddBlock={handleAddBlock}
onRemoveBlock={handleRemoveBlock} onRemoveBlock={handleRemoveBlock}
onModelBrowser={handleOpenModelBrowser} onModelBrowser={handleOpenModelBrowser}
onBlockSelected={handleAddBlockType}
/> />
</div> </div>
@@ -958,7 +979,7 @@ export default function App(){
{view==="captures"&&<CapturesScreen onClose={()=>setView("main")}/>} {view==="captures"&&<CapturesScreen onClose={()=>setView("main")}/>}
{view==="presets"&&( {view==="presets"&&(
<div style={{display:"flex",flexDirection:"column",height:"100%",background:T.bg}}> <div style={{display:"flex",flexDirection:"column",height:"100%",background:T.bg}}>
<div className="screen-header"><div className="screen-title">Presets</div><button className="btn-icon"style={{width:28,height:28,fontSize:13}}onClick={()=>setView("main")}></button></div> <div className="screen-header"><div className="screen-title">Presets</div><button className="btn-icon"style={{width:36,height:36,fontSize:13}}onClick={()=>setView("main")}></button></div>
<div className="screen-body"> <div className="screen-body">
{presets.map((p,i)=>( {presets.map((p,i)=>(
<div key={i} className={`preset-row ${p.num===currentPreset?"active":""}`} <div key={i} className={`preset-row ${p.num===currentPreset?"active":""}`}
@@ -1027,6 +1048,133 @@ export default function App(){
/> />
)} )}
{/* ── Save Preset overlay ── */}
{overlayView==="save"&&(
<div style={{position:"fixed",inset:0,zIndex:1000,background:"rgba(0,0,0,.7)",display:"flex",alignItems:"center",justifyContent:"center"}}>
<div className="card" style={{width:320,maxWidth:"85vw",padding:20}}>
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16}}>
<div style={{fontSize:14,fontWeight:600,color:T.textPrimary}}>💾 Save Preset</div>
<button className="btn-icon" style={{width:26,height:26,fontSize:12}}onClick={()=>setOverlayView(null)}></button>
</div>
<div style={{display:"flex",flexDirection:"column",gap:12}}>
<div>
<div style={{fontSize:9,color:T.textSec,marginBottom:4,textTransform:"uppercase",letterSpacing:".06em"}}>Preset Name</div>
<input defaultValue={presets.find(p=>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"}}/>
</div>
<div>
<div style={{fontSize:9,color:T.textSec,marginBottom:4,textTransform:"uppercase",letterSpacing:".06em"}}>Target</div>
<div style={{display:"flex",gap:4,flexWrap:"wrap"}}>
{[1,2,3,4].map(slot=>(
<button key={slot} className="btn-ghost" style={{flex:1,padding:"6px",fontSize:10,textAlign:"center"}}
onClick={()=>{setOverlayView(null);API.savePreset(currentBank-1,slot-1,"Saved Preset",[]).catch(()=>{});}}>
Slot {slot}<br/>
<span style={{fontSize:8,color:T.textDim}}>{presets.find(p=>p.num===slot)?.name||"Empty"}</span>
</button>
))}
</div>
</div>
</div>
</div>
</div>
)}
{/* ── Bypass/Control overlay ── */}
{overlayView==="bypass"&&(
<div style={{position:"fixed",inset:0,zIndex:1000,background:"rgba(0,0,0,.7)",display:"flex",alignItems:"center",justifyContent:"center"}}>
<div className="card" style={{width:340,maxWidth:"85vw",padding:20}}>
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16}}>
<div style={{fontSize:14,fontWeight:600,color:T.textPrimary}}>🔌 Bypass / Control</div>
<button className="btn-icon" style={{width:26,height:26,fontSize:12}}onClick={()=>setOverlayView(null)}></button>
</div>
<div style={{display:"flex",flexDirection:"column",gap:14}}>
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center"}}>
<span style={{fontSize:12,color:T.textPrimary}}>🚫 Global Bypass</span>
<button className="btn-icon" style={{width:40,height:26,fontSize:10,background:globalBypass?`${T.red}22`:T.surface,borderColor:globalBypass?T.red:T.border}}
onClick={()=>{setGlobalBypass(p=>!p);API.bypassToggle().catch(()=>{});}}>
{globalBypass?"ON":"OFF"}
</button>
</div>
<div style={{height:1,background:T.border}}/>
<div>
<div style={{fontSize:9,color:T.textSec,marginBottom:6,textTransform:"uppercase",letterSpacing:".06em"}}>Block Bypass States</div>
{blocks.map(b=>(
<div key={b.id} style={{display:"flex",justifyContent:"space-between",alignItems:"center",padding:"4px 0"}}>
<span style={{fontSize:11,color:T.textPrimary}}>{b.name||b.type}</span>
<button className="btn-icon" style={{width:36,height:22,fontSize:9,background:b.bypassed?`${T.red}22`:T.surface,borderColor:b.bypassed?T.red:T.border}}
onClick={()=>handleToggleBlock(b.id)}>
{b.bypassed?"BYP":"ACT"}
</button>
</div>
))}
</div>
<div style={{height:1,background:T.border}}/>
<div>
<div style={{fontSize:9,color:T.textSec,marginBottom:6,textTransform:"uppercase",letterSpacing:".06em"}}>Footswitch Mode</div>
<div style={{display:"flex",gap:4,background:"#0A0A10",borderRadius:6,padding:4}}>
{["stomp","preset"].map(m=>(
<button key={m} onClick={()=>setFootswitchMode(m)} style={{
flex:1,padding:"5px 8px",borderRadius:4,border:"none",fontSize:10,fontWeight:600,
cursor:"pointer",background:footswitchMode===m?T.amber:"transparent",
color:footswitchMode===m?"#000":T.textSec,
}}>{m==="stomp"?"Stompbox":"Preset"}</button>
))}
</div>
</div>
</div>
</div>
</div>
)}
{/* ── Wi-Fi / Bluetooth overlay ── */}
{overlayView==="wifi"&&(
<div style={{position:"fixed",inset:0,zIndex:1000,background:"rgba(0,0,0,.7)",display:"flex",alignItems:"center",justifyContent:"center"}}>
<div className="card" style={{width:340,maxWidth:"85vw",padding:20}}>
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16}}>
<div style={{fontSize:14,fontWeight:600,color:T.textPrimary}}>📶 Wireless</div>
<button className="btn-icon" style={{width:26,height:26,fontSize:12}}onClick={()=>setOverlayView(null)}></button>
</div>
<div style={{display:"flex",flexDirection:"column",gap:14}}>
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center"}}>
<span style={{fontSize:12,color:T.textPrimary}}>📶 WiFi</span>
<button className="btn-icon" style={{width:40,height:26,fontSize:10}}>Scan</button>
</div>
<div className="card-sm" style={{textAlign:"center",padding:16}}>
<span style={{fontSize:11,color:T.textSec}}>Use pedal as access point or connect to existing network via the settings panel.</span>
</div>
<div style={{height:1,background:T.border}}/>
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center"}}>
<span style={{fontSize:12,color:T.textPrimary}}>🔵 Bluetooth</span>
<button className="btn-icon" style={{width:40,height:26,fontSize:10,background:`${T.blue}22`,borderColor:T.blue,color:T.blue}}>ON</button>
</div>
<div style={{fontSize:10,color:T.textSec}}>Bluetooth MIDI is enabled. Pair with your mobile device or MIDI controller.</div>
</div>
</div>
</div>
)}
{/* ── Block Type Picker ── */}
{addBlockType==="picker"&&(
<div style={{position:"fixed",inset:0,zIndex:1000,background:"rgba(0,0,0,.7)",display:"flex",alignItems:"center",justifyContent:"center"}}
onClick={(e)=>e.target===e.currentTarget&&setAddBlockType(null)}>
<div className="card" style={{width:320,maxWidth:"85vw",padding:20,maxHeight:"80vh",overflowY:"auto"}}>
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:12}}>
<div style={{fontSize:13,fontWeight:600,color:T.textPrimary}}>+ Add Block</div>
<button className="btn-icon" style={{width:24,height:24,fontSize:11}}onClick={()=>setAddBlockType(null)}></button>
</div>
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:6}}>
{Object.entries(BLOCK_DISPLAY_NAMES).filter(([k])=>!["loop","split","merge"].includes(k)).map(([type,name])=>(
<button key={type} className="btn-ghost" style={{padding:"8px 6px",fontSize:10,textAlign:"center",lineHeight:1.3}}
onClick={()=>handleAddBlockType(type)}>
<div style={{fontWeight:600}}>{name}</div>
<div style={{fontSize:8,color:T.textDim,marginTop:1,textTransform:"capitalize"}}>{type}</div>
</button>
))}
</div>
</div>
</div>
)}
</div></> </div></>
); );
} }
+7 -7
View File
@@ -8,13 +8,13 @@ const T = {
border: "#2A2A32", border: "#2A2A32",
amber: "#E8A030", amber: "#E8A030",
amberDim: "#7A5218", amberDim: "#7A5218",
blue: "#3A7BA8", blue: "#4A8BC8",
blueDim: "#1E4060", blueDim: "#1E4060",
green: "#3AB87A", green: "#3AB87A",
red: "#C84040", red: "#E06060",
textPrimary: "#F0EDE6", textPrimary: "#F0EDE6",
textSec: "#8888A0", textSec: "#A0A0BC",
textDim: "#444458", textDim: "#7878A0",
}; };
// ── Split type metadata ──────────────────────────── // ── Split type metadata ────────────────────────────
@@ -95,7 +95,7 @@ function BlockIcon({ type, size = 14, style = {} }) {
alt={type || 'fx'} alt={type || 'fx'}
style={{ style={{
width: size, height: size, flexShrink: 0, width: size, height: size, flexShrink: 0,
filter: 'invert(0.95) brightness(2)', filter: 'none',
objectFit: 'contain', objectFit: 'contain',
...style, ...style,
}} }}
@@ -865,8 +865,8 @@ export default function BlockChain({
</button> </button>
{selectedBlockId && ( {selectedBlockId && (
<button onClick={() => onRemoveBlock(selectedBlockId)} style={{ <button onClick={() => onRemoveBlock(selectedBlockId)} style={{
padding: "10px 14px", borderRadius: 7, background: "rgba(200,64,64,.15)", padding: "10px 14px", borderRadius: 7, background: "rgba(224,96,96,.15)",
border: `1px solid rgba(200,64,64,.3)`, color: T.red, fontSize: 12, border: `1px solid rgba(224,96,96,.3)`, color: T.red, fontSize: 12,
fontWeight: 600, cursor: "pointer", transition: "all .12s", fontWeight: 600, cursor: "pointer", transition: "all .12s",
}}> }}>
Remove Remove
+4 -4
View File
@@ -8,13 +8,13 @@ const T = {
border: "#2A2A32", border: "#2A2A32",
amber: "#E8A030", amber: "#E8A030",
amberDim: "#7A5218", amberDim: "#7A5218",
blue: "#3A7BA8", blue: "#4A8BC8",
blueDim: "#1E4060", blueDim: "#1E4060",
green: "#3AB87A", green: "#3AB87A",
red: "#C84040", red: "#E06060",
textPrimary: "#F0EDE6", textPrimary: "#F0EDE6",
textSec: "#8888A0", textSec: "#A0A0BC",
textDim: "#444458", textDim: "#7878A0",
}; };
// Block param definitions (mirrored from App.jsx) // Block param definitions (mirrored from App.jsx)
+2 -2
View File
@@ -2,8 +2,8 @@ import { useRef, useEffect } from "react";
const T = { const T = {
bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32", bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32",
amber: "#E8A030", amberDim: "#7A5218", blue: "#3A7BA8", blueDim: "#1E4060", amber: "#E8A030", amberDim: "#7A5218", blue: "#4A8BC8", blueDim: "#1E4060",
green: "#3AB87A", red: "#C84040", textPrimary: "#F0EDE6", textSec: "#8888A0", textDim: "#444458", green: "#3AB87A", red: "#E06060", textPrimary: "#F0EDE6", textSec: "#A0A0BC", textDim: "#7878A0",
}; };
const TYPE_COLORS = { const TYPE_COLORS = {
+2 -2
View File
@@ -2,8 +2,8 @@ import { useState, useRef, useCallback, useEffect } from "react";
const T = { const T = {
bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32", bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32",
amber: "#E8A030", amberDim: "#7A5218", blue: "#3A7BA8", blueDim: "#1E4060", amber: "#E8A030", amberDim: "#7A5218", blue: "#4A8BC8", blueDim: "#1E4060",
green: "#3AB87A", red: "#C84040", textPrimary: "#F0EDE6", textSec: "#8888A0", textDim: "#444458", green: "#3AB87A", red: "#E06060", textPrimary: "#F0EDE6", textSec: "#A0A0BC", textDim: "#7878A0",
}; };
// EQ Band definitions // EQ Band definitions
+2 -2
View File
@@ -2,8 +2,8 @@ import { useState, useCallback, useRef, useEffect, useMemo } from "react";
const T = { const T = {
bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32", bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32",
amber: "#E8A030", amberDim: "#7A5218", blue: "#3A7BA8", blueDim: "#1E4060", amber: "#E8A030", amberDim: "#7A5218", blue: "#4A8BC8", blueDim: "#1E4060",
green: "#3AB87A", red: "#C84040", textPrimary: "#F0EDE6", textSec: "#8888A0", textDim: "#444458", green: "#3AB87A", red: "#E06060", textPrimary: "#F0EDE6", textSec: "#A0A0BC", textDim: "#7878A0",
}; };
async function api(method, path, body) { async function api(method, path, body) {
+2 -2
View File
@@ -2,8 +2,8 @@ import { useRef, useEffect } from "react";
const T = { const T = {
bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32", bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32",
amber: "#E8A030", amberDim: "#7A5218", blue: "#3A7BA8", blueDim: "#1E4060", amber: "#E8A030", amberDim: "#7A5218", blue: "#4A8BC8", blueDim: "#1E4060",
green: "#3AB87A", red: "#C84040", textPrimary: "#F0EDE6", textSec: "#8888A0", textDim: "#444458", green: "#3AB87A", red: "#E06060", textPrimary: "#F0EDE6", textSec: "#A0A0BC", textDim: "#7878A0",
}; };
const MENU_ITEMS = [ const MENU_ITEMS = [
+4 -4
View File
@@ -8,13 +8,13 @@ const T = {
border: "#2A2A32", border: "#2A2A32",
amber: "#E8A030", amber: "#E8A030",
amberDim: "#7A5218", amberDim: "#7A5218",
blue: "#3A7BA8", blue: "#4A8BC8",
blueDim: "#1E4060", blueDim: "#1E4060",
green: "#3AB87A", green: "#3AB87A",
red: "#C84040", red: "#E06060",
textPrimary: "#F0EDE6", textPrimary: "#F0EDE6",
textSec: "#8888A0", textSec: "#A0A0BC",
textDim: "#444458", textDim: "#7878A0",
}; };
const IMG_PATH = (import.meta.env.BASE_URL || '/ui/') + 'img/'; const IMG_PATH = (import.meta.env.BASE_URL || '/ui/') + 'img/';
+3 -3
View File
@@ -3,9 +3,9 @@ import { useState, useEffect, useRef, useCallback } from "react";
// Design tokens (mirrored) // Design tokens (mirrored)
const T = { const T = {
bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32", bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32",
amber: "#E8A030", amberDim: "#7A5218", blue: "#3A7BA8", amber: "#E8A030", amberDim: "#7A5218", blue: "#4A8BC8",
green: "#3AB87A", red: "#C84040", textPrimary: "#F0EDE6", green: "#3AB87A", red: "#E06060", textPrimary: "#F0EDE6",
textSec: "#8888A0", textDim: "#444458", textSec: "#A0A0BC", textDim: "#7878A0",
}; };
// String labels matching _STRING_NAMES // String labels matching _STRING_NAMES
+102 -7
View File
@@ -1,7 +1,7 @@
/* ── Reset & base ── */ /* ── Reset & base ── */
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
html, body, #root { height: 100%; overflow: hidden; } html, body, #root { height: 100%; overflow: hidden; }
body { background: #0A0A0C; color: #F0EDE6; font-family: 'Inter', sans-serif; -webkit-font-smoothing: antialiased; } body { background: #0A0A0C; color: #F0EDE6; font-family: 'Space Grotesk', sans-serif; -webkit-font-smoothing: antialiased; }
/* ── Utility classes (from pipedal) ── */ /* ── Utility classes (from pipedal) ── */
.mono { font-family: 'JetBrains Mono', monospace; } .mono { font-family: 'JetBrains Mono', monospace; }
@@ -12,8 +12,8 @@ body { background: #0A0A0C; color: #F0EDE6; font-family: 'Inter', sans-serif; -w
letter-spacing: .06em; text-transform: uppercase; } letter-spacing: .06em; text-transform: uppercase; }
.badge-amber { background: rgba(232,160,48,.2); color: #E8A030; } .badge-amber { background: rgba(232,160,48,.2); color: #E8A030; }
.badge-green { background: rgba(58,184,122,.2); color: #3AB87A; } .badge-green { background: rgba(58,184,122,.2); color: #3AB87A; }
.badge-blue { background: rgba(58,123,168,.2); color: #3A7BA8; } .badge-blue { background: rgba(74,139,200,.2); color: #4A8BC8; }
.badge-red { background: rgba(200,64,64,.2); color: #C84040; } .badge-red { background: rgba(224,96,96,.2); color: #E06060; }
/* Button system */ /* Button system */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; .btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px;
@@ -23,7 +23,7 @@ body { background: #0A0A0C; color: #F0EDE6; font-family: 'Inter', sans-serif; -w
.btn:active { transform: scale(.96); } .btn:active { transform: scale(.96); }
.btn-sm { padding: 5px 10px; font-size: 10px; } .btn-sm { padding: 5px 10px; font-size: 10px; }
.btn-ghost { background: transparent; border-color: transparent; } .btn-ghost { background: transparent; border-color: transparent; }
.btn-danger { border-color: #C84040; background: rgba(200,64,64,.15); color: #C84040; } .btn-danger { border-color: #E06060; background: rgba(224,96,96,.15); color: #E06060; }
.btn-icon { display: inline-flex; align-items: center; justify-content: center; .btn-icon { display: inline-flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: 5px; border: 1px solid #2A2A32; width: 28px; height: 28px; border-radius: 5px; border: 1px solid #2A2A32;
background: #1C1C22; cursor: pointer; transition: all .12s; touch-action: manipulation; } background: #1C1C22; cursor: pointer; transition: all .12s; touch-action: manipulation; }
@@ -72,7 +72,7 @@ body { background: #0A0A0C; color: #F0EDE6; font-family: 'Inter', sans-serif; -w
align-items: center; justify-content: center; font-size: 16px; cursor: pointer; align-items: center; justify-content: center; font-size: 16px; cursor: pointer;
border: 2px solid #2A2A32; background: #1C1C22; transition: all .12s; } border: 2px solid #2A2A32; background: #1C1C22; transition: all .12s; }
.transport-btn:active { transform: scale(.92); } .transport-btn:active { transform: scale(.92); }
.transport-btn.rec { border-color: #C84040; background: rgba(200,64,64,.15); } .transport-btn.rec { border-color: #E06060; background: rgba(224,96,96,.15); }
.transport-btn.play { border-color: #3AB87A; background: rgba(58,184,122,.15); } .transport-btn.play { border-color: #3AB87A; background: rgba(58,184,122,.15); }
/* Switch toggle */ /* Switch toggle */
@@ -112,10 +112,105 @@ body { background: #0A0A0C; color: #F0EDE6; font-family: 'Inter', sans-serif; -w
/* NAM chip */ /* NAM chip */
.nam-chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; .nam-chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px;
background: rgba(58,123,168,.15); border: 1px solid rgba(58,123,168,.35); background: rgba(74,139,200,.15); border: 1px solid rgba(74,139,200,.35);
border-radius: 5px; font-family: 'JetBrains Mono', monospace; font-size: 11px; color: #3A7BA8; } border-radius: 5px; font-family: 'JetBrains Mono', monospace; font-size: 11px; color: #4A8BC8; }
/* Bypass LED */ /* Bypass LED */
.bypass-led { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } .bypass-led { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.led-on { background: #3AB87A; box-shadow: 0 0 8px #3AB87A; } .led-on { background: #3AB87A; box-shadow: 0 0 8px #3AB87A; }
.led-off { background: #444458; } .led-off { background: #444458; }
/* ── Reduced motion ── */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0s !important;
animation-iteration-count: 1 !important;
transition-duration: 0s !important;
}
}
/* ── Responsive breakpoints ── */
/* Portrait phones */
@media (max-width: 640px) {
.slide-panel-inner {
max-width: 95vw;
max-height: 90vh;
border-radius: 12px 12px 0 0;
}
.screen-header {
padding: 8px 10px 6px;
}
.screen-body {
padding: 8px 10px;
gap: 8px;
}
.screen-title {
font-size: 11px;
}
.preset-row {
padding: 8px 10px;
gap: 8px;
}
.preset-chip-s {
padding: 5px 4px;
}
.preset-chip-s .pcs-num {
font-size: 12px;
}
.preset-chip-s .pcs-name {
font-size: 8px;
}
.setting-row {
padding: 8px 0;
}
.btn {
padding: 6px 12px;
font-size: 11px;
}
.btn-icon {
width: 24px;
height: 24px;
font-size: 10px;
}
.card {
padding: 10px;
}
.card-sm {
padding: 8px;
}
.snap-slot {
min-width: 50px;
padding: 6px 3px;
}
.snap-num {
font-size: 14px;
}
.mono {
font-size: 8px;
}
}
/* Landscape phone / tablet portrait */
@media (min-width: 641px) and (max-width: 1024px) {
.slide-panel-inner {
max-width: 540px;
}
.screen-header {
padding: 10px 12px 7px;
}
}
/* Landscape short screens */
@media (orientation: landscape) and (max-height: 500px) {
.slide-panel-inner {
max-height: 92vh;
}
.screen-header {
padding: 6px 12px 5px;
}
.screen-body {
padding: 6px 10px;
gap: 6px;
}
}