diff --git a/src/App.jsx b/src/App.jsx index 6818aa3..9f237f4 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -23,7 +23,7 @@ const API = { listModels: () => api("GET", "/api/models"), loadModel: (path) => api("POST", "/api/models/load", { path }), listPresets: () => api("GET", "/api/presets"), - loadPreset: (bank, program) => api("GET", "/api/presets", { bank, program }), + loadPreset: (bank, program) => api("GET", "/api/presets/{bank}/{program}".replace("{bank}",bank).replace("{program}",program)), savePreset: (bank, program, name, tags) => api("PUT", "/api/presets", { bank, program, name, tags }), searchModels: (q) => api("GET", `/api/tonehub/search?q=${encodeURIComponent(q)}`), searchIrs: (q) => api("GET", `/api/tonehub/search/irs?q=${encodeURIComponent(q)}`), @@ -81,47 +81,39 @@ function getBlockColor(type) { return BLOCK_COLORS[(type || '').toLowerCase()] || T.amber; } -// ── CSS (Helix Stadium theme) ───────────────────────────────── +// ── CSS ───────────────────────────────────────────────────── const css = ` @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@300;400;500;600&display=swap'); * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; user-select: none; } - body { background: ${T.bg}; color: ${T.textPrimary}; font-family: 'Inter', sans-serif; } - + html, body { height: 100%; overflow: hidden; background: ${T.bg}; } + body { color: ${T.textPrimary}; font-family: 'Inter', sans-serif; } :root { --amber: ${T.amber}; --blue: ${T.blue}; --green: ${T.green}; } .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; } /* ── Knob ── */ - .knob-wrap { display: flex; flex-direction: column; align-items: center; gap: 6px; cursor: pointer; } + .knob-wrap { display: flex; flex-direction: column; align-items: center; gap: 4px; cursor: pointer; } .knob { border-radius: 50%; position: relative; touch-action: none; } .knob-inner { border-radius: 50%; width: 100%; height: 100%; position: relative; 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: 10px; color: ${T.textSec}; letter-spacing: .06em; text-transform: uppercase; text-align: center; } - .knob-value { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: ${T.amber}; } - - /* ── Toggle ── */ - .toggle { display: flex; background: ${T.surface}; border-radius: 6px; border: 1px solid ${T.border}; overflow: hidden; } - .toggle-btn { flex: 1; padding: 7px 12px; font-size: 12px; font-weight: 500; letter-spacing: .04em; - background: none; border: none; color: ${T.textSec}; cursor: pointer; transition: all .15s; } - .toggle-btn.active { background: ${T.amber}; color: #000; } + .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}; } /* ── Buttons ── */ - .btn { padding: 10px 18px; border-radius: 7px; font-size: 13px; font-weight: 600; + .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: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-danger { background: rgba(200,64,64,.2); color: ${T.red}; border: 1px solid rgba(200,64,64,.3); } - .btn-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; + .btn-icon { width: 32px; height: 32px; border-radius: 7px; display: flex; align-items: center; justify-content: center; background: ${T.surface}; border: 1px solid ${T.border}; cursor: pointer; - font-size: 16px; transition: all .12s; } + 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}; } @@ -132,58 +124,54 @@ const css = ` .badge-amber { background: rgba(232,160,48,.2); color: ${T.amber}; } .badge-green { background: rgba(58,184,122,.2); color: ${T.green}; } .badge-blue { background: rgba(58,123,168,.2); color: ${T.blue}; } - .badge-red { background: rgba(200,64,64,.2); color: ${T.red}; } - - /* ── Screen header (for overlay screens) ── */ - .screen-header { padding: 14px 16px 10px; display: flex; align-items: center; - justify-content: space-between; border-bottom: 1px solid ${T.border}; } - .screen-title { font-size: 13px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: ${T.textSec}; } - .screen-body { flex: 1; overflow-y: auto; padding: 14px 12px; display: flex; - flex-direction: column; gap: 14px; } /* ── Param slider ── */ - .param-panel { background: ${T.surface}; border-radius: 12px 12px 0 0; padding: 10px 14px 8px; - flex-shrink: 0; overflow-y: auto; border-top: 1px solid ${T.border}; } - .param-panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; } - .param-panel-title { font-size: 11px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; color: ${T.textSec}; } - .param-row { display: flex; flex-direction: column; gap: 2px; padding: 2px 0; } + .param-panel { background: ${T.surface}; padding: 8px 12px 6px; border-top: 1px solid ${T.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-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: 10px; font-weight: 500; text-transform: uppercase; letter-spacing: .06em; color: ${T.textSec}; } - .param-val { font-family: 'JetBrains Mono',monospace; font-size: 12px; font-weight: 600; } - .param-track-wrap { position: relative; height: 44px; display: flex; align-items: center; cursor: pointer; touch-action: none; } - .param-track { width: 100%; height: 8px; background: ${T.border}; border-radius: 4px; position: relative; overflow: hidden; } - .param-fill { height: 100%; border-radius: 4px; position: absolute; top: 0; left: 0; transition: width .04s; } - .param-thumb { width: 26px; height: 26px; border-radius: 50%; position: absolute; top: 50%; + .param-label { font-size: 9px; font-weight: 500; text-transform: uppercase; letter-spacing: .06em; color: ${T.textSec}; } + .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-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: 32px; height: 32px; border-radius: 50%; border: 1px solid ${T.border}; - background: ${T.panel}; color: ${T.textPrimary}; font-size: 15px; font-weight: 700; + .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; display: flex; align-items: center; justify-content: center; cursor: pointer; line-height: 1; } .param-btn:active { background: ${T.border}; transform: scale(.92); } - .param-knobs { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; padding: 8px 0; } + .param-knobs { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; padding: 4px 0; } - /* ── Preset chip ── */ - .preset-chip { padding: 5px 8px; border-radius: 6px; background: ${T.surface}; border: 1px solid ${T.border}; - display: flex; flex-direction: column; align-items: center; cursor: pointer; transition: all .12s; flex: 1; - min-width: 0; } - .preset-chip.active { border-color: ${T.amber}; background: rgba(232,160,48,.08); } - .preset-chip .pc-name { font-size: 10px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } - .preset-chip .pc-num { font-family: 'JetBrains Mono', monospace; font-size: 8px; color: ${T.textDim}; margin-top: 1px; } + /* ── 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}; } + .screen-body { flex: 1; overflow-y: auto; padding: 10px 12px; display: flex; flex-direction: column; gap: 10px; } - /* ── Preset row (captures screen) ── */ - .preset-row { display: flex; align-items: center; gap: 12px; padding: 11px 14px; - border-radius: 8px; cursor: pointer; transition: background .12s; border: 1px solid transparent; } + /* ── Preset sidebar chip ── */ + .preset-chip-s { width: 100%; padding: 7px 6px; border-radius: 5px; background: ${T.surface}; border: 1px solid ${T.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 { 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-indicator { width: 4px; height: 4px; border-radius: 50%; margin-top: 3px; } + + /* ── Preset row (captures) ── */ + .preset-row { display: flex; align-items: center; gap: 10px; padding: 9px 12px; + 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: 10px; } - - /* ── Card ── */ - .card { background: ${T.panel}; border: 1px solid ${T.border}; border-radius: 10px; padding: 16px; } - .card-sm { background: ${T.surface}; border: 1px solid ${T.border}; border-radius: 8px; padding: 12px; } + 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; } `; // ── Knob ────────────────────────────────────────────────────── -function Knob({ label, value = 50, onChange, size = 52, color = T.amber, min = 0, max = 100 }) { +function Knob({ label, value = 50, onChange, size = 48, color = T.amber, min = 0, max = 100 }) { const startRef = useRef(null); const norm = (value - min) / (max - min); const angle = -140 + norm * 280; @@ -200,121 +188,76 @@ function Knob({ label, value = 50, onChange, size = 52, color = T.amber, min = 0 const next = Math.max(min, Math.min(max, startRef.current.val + delta * (max - min))); onChange?.(Math.round(next)); }; - const up = () => { window.removeEventListener("mousemove", move); window.removeEventListener("mouseup", up); - window.removeEventListener("touchmove", move); window.removeEventListener("touchend", up); }; - window.addEventListener("mousemove", move); window.addEventListener("mouseup", up); - window.addEventListener("touchmove", move, { passive: false }); - window.addEventListener("touchend", up); + const up = () => { window.removeEventListener("mousemove",move);window.removeEventListener("mouseup",up); + window.removeEventListener("touchmove",move);window.removeEventListener("touchend",up); }; + window.addEventListener("mousemove",move);window.addEventListener("mouseup",up); + window.addEventListener("touchmove",move,{passive:false});window.addEventListener("touchend",up); }; - const svgSize = size + 12; - const cx = svgSize / 2, cy = svgSize / 2, r = size / 2 + 3; - const lx = cx + r * Math.cos((-140 - 90) * (Math.PI / 180)); - const ly = cy + r * Math.sin((-140 - 90) * (Math.PI / 180)); - const ex = cx + r * Math.cos((angle - 90) * (Math.PI / 180)); - const ey = cy + r * Math.sin((angle - 90) * (Math.PI / 180)); - const large = norm * 280 > 180 ? 1 : 0; + const svgSize=size+12,cx=svgSize/2,cy=svgSize/2,r=size/2+3; + const lx=cx+r*Math.cos((-140-90)*(Math.PI/180)),ly=cy+r*Math.sin((-140-90)*(Math.PI/180)); + const ex=cx+r*Math.cos((angle-90)*(Math.PI/180)),ey=cy+r*Math.sin((angle-90)*(Math.PI/180)); + const large=norm*280>180?1:0; return ( -