Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87ee14e4e5 | |||
| 3dcf31f600 | |||
| a5a01fda02 | |||
| 0af8ed05b0 |
+66
-83
@@ -7,6 +7,7 @@ import ModelBrowser from "./ModelBrowser.jsx";
|
||||
import MainMenu from "./MainMenu.jsx";
|
||||
import GlobalSettings from "./GlobalSettings.jsx";
|
||||
import GlobalEQ from "./GlobalEQ.jsx";
|
||||
import { T } from "./theme.js";
|
||||
|
||||
// ── API Layer ──────────────────────────────────────────────────────
|
||||
const API_BASE = "";
|
||||
@@ -44,23 +45,6 @@ const API = {
|
||||
tunerToggle: (enabled) => api("POST", "/api/tuner", { enabled }),
|
||||
};
|
||||
|
||||
// ── Design tokens ──────────────────────────────────────────────
|
||||
const T = {
|
||||
bg: "#0A0A0C",
|
||||
panel: "#141418",
|
||||
surface: "#1C1C22",
|
||||
border: "#2A2A32",
|
||||
amber: "#E8A030",
|
||||
amberDim: "#7A5218",
|
||||
blue: "#4A8BC8",
|
||||
blueDim: "#1E4060",
|
||||
green: "#3AB87A",
|
||||
red: "#E06060",
|
||||
textPrimary: "#F0EDE6",
|
||||
textSec: "#A0A0BC",
|
||||
textDim: "#7878A0",
|
||||
};
|
||||
|
||||
// ── Block parameter definitions ─────────────────────────────────
|
||||
const SPLIT_TYPES = [
|
||||
{ value:'y', label:'Y — Split evenly', pan:true },
|
||||
@@ -97,7 +81,7 @@ const BLOCK_PARAMS = {
|
||||
|
||||
const BLOCK_COLORS = {
|
||||
overdrive: T.amber, distortion: T.red, fuzz: '#C07030',
|
||||
delay: T.blue, echo: T.blue, reverb: '#60B0D0',
|
||||
delay: T.blue, echo: T.blue, reverb: '#5090A0',
|
||||
chorus: T.green, flanger: '#50C080', phaser: '#80C050',
|
||||
tremolo: T.green, compressor: '#D09040', gate: T.textSec, eq: '#7080D0',
|
||||
cabinet: '#D07090', ir: '#D07090', nam: '#E0A040', nam_amp: '#E0A040', boost: T.amber,
|
||||
@@ -141,13 +125,12 @@ function getBlockDisplayName(block) {
|
||||
const css = `
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Space+Grotesk:wght@300;400;500;600&display=swap');
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; user-select: none; }
|
||||
html, body { height: 100%; overflow: hidden; background: ${T.bg}; }
|
||||
body { color: ${T.textPrimary}; font-family: 'Space Grotesk', sans-serif; }
|
||||
:root { --amber: ${T.amber}; --blue: ${T.blue}; --green: ${T.green}; }
|
||||
html, body { height: 100%; overflow: hidden; background: var(--bg); }
|
||||
body { color: var(--text-primary); font-family: 'Space Grotesk', sans-serif; }
|
||||
.mono { font-family: 'JetBrains Mono', monospace; }
|
||||
::-webkit-scrollbar { width: 4px; height: 4px; }
|
||||
::-webkit-scrollbar-track { background: ${T.panel}; }
|
||||
::-webkit-scrollbar-thumb { background: ${T.border}; border-radius: 2px; }
|
||||
::-webkit-scrollbar-track { background: var(--panel); }
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
|
||||
/* ── Knob ── */
|
||||
.knob-wrap { display: flex; flex-direction: column; align-items: center; gap: 4px; cursor: pointer; }
|
||||
@@ -156,22 +139,22 @@ const css = `
|
||||
background: radial-gradient(circle at 35% 30%, #3A3A48, #1A1A22);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.07); }
|
||||
.knob-dot { position: absolute; width: 3px; border-radius: 2px; background: var(--amber);
|
||||
left: 50%; transform-origin: bottom center; box-shadow: 0 0 6px var(--amber); }
|
||||
.knob-label { font-size: 9px; color: ${T.textSec}; letter-spacing: .06em; text-transform: uppercase; text-align: center; }
|
||||
.knob-value { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: ${T.amber}; }
|
||||
left: 50%; transform-origin: bottom center; }
|
||||
.knob-label { font-size: 10px; color: var(--text-sec); letter-spacing: .06em; text-transform: uppercase; text-align: center; }
|
||||
.knob-value { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--amber); }
|
||||
|
||||
/* ── Buttons ── */
|
||||
.btn { padding: 8px 14px; border-radius: 6px; font-size: 12px; font-weight: 600;
|
||||
letter-spacing: .04em; border: none; cursor: pointer; transition: all .12s; }
|
||||
.btn-primary { background: ${T.amber}; color: #000; }
|
||||
.btn-primary { background: var(--amber); color: #000; }
|
||||
.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: 36px; height: 36px; border-radius: 7px; display: flex; align-items: center;
|
||||
justify-content: center; background: ${T.surface}; border: 1px solid ${T.border}; cursor: pointer;
|
||||
.btn-ghost { background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); }
|
||||
.btn-ghost:active { background: var(--border); }
|
||||
.btn-icon { width: 44px; height: 44px; border-radius: 8px; display: flex; align-items: center;
|
||||
justify-content: center; background: var(--surface); border: 1px solid var(--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-icon.active { background: rgba(232,160,48,.15); border-color: var(--amber); color: var(--amber); }
|
||||
.btn:focus-visible { outline: 2px solid #3AB87A; outline-offset: 2px; }
|
||||
.btn-icon:focus-visible { outline: 2px solid #3AB87A; outline-offset: 2px; }
|
||||
|
||||
@@ -179,43 +162,43 @@ const css = `
|
||||
.badge { display: inline-flex; align-items: center; justify-content: center;
|
||||
padding: 2px 7px; border-radius: 4px; font-size: 10px; font-weight: 700;
|
||||
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(74,139,200,.2); color: ${T.blue}; }
|
||||
.badge-amber { background: rgba(232,160,48,.2); color: var(--amber); }
|
||||
.badge-green { background: rgba(58,184,122,.2); color: var(--green); }
|
||||
.badge-blue { background: rgba(74,139,200,.2); color: var(--blue); }
|
||||
|
||||
/* ── Param slider ── */
|
||||
.param-panel { background: ${T.surface}; padding: 8px 12px 6px; border-top: 1px solid ${T.border}; flex-shrink: 0; }
|
||||
.param-panel { background: var(--surface); padding: 8px 12px 6px; border-top: 1px solid var(--border); flex-shrink: 0; }
|
||||
.param-panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px; }
|
||||
.param-panel-title { font-size: 10px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; color: ${T.textSec}; }
|
||||
.param-panel-title { font-size: 11px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; color: var(--text-sec); }
|
||||
.param-row { display: flex; flex-direction: column; gap: 1px; padding: 1px 0; }
|
||||
.param-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.param-label { font-size: 9px; font-weight: 500; text-transform: uppercase; letter-spacing: .06em; color: ${T.textSec}; }
|
||||
.param-label { font-size: 10px; font-weight: 500; text-transform: uppercase; letter-spacing: .06em; color: var(--text-sec); }
|
||||
.param-val { font-family: 'JetBrains Mono',monospace; font-size: 11px; font-weight: 600; }
|
||||
.param-track-wrap { position: relative; height: 30px; display: flex; align-items: center; cursor: pointer; touch-action: none; }
|
||||
.param-track { width: 100%; height: 6px; background: ${T.border}; border-radius: 3px; position: relative; overflow: hidden; }
|
||||
.param-fill { height: 100%; border-radius: 3px; position: absolute; top: 0; left: 0; transition: width .04s; }
|
||||
.param-track { width: 100%; height: 6px; background: var(--border); border-radius: 3px; position: relative; overflow: hidden; }
|
||||
.param-fill { height: 100%; border-radius: 3px; position: absolute; top: 0; left: 0; transform-origin: left center; transition: transform .04s; }
|
||||
.param-thumb { width: 22px; height: 22px; border-radius: 50%; position: absolute; top: 50%;
|
||||
transform: translate(-50%,-50%); box-shadow: 0 1px 6px rgba(0,0,0,.6); pointer-events: none;
|
||||
border: 3px solid ${T.bg}; }
|
||||
.param-btn { width: 28px; height: 28px; border-radius: 50%; border: 1px solid ${T.border};
|
||||
background: ${T.panel}; color: ${T.textPrimary}; font-size: 14px; font-weight: 700;
|
||||
border: 3px solid var(--bg); }
|
||||
.param-btn { width: 28px; height: 28px; border-radius: 50%; border: 1px solid var(--border);
|
||||
background: var(--panel); color: var(--text-primary); font-size: 14px; font-weight: 700;
|
||||
display: flex; align-items: center; justify-content: center; cursor: pointer; line-height: 1; }
|
||||
.param-btn:active { background: ${T.border}; transform: scale(.92); }
|
||||
.param-btn:active { background: var(--border); transform: scale(.92); }
|
||||
.param-knobs { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; padding: 4px 0; }
|
||||
|
||||
/* ── Screen header (overlays) ── */
|
||||
.screen-header { padding: 10px 14px 8px; display: flex; align-items: center;
|
||||
justify-content: space-between; border-bottom: 1px solid ${T.border}; }
|
||||
.screen-title { font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: ${T.textSec}; }
|
||||
justify-content: space-between; border-bottom: 1px solid var(--border); }
|
||||
.screen-title { font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--text-sec); }
|
||||
.screen-body { flex: 1; overflow-y: auto; padding: 10px 12px; display: flex; flex-direction: column; gap: 10px; }
|
||||
|
||||
/* ── Preset sidebar chip ── */
|
||||
.preset-chip-s { width: 100%; padding: 7px 6px; border-radius: 5px; background: ${T.surface}; border: 1px solid ${T.border};
|
||||
.preset-chip-s { width: 100%; padding: 8px 8px; border-radius: 5px; background: var(--surface); border: 1px solid var(--border);
|
||||
display: flex; flex-direction: column; align-items: center; cursor: pointer; transition: all .12s; }
|
||||
.preset-chip-s.active { border-color: ${T.amber}; background: rgba(232,160,48,.08); }
|
||||
.preset-chip-s.active { border-color: var(--amber); background: rgba(232,160,48,.08); }
|
||||
.preset-chip-s:active { transform: scale(.96); }
|
||||
.preset-chip-s .pcs-num { font-family: 'JetBrains Mono',monospace; font-size: 14px; font-weight: 700; color: ${T.amber}; line-height: 1.1; }
|
||||
.preset-chip-s .pcs-name { font-size: 9px; color: ${T.textSec}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; text-align: center; line-height: 1.2; }
|
||||
.preset-chip-s .pcs-num { font-family: 'JetBrains Mono',monospace; font-size: 14px; font-weight: 700; color: var(--amber); line-height: 1.1; }
|
||||
.preset-chip-s .pcs-name { font-size: 9px; color: var(--text-sec); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; text-align: center; line-height: 1.2; }
|
||||
.preset-chip-s .pcs-indicator { width: 4px; height: 4px; border-radius: 50%; margin-top: 3px; }
|
||||
|
||||
/* ── Preset row (captures) ── */
|
||||
@@ -223,9 +206,9 @@ const css = `
|
||||
border-radius: 6px; cursor: pointer; transition: background .12s; border: 1px solid transparent; }
|
||||
.preset-row:active, .preset-row.active { background: rgba(232,160,48,.08); border-color: rgba(232,160,48,.25); }
|
||||
.section-label { font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
|
||||
color: ${T.textDim}; font-weight: 600; margin-bottom: 8px; }
|
||||
.card { background: ${T.panel}; border: 1px solid ${T.border}; border-radius: 8px; padding: 14px; }
|
||||
.card-sm { background: ${T.surface}; border: 1px solid ${T.border}; border-radius: 6px; padding: 10px; }
|
||||
color: var(--text-dim); font-weight: 600; margin-bottom: 8px; }
|
||||
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px; }
|
||||
.card-sm { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px; }
|
||||
|
||||
/* ── Responsive: narrow screens ── */
|
||||
@media (max-width: 640px) {
|
||||
@@ -239,7 +222,7 @@ const css = `
|
||||
.screen-title { font-size: 11px; }
|
||||
.screen-body { padding: 8px 10px; gap: 8px; }
|
||||
.knob-wrap { transform: scale(.9); }
|
||||
.btn-icon { width: 24px; height: 24px; font-size: 10px; }
|
||||
.btn-icon { width: 44px; height: 44px; font-size: 12px; }
|
||||
}
|
||||
|
||||
/* ── Responsive: landscape short ── */
|
||||
@@ -288,7 +271,7 @@ function Knob({ label, value = 50, onChange, size = 48, color = T.amber, min = 0
|
||||
<div className="knob" style={{width:size,height:size}} onMouseDown={handleStart} onTouchStart={handleStart}>
|
||||
<svg style={{position:"absolute",inset:-6,width:svgSize,height:svgSize,overflow:"visible"}}>
|
||||
<circle cx={cx} cy={cy} r={r} fill="none" stroke={T.border} strokeWidth="2" strokeLinecap="round" opacity=".5"/>
|
||||
{norm>0&&<path d={`M ${lx} ${ly} A ${r} ${r} 0 ${large} 1 ${ex} ${ey}`} fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" style={{filter:`drop-shadow(0 0 3px ${color})`}}/>}
|
||||
{norm>0&&<path d={`M ${lx} ${ly} A ${r} ${r} 0 ${large} 1 ${ex} ${ey}`} fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" style={{opacity:0.8}}/>}
|
||||
</svg>
|
||||
<div className="knob-inner" style={{"--amber":color}}>
|
||||
<div className="knob-dot" style={{height:dotH,top:dotTop,transform:`translateX(-50%) rotate(${angle}deg)`,transformOrigin:`50% ${dotH}px`}}/>
|
||||
@@ -340,7 +323,7 @@ function ParamSlider({ label,value=50,onChange,onChangeEnd,min=0,max=100,color=T
|
||||
</div>
|
||||
<div className="param-track-wrap" ref={trackRef} onMouseDown={handleStart} onTouchStart={handleStart}>
|
||||
<div className="param-track">
|
||||
<div className="param-fill" style={{width:`${pct}%`,background:color}}/>
|
||||
<div className="param-fill" style={{transform:`scaleX(${pct/100})`,background:color}}/>
|
||||
<div className="param-thumb" style={{left:`${pct}%`,background:color}}/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -394,7 +377,7 @@ function ParameterPanel({ block,params,onParamChange,onParamChangeEnd,viewMode,o
|
||||
<div style={{display:'flex',gap:4,alignItems:'center'}}>
|
||||
{onFocus&&<button className="btn" style={{padding:'4px 8px',fontSize:9,fontWeight:600,background:`${T.amber}22`,border:`1px solid ${T.amber}44`,color:T.amber,borderRadius:5,letterSpacing:'.04em',cursor:'pointer'}}
|
||||
onClick={onFocus}>🔍 Focus</button>}
|
||||
<button className="btn-icon" style={{width:26,height:26,fontSize:11,flexShrink:0}}
|
||||
<button className="btn-icon" style={{width:44,height:44,fontSize:14,flexShrink:0}}
|
||||
onClick={onToggleView}>{viewMode==='slider'?'◉':'▦'}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -426,7 +409,7 @@ function VUMeter({level=0,height=24,vertical=false}){const segs=12,active=Math.r
|
||||
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(<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: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>);
|
||||
{Array.from({length:segs}).map((_,i)=>(<div key={i} style={{width:4,height:segH,borderRadius:1.5,background:i<active?c(i):T.border,transition:"all .2s ease, opacity .15s"}}/>))}</div>);
|
||||
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:"all .2s ease"}}/>))}</div>);
|
||||
}
|
||||
@@ -439,7 +422,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(<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:36,height:36,fontSize:13}}onClick={onClose}>✕</button></div>
|
||||
<div className="screen-header"><div className="screen-title">Downloads</div><button className="btn-icon" style={{width:44,height:44,fontSize:16}}onClick={onClose}>✕</button></div>
|
||||
<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==="irs"?"active":""}`}onClick={()=>setTab("irs")}>IR Cabs</button></div></div>
|
||||
@@ -447,7 +430,7 @@ function CapturesScreen({onClose}){
|
||||
<div style={{flex:1,display:"flex",background:T.surface,borderRadius:6,border:`1px solid ${T.border}`,padding:"6px 10px",gap:6,alignItems:"center"}}>
|
||||
<span style={{color:T.textDim,fontSize:11}}>🔍</span>
|
||||
<input placeholder={`Search ${tab==="models"?"NAM models":"IR cabs"}...`}value={query}onChange={e=>setQuery(e.target.value)}onKeyDown={e=>e.key==="Enter"&&doSearch()}style={{flex:1,background:"none",border:"none",color:T.textPrimary,fontSize:12,outline:"none"}}/></div>
|
||||
<button className="btn btn-primary"style={{padding:"6px 11px",fontSize:11}}onClick={doSearch}>{loading?"...":"Search"}</button></div>
|
||||
<button className="btn btn-primary"style={{padding:"6px 11px",fontSize:11,color:"#000"}}onClick={doSearch}>{loading?"...":"Search"}</button></div>
|
||||
<div className="screen-body">
|
||||
{results===null&&models.length>0&&(<><div className="section-label">Installed Models</div>
|
||||
{models.map((m,i)=>(<div key={i}className="preset-row"style={{cursor:"default"}}>
|
||||
@@ -848,19 +831,19 @@ export default function App(){
|
||||
padding:isLandscapeShort?"2px 10px":"4px 12px",background:T.panel,borderBottom:`1px solid ${T.border}`,flexShrink:0,minHeight:isLandscapeShort?26:32}}>
|
||||
<div style={{display:"flex",gap:8,alignItems:"center"}}>
|
||||
{/* Sidebar toggle (narrow only) */}
|
||||
{isNarrow&&<button className="btn-icon" style={{width:24,height:24,fontSize:13,position:'relative',marginRight:-2}}
|
||||
{isNarrow&&<button className="btn-icon" style={{width:44,height:44,fontSize:16,position:'relative',marginRight:-2}}
|
||||
onClick={()=>setSidebarOpen(s=>!s)} title="Presets">
|
||||
<span style={{fontSize:12,lineHeight:1}}>▤</span>
|
||||
</button>}
|
||||
<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}}/>
|
||||
{!isNarrow&&<span className="mono" style={{fontSize:9,color:state.connected?T.green:T.red}}>{state.connected?"CONNECTED":"OFFLINE"}</span>}
|
||||
{/* 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:44,height:44,fontSize:14,opacity:undoStack.length>0?1:0.3}}
|
||||
onClick={handleUndo} disabled={undoStack.length===0} title="Undo (Ctrl+Z)">
|
||||
↶
|
||||
</button>
|
||||
<button className="btn-icon" style={{width:20,height:20,fontSize:11,opacity:redoStack.length>0?1:0.3}}
|
||||
<button className="btn-icon" style={{width:44,height:44,fontSize:14,opacity:redoStack.length>0?1:0.3}}
|
||||
onClick={handleRedo} disabled={redoStack.length===0} title="Redo (Ctrl+Shift+Z)">
|
||||
↷
|
||||
</button>
|
||||
@@ -876,34 +859,34 @@ export default function App(){
|
||||
<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 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:44,height:44,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}}/>
|
||||
</button>
|
||||
<button className="btn-icon" style={{width:24,height:24,position:'relative'}}onClick={()=>setView("presets")}title="Presets">
|
||||
<button className="btn-icon" style={{width:44,height:44,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}}/>
|
||||
</button>
|
||||
<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={{
|
||||
padding:"1px 5px",borderRadius:3,border:"none",height:20,
|
||||
padding:"1px 5px",borderRadius:3,border:"none",height:44,
|
||||
fontSize:8,fontWeight:700,letterSpacing:".04em",textTransform:"uppercase",
|
||||
background:footswitchMode===m?T.amber:"transparent",
|
||||
color:footswitchMode===m?"#000":T.textDim,
|
||||
cursor:"pointer",lineHeight:1.5,minWidth:34,display:"flex",alignItems:"center",justifyContent:"center",
|
||||
cursor:"pointer",lineHeight:1.5,minWidth:44,display:"flex",alignItems:"center",justifyContent:"center",
|
||||
}}>{m==="stomp"?"SM":"PR"}</button>))}
|
||||
</div>
|
||||
<button className="btn-icon" style={{width:24,height:24,position:'relative',
|
||||
background:state.tuner_enabled?'rgba(128,208,160,.15)':T.surface,
|
||||
borderColor:state.tuner_enabled?T.green:T.border}}
|
||||
onClick={handleTunerToggle} title={state.tuner_enabled?"Exit Tuner":"Tuner"}>
|
||||
<button className="btn-icon" style={{width:44,height:44,position:"relative",
|
||||
background:state.tuner_enabled?"rgba(128,208,160,.15)":T.surface,
|
||||
borderColor:state.tuner_enabled?T.green:T.border}}
|
||||
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?'brightness(0) saturate(100%) invert(0.9) sepia(0.4) hue-rotate(80deg)':'none'}}/>
|
||||
</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:44,height:44,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">
|
||||
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<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:44,height:44,position:"relative"}}
|
||||
onClick={()=>setMenuOpen(true)} title="Main Menu">
|
||||
<span style={{fontSize:14,color:T.amber,lineHeight:1}}>☰</span>
|
||||
</button>
|
||||
@@ -1044,7 +1027,7 @@ export default function App(){
|
||||
{view==="captures"&&<CapturesScreen onClose={()=>setView("main")}/>}
|
||||
{view==="presets"&&(
|
||||
<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:36,height:36,fontSize:13}}onClick={()=>setView("main")}>✕</button></div>
|
||||
<div className="screen-header"><div className="screen-title">Presets</div><button className="btn-icon"style={{width:44,height:44,fontSize:16}}onClick={()=>setView("main")}>✕</button></div>
|
||||
<div className="screen-body">
|
||||
{presets.map((p,i)=>(
|
||||
<div key={i} className={`preset-row ${p.num===currentPreset?"active":""}`}
|
||||
@@ -1119,7 +1102,7 @@ export default function App(){
|
||||
<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>
|
||||
<button className="btn-icon" style={{width:44,height:44,fontSize:14}}onClick={()=>setOverlayView(null)}>✕</button>
|
||||
</div>
|
||||
<div style={{display:"flex",flexDirection:"column",gap:12}}>
|
||||
<div>
|
||||
@@ -1150,12 +1133,12 @@ export default function App(){
|
||||
<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>
|
||||
<button className="btn-icon" style={{width:44,height:44,fontSize:14}}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}}
|
||||
<button className="btn-icon" style={{width:44,height:44,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>
|
||||
@@ -1166,7 +1149,7 @@ export default function App(){
|
||||
{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}}
|
||||
<button className="btn-icon" style={{width:44,height:44,fontSize:11,background:b.bypassed?`${T.red}22`:T.surface,borderColor:b.bypassed?T.red:T.border}}
|
||||
onClick={()=>handleToggleBlock(b.id)}>
|
||||
{b.bypassed?"BYP":"ACT"}
|
||||
</button>
|
||||
@@ -1197,12 +1180,12 @@ export default function App(){
|
||||
<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>
|
||||
<button className="btn-icon" style={{width:44,height:44,fontSize:14}}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}}>📶 Wi‑Fi</span>
|
||||
<button className="btn-icon" style={{width:40,height:26,fontSize:10}}>Scan</button>
|
||||
<button className="btn-icon" style={{width:44,height:44,fontSize:11}}>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>
|
||||
@@ -1210,7 +1193,7 @@ export default function App(){
|
||||
<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>
|
||||
<button className="btn-icon" style={{width:44,height:44,fontSize:11,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>
|
||||
@@ -1225,7 +1208,7 @@ export default function App(){
|
||||
<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>
|
||||
<button className="btn-icon" style={{width:44,height:44,fontSize:14}}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])=>(
|
||||
|
||||
+3
-20
@@ -1,21 +1,5 @@
|
||||
import { useState, useRef, useCallback, useMemo } from "react";
|
||||
|
||||
// ── Design tokens ──────────────────────────────────
|
||||
const T = {
|
||||
bg: "#0A0A0C",
|
||||
panel: "#141418",
|
||||
surface: "#1C1C22",
|
||||
border: "#2A2A32",
|
||||
amber: "#E8A030",
|
||||
amberDim: "#7A5218",
|
||||
blue: "#4A8BC8",
|
||||
blueDim: "#1E4060",
|
||||
green: "#3AB87A",
|
||||
red: "#E06060",
|
||||
textPrimary: "#F0EDE6",
|
||||
textSec: "#A0A0BC",
|
||||
textDim: "#7878A0",
|
||||
};
|
||||
import { T } from "./theme.js";
|
||||
|
||||
// ── Split type metadata ────────────────────────────
|
||||
const SPLIT_TYPES = {
|
||||
@@ -32,7 +16,7 @@ const TYPE_COLORS = {
|
||||
fuzz: "#D060E0",
|
||||
mod: T.blue, chorus: T.blue, flange: T.blue, phaser: T.blue,
|
||||
tremolo: T.blue, vibrato: T.blue, rotary: T.blue,
|
||||
delay: "#60A0E0", echo: "#60A0E0", reverb: "#60C0D0",
|
||||
delay: "#6080B0", echo: "#6080B0", reverb: "#5090A0",
|
||||
pitch: "#A060E0", comp: T.green, compressor: T.green,
|
||||
gate: T.green, filter: "#D0A060", wah: "#D0A060",
|
||||
eq: "#80A0B0", ir: T.green, cab: T.green, nam: "#E0A040",
|
||||
@@ -389,12 +373,11 @@ function BlockTile({ block, selected, onSelect, onToggle, pathLabel, onModelBrow
|
||||
<div style={{
|
||||
position: "absolute", top: 0, left: 0, right: 0, height: 3,
|
||||
background: color,
|
||||
boxShadow: enabled ? `0 0 6px ${color}` : "none",
|
||||
}} />
|
||||
{/* Drag handle */}
|
||||
<div style={{
|
||||
position: "absolute", top: 5, left: 5, fontSize: 9, color: T.textDim,
|
||||
letterSpacing: 1.5, lineHeight: 1, cursor: "grab", opacity: 0.5,
|
||||
letterSpacing: 1.5, lineHeight: 1, cursor: "grab",
|
||||
}}>
|
||||
⋮⋮
|
||||
</div>
|
||||
|
||||
+11
-26
@@ -1,21 +1,5 @@
|
||||
import { useState, useRef, useCallback, useMemo } from "react";
|
||||
|
||||
// ── Design tokens ──
|
||||
const T = {
|
||||
bg: "#0A0A0C",
|
||||
panel: "#141418",
|
||||
surface: "#1C1C22",
|
||||
border: "#2A2A32",
|
||||
amber: "#E8A030",
|
||||
amberDim: "#7A5218",
|
||||
blue: "#4A8BC8",
|
||||
blueDim: "#1E4060",
|
||||
green: "#3AB87A",
|
||||
red: "#E06060",
|
||||
textPrimary: "#F0EDE6",
|
||||
textSec: "#A0A0BC",
|
||||
textDim: "#7878A0",
|
||||
};
|
||||
import { T } from "./theme.js";
|
||||
|
||||
// ── Block param definitions (mirrored from App.jsx) ──
|
||||
const BLOCK_PARAMS = {
|
||||
@@ -37,7 +21,7 @@ const BLOCK_PARAMS = {
|
||||
|
||||
const BLOCK_COLORS = {
|
||||
overdrive: T.amber, distortion: T.red, fuzz: '#C07030',
|
||||
delay: T.blue, echo: T.blue, reverb: '#60B0D0',
|
||||
delay: T.blue, echo: T.blue, reverb: '#5090A0',
|
||||
chorus: T.green, flanger: '#50C080', phaser: '#80C050',
|
||||
tremolo: T.green, compressor: '#D09040', gate: T.textSec, eq: '#7080D0',
|
||||
cabinet: '#D07090', ir: '#D07090', nam: '#E0A040', boost: T.amber,
|
||||
@@ -111,7 +95,7 @@ function FocusKnob({ label, value = 50, onChange, size = 72, color = T.amber, mi
|
||||
onMouseDown={handleStart} onTouchStart={handleStart}>
|
||||
<svg style={{ position: 'absolute', inset: -8, width: svgSize, height: svgSize, overflow: 'visible' }}>
|
||||
<circle cx={cx} cy={cy} r={r} fill="none" stroke={T.border} strokeWidth="3" strokeLinecap="round" opacity=".5" />
|
||||
{norm > 0 && <path d={`M ${lx} ${ly} A ${r} ${r} 0 ${large} 1 ${ex} ${ey}`} fill="none" stroke={color} strokeWidth="3" strokeLinecap="round" style={{ filter: `drop-shadow(0 0 4px ${color})` }} />}
|
||||
{norm > 0 && <path d={`M ${lx} ${ly} A ${r} ${r} 0 ${large} 1 ${ex} ${ey}`} fill="none" stroke={color} strokeWidth="3" strokeLinecap="round" style={{ opacity: 0.8 }} />}
|
||||
</svg>
|
||||
<div style={{
|
||||
borderRadius: '50%', width: '100%', height: '100%', position: 'relative',
|
||||
@@ -124,7 +108,7 @@ function FocusKnob({ label, value = 50, onChange, size = 72, color = T.amber, mi
|
||||
height: dotH, top: dotTop,
|
||||
transform: `translateX(-50%) rotate(${angle}deg)`,
|
||||
transformOrigin: `50% ${dotH}px`,
|
||||
boxShadow: `0 0 6px ${color}`,
|
||||
boxShadow: `0 0 4px ${color}33`,
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,7 +164,7 @@ function FocusSlider({ label, value = 50, onChange, onChangeEnd, min = 0, max =
|
||||
<span style={{ fontSize: 11, fontWeight: 500, textTransform: 'uppercase', letterSpacing: '.06em', color }}>{label}</span>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<button style={{
|
||||
width: 34, height: 34, borderRadius: '50%', border: `1px solid ${T.border}`,
|
||||
width: 44, height: 44, borderRadius: '50%', border: `1px solid ${T.border}`,
|
||||
background: T.panel, color: T.textPrimary, fontSize: 16, fontWeight: 700,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
|
||||
lineHeight: 1,
|
||||
@@ -190,7 +174,7 @@ function FocusSlider({ label, value = 50, onChange, onChangeEnd, min = 0, max =
|
||||
minWidth: 40, textAlign: 'center',
|
||||
}}>{touchVal ?? value}</span>
|
||||
<button style={{
|
||||
width: 34, height: 34, borderRadius: '50%', border: `1px solid ${T.border}`,
|
||||
width: 44, height: 44, borderRadius: '50%', border: `1px solid ${T.border}`,
|
||||
background: T.panel, color: T.textPrimary, fontSize: 16, fontWeight: 700,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
|
||||
lineHeight: 1,
|
||||
@@ -209,9 +193,10 @@ function FocusSlider({ label, value = 50, onChange, onChangeEnd, min = 0, max =
|
||||
}}>
|
||||
<div style={{
|
||||
height: '100%', borderRadius: 5, position: 'absolute', top: 0, left: 0,
|
||||
width: `${pct}%`, background: color,
|
||||
boxShadow: `0 0 8px ${color}44`,
|
||||
transition: 'width .03s',
|
||||
width: '100%', background: color,
|
||||
transition: 'transform .03s',
|
||||
transform: `scaleX(${pct/100})`,
|
||||
transformOrigin: 'left center',
|
||||
}} />
|
||||
</div>
|
||||
<div style={{
|
||||
@@ -352,7 +337,7 @@ export default function FocusView({
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
|
||||
<button onClick={onClose}
|
||||
style={{
|
||||
width: 34, height: 34, borderRadius: 8,
|
||||
width: 44, height: 44, borderRadius: 8,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: T.surface, border: `1px solid ${T.border}`,
|
||||
color: T.textPrimary, fontSize: 16, cursor: 'pointer',
|
||||
|
||||
+4
-11
@@ -1,15 +1,10 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
const T = {
|
||||
bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32",
|
||||
amber: "#E8A030", amberDim: "#7A5218", blue: "#4A8BC8", blueDim: "#1E4060",
|
||||
green: "#3AB87A", red: "#E06060", textPrimary: "#F0EDE6", textSec: "#A0A0BC", textDim: "#7878A0",
|
||||
};
|
||||
import { T } from "./theme.js";
|
||||
|
||||
const TYPE_COLORS = {
|
||||
overdrive: T.amber, drive: T.amber, distortion: T.red, fuzz: "#D060E0",
|
||||
chorus: T.blue, flanger: T.blue, phaser: T.blue, tremolo: T.blue,
|
||||
delay: "#60A0E0", echo: "#60A0E0", reverb: "#60C0D0",
|
||||
delay: "#6080B0", echo: "#6080B0", reverb: "#5090A0",
|
||||
compressor: T.green, gate: T.green, eq: "#80A0B0",
|
||||
ir: T.green, cab: T.green, nam: "#E0A040",
|
||||
};
|
||||
@@ -65,8 +60,8 @@ function Footswitch({ id, label, mainLabel, type, sub, active, enabled, onClick,
|
||||
cursor: "pointer", minHeight: 58, position: "relative",
|
||||
transition: "all .1s", WebkitTapHighlightColor: "transparent",
|
||||
boxShadow: isActive && !isBypassSwitch
|
||||
? `0 0 8px ${T.amber}33, inset 0 0 0 1px ${T.amber}22`
|
||||
: isBypassSwitch && !enabled ? `0 0 8px ${T.red}33` : "none",
|
||||
? `inset 0 0 0 1px ${T.amber}22`
|
||||
: isBypassSwitch && !enabled ? `inset 0 0 0 1px ${T.red}22` : "none",
|
||||
}}
|
||||
>
|
||||
{/* Scribble strip */}
|
||||
@@ -75,7 +70,6 @@ function Footswitch({ id, label, mainLabel, type, sub, active, enabled, onClick,
|
||||
border: `1px solid ${isActive ? T.amber + "66" : T.border}`,
|
||||
borderRadius: 3, padding: "2px 2px", textAlign: "center",
|
||||
minHeight: 18, display: "flex", alignItems: "center", justifyContent: "center",
|
||||
boxShadow: isActive ? `inset 0 0 4px ${T.amber}22` : "none",
|
||||
}}>
|
||||
<span style={{
|
||||
fontFamily: "'JetBrains Mono', monospace", fontSize: 7, fontWeight: 700,
|
||||
@@ -110,7 +104,6 @@ function Footswitch({ id, label, mainLabel, type, sub, active, enabled, onClick,
|
||||
position: "absolute", top: 3, right: 3, width: 4, height: 4,
|
||||
borderRadius: "50%",
|
||||
background: enabled !== false ? T.green : T.textDim,
|
||||
boxShadow: enabled !== false ? `0 0 5px ${T.green}` : "none",
|
||||
transition: "all .15s",
|
||||
}} />
|
||||
)}
|
||||
|
||||
+4
-9
@@ -1,14 +1,9 @@
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
|
||||
const T = {
|
||||
bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32",
|
||||
amber: "#E8A030", amberDim: "#7A5218", blue: "#4A8BC8", blueDim: "#1E4060",
|
||||
green: "#3AB87A", red: "#E06060", textPrimary: "#F0EDE6", textSec: "#A0A0BC", textDim: "#7878A0",
|
||||
};
|
||||
import { T } from "./theme.js";
|
||||
|
||||
// ── EQ Band definitions ──
|
||||
const EQ_BANDS = [
|
||||
{ key: "low", label: "Low", freq: "80 Hz", color: "#60A0E0", shadow: "#60A0E044" },
|
||||
{ key: "low", label: "Low", freq: "80 Hz", color: "#5090A0", shadow: "#5090A044" },
|
||||
{ key: "mid", label: "Mid", freq: "800 Hz", color: T.amber, shadow: `${T.amber}44` },
|
||||
{ key: "high", label: "High", freq: "6.4 kHz",color: "#60D080", shadow: "#60D08044" },
|
||||
];
|
||||
@@ -64,7 +59,7 @@ function EqKnob({ label, freq, value, min, max, color, onChange }) {
|
||||
{norm > 0 && (
|
||||
<path d={`M ${lx} ${ly} A ${r} ${r} 0 ${large} 1 ${ex} ${ey}`}
|
||||
fill="none" stroke={color} strokeWidth="3" strokeLinecap="round"
|
||||
style={{ filter: `drop-shadow(0 0 4px ${color})` }} />
|
||||
style={{ opacity: 0.8 }} />
|
||||
)}
|
||||
</svg>
|
||||
<div style={{
|
||||
@@ -78,7 +73,7 @@ function EqKnob({ label, freq, value, min, max, color, onChange }) {
|
||||
height: dotH, top: dotTop,
|
||||
transform: `translateX(-50%) rotate(${angle}deg)`,
|
||||
transformOrigin: `50% ${dotH}px`,
|
||||
boxShadow: `0 0 6px ${color}`,
|
||||
boxShadow: `0 0 4px ${color}33`,
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+6
-11
@@ -1,10 +1,5 @@
|
||||
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
||||
|
||||
const T = {
|
||||
bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32",
|
||||
amber: "#E8A030", amberDim: "#7A5218", blue: "#4A8BC8", blueDim: "#1E4060",
|
||||
green: "#3AB87A", red: "#E06060", textPrimary: "#F0EDE6", textSec: "#A0A0BC", textDim: "#7878A0",
|
||||
};
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from "react";
|
||||
import { T } from "./theme.js";
|
||||
|
||||
async function api(method, path, body) {
|
||||
const opts = { method, headers: { "Content-Type": "application/json" } };
|
||||
@@ -174,7 +169,7 @@ function SettingsSlider({ label, value, min, max, unit, onChange }) {
|
||||
<span style={{ fontSize: 11, fontWeight: 500, color: T.textSec }}>{label}</span>
|
||||
<div style={{ display: "flex", gap: 6, alignItems: "center" }}>
|
||||
<button style={{
|
||||
width: 26, height: 26, borderRadius: "50%", border: `1px solid ${T.border}`,
|
||||
width: 44, height: 44, borderRadius: "50%", border: `1px solid ${T.border}`,
|
||||
background: T.panel, color: T.textPrimary, fontSize: 14, fontWeight: 700,
|
||||
display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", lineHeight: 1,
|
||||
}} onMouseDown={quick(-1)} onTouchStart={quick(-1)}>−</button>
|
||||
@@ -183,7 +178,7 @@ function SettingsSlider({ label, value, min, max, unit, onChange }) {
|
||||
minWidth: 36, textAlign: "center",
|
||||
}}>{value}{unit}</span>
|
||||
<button style={{
|
||||
width: 26, height: 26, borderRadius: "50%", border: `1px solid ${T.border}`,
|
||||
width: 44, height: 44, borderRadius: "50%", border: `1px solid ${T.border}`,
|
||||
background: T.panel, color: T.textPrimary, fontSize: 14, fontWeight: 700,
|
||||
display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", lineHeight: 1,
|
||||
}} onMouseDown={quick(1)} onTouchStart={quick(1)}>+</button>
|
||||
@@ -196,7 +191,7 @@ function SettingsSlider({ label, value, min, max, unit, onChange }) {
|
||||
cursor: "pointer", touchAction: "none",
|
||||
}}>
|
||||
<div style={{ width: "100%", height: 6, background: T.border, borderRadius: 3, position: "relative", overflow: "hidden" }}>
|
||||
<div style={{ height: "100%", borderRadius: 3, position: "absolute", top: 0, left: 0, width: `${pct}%`, background: T.amber, boxShadow: `0 0 6px ${T.amber}44`, transition: "width .03s" }} />
|
||||
<div style={{ height: "100%", borderRadius: 3, position: "absolute", top: 0, left: 0, width: "100%", background: T.amber, transform: `scaleX(${pct/100})`, transformOrigin: "left center", transition: "transform .03s" }} />
|
||||
</div>
|
||||
<div style={{ width: 24, height: 24, borderRadius: "50%", position: "absolute", top: "50%", left: `${pct}%`, transform: "translate(-50%,-50%)", background: T.amber, border: `3px solid ${T.bg}`, boxShadow: `0 1px 6px rgba(0,0,0,.5)`, pointerEvents: "none", transition: "left .03s" }} />
|
||||
</div>
|
||||
@@ -397,7 +392,7 @@ export default function GlobalSettings({ onClose, onMasterVolume, onAudioProfile
|
||||
<div style={{ position: "fixed", inset: 0, zIndex: 998, display: "flex", flexDirection: "column", background: T.bg, color: T.textPrimary, fontFamily: "'Space Grotesk', sans-serif" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "10px 14px", borderBottom: `1px solid ${T.border}`, background: T.panel, flexShrink: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<button onClick={onClose} style={{ width: 34, height: 34, borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", background: T.surface, border: `1px solid ${T.border}`, color: T.textPrimary, fontSize: 16, cursor: "pointer" }}>✕</button>
|
||||
<button onClick={onClose} style={{ width: 44, height: 44, borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", background: T.surface, border: `1px solid ${T.border}`, color: T.textPrimary, fontSize: 16, cursor: "pointer" }}>✕</button>
|
||||
<div>
|
||||
<div style={{ fontSize: 15, fontWeight: 600 }}>Settings</div>
|
||||
{!loaded && <span style={{ fontSize: 10, color: T.textDim }}>Loading…</span>}
|
||||
|
||||
+1
-6
@@ -1,10 +1,5 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
const T = {
|
||||
bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32",
|
||||
amber: "#E8A030", amberDim: "#7A5218", blue: "#4A8BC8", blueDim: "#1E4060",
|
||||
green: "#3AB87A", red: "#E06060", textPrimary: "#F0EDE6", textSec: "#A0A0BC", textDim: "#7878A0",
|
||||
};
|
||||
import { T } from "./theme.js";
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{ id: "save", label: "Save Preset", icon: "💾", desc: "Save current preset to bank" },
|
||||
|
||||
+6
-23
@@ -1,21 +1,5 @@
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
||||
|
||||
// ── Design tokens ──
|
||||
const T = {
|
||||
bg: "#0A0A0C",
|
||||
panel: "#141418",
|
||||
surface: "#1C1C22",
|
||||
border: "#2A2A32",
|
||||
amber: "#E8A030",
|
||||
amberDim: "#7A5218",
|
||||
blue: "#4A8BC8",
|
||||
blueDim: "#1E4060",
|
||||
green: "#3AB87A",
|
||||
red: "#E06060",
|
||||
textPrimary: "#F0EDE6",
|
||||
textSec: "#A0A0BC",
|
||||
textDim: "#7878A0",
|
||||
};
|
||||
import { T } from "./theme.js";
|
||||
|
||||
const IMG_PATH = (import.meta.env.BASE_URL || '/ui/') + 'img/';
|
||||
|
||||
@@ -24,8 +8,8 @@ const CATEGORY_DEFS = {
|
||||
"Distortions": { icon: "fx_distortion", color: T.red },
|
||||
"Dynamics": { icon: "fx_compressor", color: T.green },
|
||||
"Modulations": { icon: "fx_modulator", color: T.blue },
|
||||
"Delays": { icon: "fx_delay", color: "#60A0E0" },
|
||||
"Reverbs": { icon: "fx_reverb", color: "#60C0D0" },
|
||||
"Delays": { icon: "fx_delay", color: "#6080B0" },
|
||||
"Reverbs": { icon: "fx_reverb", color: "#5090A0" },
|
||||
"Filters": { icon: "fx_filter", color: "#D0A060" },
|
||||
"Amps": { icon: "fx_amplifier", color: "#E0A040" },
|
||||
"Preamps": { icon: "fx_simulator", color: "#D08040" },
|
||||
@@ -75,7 +59,7 @@ const TYPE_COLORS = {
|
||||
fuzz: "#D060E0",
|
||||
mod: T.blue, chorus: T.blue, flange: T.blue, phaser: T.blue,
|
||||
tremolo: T.blue, vibrato: T.blue, rotary: T.blue,
|
||||
delay: "#60A0E0", echo: "#60A0E0", reverb: "#60C0D0",
|
||||
delay: "#6080B0", echo: "#6080B0", reverb: "#5090A0",
|
||||
pitch: "#A060E0", comp: T.green, compressor: T.green,
|
||||
gate: T.green, filter: "#D0A060", wah: "#D0A060",
|
||||
eq: "#80A0B0", ir: T.green, cab: T.green, nam: "#E0A040",
|
||||
@@ -587,7 +571,7 @@ function ModelTile({ model, isCurrent, isFavorite, onSelect, onToggleFav }) {
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
transition: "all .12s",
|
||||
boxShadow: isCurrent ? `0 0 12px ${color}33` : "none",
|
||||
boxShadow: isCurrent ? `0 0 6px ${color}22` : "none",
|
||||
minHeight: 72,
|
||||
}}
|
||||
>
|
||||
@@ -615,7 +599,7 @@ function ModelTile({ model, isCurrent, isFavorite, onSelect, onToggleFav }) {
|
||||
alt={model.type}
|
||||
style={{
|
||||
width: 28, height: 28,
|
||||
filter: `invert(0.95) brightness(2) drop-shadow(0 0 4px ${color}44)`,
|
||||
filter: `invert(0.9) brightness(1.6)`,
|
||||
objectFit: "contain",
|
||||
}}
|
||||
onError={() => setImgError(true)}
|
||||
@@ -672,7 +656,6 @@ function ModelTile({ model, isCurrent, isFavorite, onSelect, onToggleFav }) {
|
||||
position: "absolute", top: 4, left: 4,
|
||||
width: 6, height: 6, borderRadius: "50%",
|
||||
background: T.green,
|
||||
boxShadow: `0 0 6px ${T.green}`,
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
+6
-13
@@ -1,12 +1,5 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
|
||||
// ── Design tokens (mirrored) ──────────────────────────────────
|
||||
const T = {
|
||||
bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32",
|
||||
amber: "#E8A030", amberDim: "#7A5218", blue: "#4A8BC8",
|
||||
green: "#3AB87A", red: "#E06060", textPrimary: "#F0EDE6",
|
||||
textSec: "#A0A0BC", textDim: "#7878A0",
|
||||
};
|
||||
import { T } from "./theme.js";
|
||||
|
||||
// ── String labels matching _STRING_NAMES ──────────────────────
|
||||
const STRING_LABELS = [
|
||||
@@ -286,8 +279,8 @@ export default function TunerScreen({ tunerData, connected, onExit }) {
|
||||
borderRadius: 1,
|
||||
margin: "0 auto",
|
||||
boxShadow: isInTune
|
||||
? `0 0 8px ${T.green}`
|
||||
: `0 0 4px ${isClose ? T.amber : "transparent"}`,
|
||||
? `0 0 4px ${T.green}44`
|
||||
: `0 0 2px ${isClose ? T.amber : "transparent"}`,
|
||||
}} />
|
||||
{/* Needle dot */}
|
||||
<div style={{
|
||||
@@ -297,8 +290,8 @@ export default function TunerScreen({ tunerData, connected, onExit }) {
|
||||
background: isInTune ? T.green : (isClose ? T.amber : T.textPrimary),
|
||||
margin: "-1px auto 0",
|
||||
boxShadow: isInTune
|
||||
? `0 0 12px ${T.green}`
|
||||
: `0 0 6px ${isClose ? T.amber : "transparent"}`,
|
||||
? `0 0 6px ${T.green}44`
|
||||
: `0 0 4px ${isClose ? T.amber : "transparent"}`,
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -341,7 +334,7 @@ export default function TunerScreen({ tunerData, connected, onExit }) {
|
||||
background: isActive ? `${s.color}22` : T.surface,
|
||||
border: `2px solid ${isActive ? s.color : T.border}`,
|
||||
transition: "all .1s",
|
||||
boxShadow: isActive ? `0 0 12px ${s.color}44` : "none",
|
||||
boxShadow: isActive ? `0 0 6px ${s.color}22` : "none",
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
|
||||
+12
-10
@@ -6,13 +6,13 @@
|
||||
--border: #2A2A32;
|
||||
--amber: #E8A030;
|
||||
--amber-dim: #7A5218;
|
||||
--blue: #4A8BC8;
|
||||
--blue: #60A8D8;
|
||||
--blue-dim: #1E4060;
|
||||
--green: #3AB87A;
|
||||
--red: #E06060;
|
||||
--text-primary: #F0EDE6;
|
||||
--text-sec: #A0A0BC;
|
||||
--text-dim: #7878A0;
|
||||
--text-dim: #8484B0;
|
||||
}
|
||||
|
||||
/* ── Reset & base ── */
|
||||
@@ -29,7 +29,7 @@ body { background: #0A0A0C; color: #F0EDE6; font-family: 'Space Grotesk', sans-s
|
||||
letter-spacing: .06em; text-transform: uppercase; }
|
||||
.badge-amber { background: rgba(232,160,48,.2); color: #E8A030; }
|
||||
.badge-green { background: rgba(58,184,122,.2); color: #3AB87A; }
|
||||
.badge-blue { background: rgba(74,139,200,.2); color: #4A8BC8; }
|
||||
.badge-blue { background: rgba(74,139,200,.2); color: #70B0E0; }
|
||||
.badge-red { background: rgba(224,96,96,.2); color: #E06060; }
|
||||
|
||||
/* Button system */
|
||||
@@ -42,9 +42,11 @@ body { background: #0A0A0C; color: #F0EDE6; font-family: 'Space Grotesk', sans-s
|
||||
.btn-ghost { background: transparent; border-color: transparent; }
|
||||
.btn-danger { border-color: #E06060; background: rgba(224,96,96,.15); color: #E06060; }
|
||||
.btn-icon { display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 28px; height: 28px; border-radius: 5px; border: 1px solid #2A2A32;
|
||||
width: 44px; height: 44px; border-radius: 8px; border: 1px solid #2A2A32;
|
||||
background: #1C1C22; cursor: pointer; transition: all .12s; touch-action: manipulation; }
|
||||
.btn-icon:active { transform: scale(.92); }
|
||||
.btn:focus-visible { outline: 2px solid #3AB87A; outline-offset: 2px; }
|
||||
.btn-icon:focus-visible { outline: 2px solid #3AB87A; outline-offset: 2px; }
|
||||
|
||||
/* Slide panel (bottom drawer) */
|
||||
.slide-panel { position: fixed; inset: 0; background: rgba(0,0,0,.7); z-index: 100;
|
||||
@@ -73,7 +75,7 @@ body { background: #0A0A0C; color: #F0EDE6; font-family: 'Space Grotesk', sans-s
|
||||
.snap-name { font-size: 9px; color: #F0EDE6; margin-top: 2px; word-break: break-all; line-height: 1.3; }
|
||||
.snap-empty { font-size: 9px; color: #444458; margin-top: 2px; font-style: italic; }
|
||||
.snap-save-indicator { position: absolute; top: 4px; right: 4px; width: 6px; height: 6px;
|
||||
border-radius: 50%; background: #3AB87A; box-shadow: 0 0 4px #3AB87A; }
|
||||
border-radius: 50%; background: #3AB87A; }
|
||||
.snap-name-input { width: 100%; background: #0A0A0C; border: 1px solid #2A2A32;
|
||||
border-radius: 3px; padding: 2px 4px; font-size: 9px; color: #F0EDE6;
|
||||
text-align: center; outline: none; margin-top: 2px; }
|
||||
@@ -130,11 +132,11 @@ body { background: #0A0A0C; color: #F0EDE6; font-family: 'Space Grotesk', sans-s
|
||||
/* NAM chip */
|
||||
.nam-chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px;
|
||||
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: #4A8BC8; }
|
||||
border-radius: 5px; font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--blue); }
|
||||
|
||||
/* Bypass LED */
|
||||
.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; }
|
||||
.led-off { background: #444458; }
|
||||
|
||||
/* ── Reduced motion ── */
|
||||
@@ -186,9 +188,9 @@ body { background: #0A0A0C; color: #F0EDE6; font-family: 'Space Grotesk', sans-s
|
||||
font-size: 11px;
|
||||
}
|
||||
.btn-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 10px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.card {
|
||||
padding: 10px;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* theme.js — Shared design tokens
|
||||
*
|
||||
* Single source of truth: CSS custom properties in index.css :root {}
|
||||
* This module reads them at load time so JS components use the same
|
||||
* values as the stylesheet. To theme the app, override the CSS vars.
|
||||
*/
|
||||
|
||||
function v(name) {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
}
|
||||
|
||||
export const T = {
|
||||
bg: v('--bg'),
|
||||
panel: v('--panel'),
|
||||
surface: v('--surface'),
|
||||
border: v('--border'),
|
||||
amber: v('--amber'),
|
||||
amberDim: v('--amber-dim'),
|
||||
blue: v('--blue'),
|
||||
blueDim: v('--blue-dim'),
|
||||
green: v('--green'),
|
||||
red: v('--red'),
|
||||
textPrimary: v('--text-primary'),
|
||||
textSec: v('--text-sec'),
|
||||
textDim: v('--text-dim'),
|
||||
};
|
||||
Reference in New Issue
Block a user