feat: pipedal SVG icons in block tiles + nav bar

- Replace emoji getBlockIcon() with <BlockIcon> component rendering SVG icons
- 38 fx_*.svg icons from pipedal mapped to block types (distortion, delay, reverb, mod, etc.)
- Split/merge tiles now use fx_split_a.svg / fx_mixer.svg
- Status bar: ic_bank.svg, ic_presets.svg, fx_analyzer.svg, ic_drawer_2.svg
- Extracted reusable CSS design system to index.css (badge, snap-grid, slide-panel, etc.)
- SnapshotPanel and component reference extracted to docs/
- 270KB JS build, SVG icons served from /ui/img/
This commit is contained in:
2026-06-12 19:29:50 -04:00
parent bd6771e203
commit 62c373b989
5 changed files with 951 additions and 38 deletions
+14 -7
View File
@@ -638,8 +638,12 @@ export default function App(){
<span className="mono" style={{fontSize:9,color:T.textDim}}>CPU {state.cpu_percent!=null?`${state.cpu_percent}%`:"—"}</span>
</div>
<div style={{width:1,height:12,background:T.border}}/>
<button className="btn-icon" style={{width:24,height:24,fontSize:10}}onClick={()=>setView("captures")}title="Downloads">📁</button>
<button className="btn-icon" style={{width:24,height:24,fontSize:10}}onClick={()=>setView("presets")}title="Presets"></button>
<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:'brightness(0.7)'}}/>
</button>
<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:'brightness(0.7)'}}/>
</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,
@@ -649,12 +653,15 @@ export default function App(){
cursor:"pointer",lineHeight:1.5,minWidth:34,display:"flex",alignItems:"center",justifyContent:"center",
}}>{m==="stomp"?"SM":"PR"}</button>))}
</div>
<button className="btn-icon" style={{width:24,height:24,fontSize:10,
<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,
color:state.tuner_enabled?T.green:T.textPrimary}}
onClick={handleTunerToggle} title={state.tuner_enabled?"Exit Tuner":"Tuner"}></button>
<button className="btn-icon" style={{width:24,height:24,fontSize:10,background:'rgba(232,160,48,.12)',borderColor:T.amber,color:T.amber}}onClick={()=>{if(!document.fullscreenElement){document.documentElement.requestFullscreen()}else{document.exitFullscreen()}}}title="Fullscreen"></button>
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(1.5) sepia(0.3) hue-rotate(80deg)':'brightness(0.7)'}}/>
</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">
<img src={`${import.meta.env.BASE_URL}img/ic_drawer_2.svg`} alt="fullscreen" style={{width:12,height:12,filter:'brightness(1.2)'}}/>
</button>
</div>
</div>
+67 -27
View File
@@ -40,6 +40,70 @@ const TYPE_COLORS = {
split: "#B080C0", merge: "#80B0C0",
};
// ── Type-to-SVG-icon mapping (from pipedal icon set) ──
const TYPE_ICON_MAP = {
od: "fx_distortion", overdrive: "fx_distortion", drive: "fx_distortion",
dist: "fx_distortion", distortion: "fx_distortion", gain: "fx_distortion",
fuzz: "fx_distortion",
delay: "fx_delay", echo: "fx_delay",
reverb: "fx_reverb",
chorus: "fx_chorus", flange: "fx_flanger", flanger: "fx_flanger",
phaser: "fx_phaser", tremolo: "fx_modulator", vibrato: "fx_modulator",
rotary: "fx_modulator", modulator: "fx_modulator",
pitch: "fx_pitch", harmonizer: "fx_pitch", shifter: "fx_pitch",
comp: "fx_compressor", compressor: "fx_compressor",
limiter: "fx_limiter",
gate: "fx_gate", noise_gate: "fx_gate",
filter: "fx_filter", wah: "fx_filter",
eq: "fx_eq", graphic_eq: "fx_eq",
parametric_eq: "fx_parametric_eq", multiband_eq: "fx_multiband_eq",
ir: "fx_spatial", cab: "fx_spatial", cabinet: "fx_spatial",
nam: "fx_simulator", capture: "fx_simulator", amp: "fx_amplifier",
amplifier: "fx_amplifier",
volume: "fx_constant", level: "fx_constant", boost: "fx_constant",
split: "fx_split_a", merge: "fx_mixer",
tuner: "fx_analyzer",
mixer: "fx_mixer", utility: "fx_utility",
plugin: "fx_plugin",
instrument: "fx_instrument",
reamp: "fx_converter",
looper: "fx_oscillator",
};
const IMG_PATH = (import.meta.env.BASE_URL || '/ui/') + 'img/';
// ── SVG Block Icon component ─────────────────────────
function BlockIcon({ type, size = 14, style = {} }) {
const key = (type || '').toLowerCase().trim();
let icon = 'fx_plugin';
let found = false;
for (const [k, v] of Object.entries(TYPE_ICON_MAP)) {
if (key === k || key.startsWith(k) || key.includes(k)) {
icon = v;
found = true;
break;
}
}
if (!found && key) {
// Try direct match by replacing spaces/special chars
const slug = key.replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
if (TYPE_ICON_MAP[slug]) icon = TYPE_ICON_MAP[slug];
}
return (
<img
src={`${IMG_PATH}${icon}.svg`}
alt={type || 'fx'}
style={{
width: size, height: size, flexShrink: 0,
filter: 'brightness(1.2) contrast(1.1)',
objectFit: 'contain',
...style,
}}
onError={(e) => { e.target.style.display = 'none'; }}
/>
);
}
function getBlockColor(type) {
const key = (type || "").toLowerCase().trim();
for (const [k, v] of Object.entries(TYPE_COLORS)) {
@@ -48,30 +112,6 @@ function getBlockColor(type) {
return T.amber;
}
function getBlockIcon(type) {
const key = (type || "").toLowerCase();
if (key === "split") return "⇄";
if (key === "merge") return "⨁";
if (key.includes("dist") || key.includes("gain")) return "💥";
if (key.includes("fuzz")) return "⚡";
if (key.includes("delay") || key.includes("echo")) return "⏳";
if (key.includes("reverb")) return "🌊";
if (key.includes("pitch") || key.includes("harm") || key.includes("shift")) return "🎵";
if (key.includes("comp") || key.includes("limit")) return "📊";
if (key.includes("gate") || key.includes("noise")) return "🔇";
if (key.includes("filter") || key.includes("wah")) return "📈";
if (key.includes("eq")) return "⚖️";
if (odMatch(key)) return "🔥";
if (modMatch(key)) return "🌀";
if (key.includes("ir") || key.includes("cab")) return "🔊";
if (key.includes("nam") || key.includes("capture")) return "🎛";
if (key.includes("volume") || key.includes("level") || key.includes("boost")) return "🔈";
if (key.includes("tuner")) return "🎹";
return "▣";
function odMatch(k) { return k.includes("od")||k.includes("overdrive")||k.includes("drive")||k.includes("boost")&&!k.includes("volume"); }
function modMatch(k) { return k.includes("mod")||k.includes("chorus")||k.includes("flange")||k.includes("phaser")||k.includes("trem")||k.includes("vibr")||k.includes("rotary"); }
}
// ── Chain Arrow SVG ──────────────────────────────
function ChainArrow({ color = T.border, narrow = false }) {
const w = narrow ? 14 : 18;
@@ -166,7 +206,7 @@ function SplitBlockTile({ block, selected, onSelect, onToggle }) {
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
lineHeight: 1.3, display: "flex", alignItems: "center", gap: 6,
}}>
<span style={{ fontSize: 14 }}></span>
<BlockIcon type="split" size={16} />
<span>SPLIT {st.label}</span>
</div>
<div style={{ fontSize: 9, color: T.textSec, marginTop: 2 }}>
@@ -234,7 +274,7 @@ function MergeBlockTile({ block, selected, onSelect, onToggle }) {
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
lineHeight: 1.3, display: "flex", alignItems: "center", gap: 6,
}}>
<span style={{ fontSize: 14 }}></span>
<BlockIcon type="merge" size={16} />
<span>MERGE</span>
</div>
<div style={{ fontSize: 9, color: T.textSec, marginTop: 2 }}>
@@ -332,7 +372,7 @@ function BlockTile({ block, selected, onSelect, onToggle, pathLabel }) {
{block.name || block.type || "FX"}
</div>
<div style={{ display: "flex", alignItems: "center", gap: 5, marginTop: 3 }}>
<span style={{ fontSize: 12, lineHeight: 1 }}>{getBlockIcon(block.type)}</span>
<BlockIcon type={block.type} size={14} />
<span style={{
fontSize: 9, fontWeight: 700, color, letterSpacing: ".08em",
textTransform: "uppercase", background: `${color}18`,
+121 -4
View File
@@ -1,4 +1,121 @@
/**
* Styles are embedded in App.jsx via CSS-in-JS template literal.
* This file intentionally left empty — remove default Vite styles.
*/
/* ── Reset & base ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body, #root { height: 100%; overflow: hidden; }
body { background: #0A0A0C; color: #F0EDE6; font-family: 'Inter', sans-serif; -webkit-font-smoothing: antialiased; }
/* ── Utility classes (from pipedal) ── */
.mono { font-family: 'JetBrains Mono', monospace; }
/* Badge system */
.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: #E8A030; }
.badge-green { background: rgba(58,184,122,.2); color: #3AB87A; }
.badge-blue { background: rgba(58,123,168,.2); color: #3A7BA8; }
.badge-red { background: rgba(200,64,64,.2); color: #C84040; }
/* Button system */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px;
padding: 8px 16px; border-radius: 6px; font-size: 12px; font-weight: 600;
cursor: pointer; border: 1px solid #2A2A32; background: #1C1C22; color: #F0EDE6;
transition: all .12s; touch-action: manipulation; }
.btn:active { transform: scale(.96); }
.btn-sm { padding: 5px 10px; font-size: 10px; }
.btn-ghost { background: transparent; border-color: transparent; }
.btn-danger { border-color: #C84040; background: rgba(200,64,64,.15); color: #C84040; }
.btn-icon { display: inline-flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: 5px; border: 1px solid #2A2A32;
background: #1C1C22; cursor: pointer; transition: all .12s; touch-action: manipulation; }
.btn-icon:active { transform: scale(.92); }
/* Slide panel (bottom drawer) */
.slide-panel { position: fixed; inset: 0; background: rgba(0,0,0,.7); z-index: 100;
display: flex; align-items: flex-end; justify-content: center; }
.slide-panel-inner { background: #0A0A0C; max-width: 440px; width: 100%; max-height: 85vh;
border-radius: 16px 16px 0 0; overflow: hidden; display: flex; flex-direction: column; }
.slide-panel-handle { width: 36px; height: 4px; border-radius: 2px; background: #2A2A32;
margin: 8px auto; flex-shrink: 0; }
/* Screen layout */
.screen-header { padding: 12px 14px 8px; display: flex; align-items: center;
justify-content: space-between; border-bottom: 1px solid #2A2A32; flex-shrink: 0; }
.screen-title { font-size: 13px; font-weight: 600; letter-spacing: .06em;
text-transform: uppercase; color: #8888A0; }
.screen-body { flex: 1; overflow-y: auto; padding: 12px; display: flex;
flex-direction: column; gap: 12px; }
/* Snapshot grid */
.snap-grid { display: flex; gap: 6px; overflow-x: auto; padding: 4px 0; }
.snap-slot { min-width: 60px; flex-shrink: 0; border-radius: 8px;
border: 1.5px solid #2A2A32; background: #1C1C22; cursor: pointer;
text-align: center; padding: 8px 4px; transition: all .12s;
touch-action: manipulation; position: relative; }
.snap-slot:active { transform: scale(.95); }
.snap-num { font-size: 16px; font-weight: 700; color: #8888A0; }
.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; }
.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; }
/* Preset rows */
.preset-row { display: flex; align-items: center; gap: 12px; padding: 10px 12px;
border-radius: 8px; 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); }
/* Transport buttons (looper) */
.transport-btn { width: 44px; height: 44px; border-radius: 50%; display: flex;
align-items: center; justify-content: center; font-size: 16px; cursor: pointer;
border: 2px solid #2A2A32; background: #1C1C22; transition: all .12s; }
.transport-btn:active { transform: scale(.92); }
.transport-btn.rec { border-color: #C84040; background: rgba(200,64,64,.15); }
.transport-btn.play { border-color: #3AB87A; background: rgba(58,184,122,.15); }
/* Switch toggle */
.switch { width: 40px; height: 22px; border-radius: 11px; background: #2A2A32;
cursor: pointer; position: relative; transition: background .2s; flex-shrink: 0; }
.switch.on { background: #3AB87A; }
.switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 18px;
height: 18px; border-radius: 50%; background: white; transition: transform .15s; }
.switch.on::after { transform: translateX(18px); }
/* Search bar */
.search-bar { display: flex; background: #1C1C22; border-radius: 7px;
border: 1px solid #2A2A32; padding: 8px 12px; gap: 8px; align-items: center; }
.search-bar input { background: none; border: none; outline: none; color: #F0EDE6;
font-size: 13px; flex: 1; }
.search-bar input::placeholder { color: #444458; }
/* File drop zone */
.file-drop { border: 2px dashed #2A2A32; border-radius: 8px; padding: 20px;
text-align: center; cursor: pointer; transition: all .15s; font-size: 13px; color: #444458; }
.file-drop:hover, .file-drop.dragover { border-color: #E8A030; color: #E8A030; }
/* Loader */
.loader { width: 20px; height: 20px; border: 2px solid #2A2A32;
border-top-color: #E8A030; border-radius: 50%; animation: spin .6s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Settings list */
.setting-row { display: flex; align-items: center; justify-content: space-between;
padding: 10px 0; border-bottom: 1px solid #2A2A32; }
.setting-label { font-size: 13px; color: #F0EDE6; }
.setting-desc { font-size: 11px; color: #444458; margin-top: 2px; }
/* Section label */
.section-label { font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
color: #444458; font-weight: 600; margin-bottom: 10px; }
/* NAM chip */
.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);
border-radius: 5px; font-family: 'JetBrains Mono', monospace; font-size: 11px; color: #3A7BA8; }
/* 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-off { background: #444458; }