PEDAL-UI: Helix Stadium-style redesign — block chain, big touch sliders, footswitch bar with scribble strips
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
# Helix Stadium UX Redesign — Spec & Mockup
|
||||
|
||||
> **Task:** Redesign the Pi Multi-FX Pedal UI from a mobile-first vertical tab layout to a
|
||||
> Helix Stadium-style horizontal block chain with footswitch navigation.
|
||||
> **Status:** Research / Spec Complete
|
||||
> **Date:** 2026-06-12
|
||||
|
||||
---
|
||||
|
||||
## 1. Design Philosophy
|
||||
|
||||
The Helix Stadium is designed for **stage use**: large touch targets, high contrast at
|
||||
arm's length, minimal scrolling, instant access to block placement and parameters.
|
||||
Our redesign translates this philosophy into a legally distinct UI using the existing
|
||||
amber/blue/green palette and JetBrains Mono + Inter typography.
|
||||
|
||||
### Key UX Principles
|
||||
|
||||
| Principle | Application |
|
||||
|-----------|-------------|
|
||||
| One primary view | Block chain is the main screen — IN → blocks → OUT |
|
||||
| Touch-first | 60-80px minimum tap targets, no hover-dependent controls |
|
||||
| Context-sensitive | Select a block → knob/slider panel shows its params |
|
||||
| Visual feedback | Bypass LED, bypassed opacity, active block highlight |
|
||||
| No dead space | Every pixel is a control or shows status |
|
||||
|
||||
---
|
||||
|
||||
## 2. Screen Layout (Top → Bottom)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ STATUS BAR │
|
||||
│ ● CONNECTED Preset Name CPU 12% ▂▃▅▇ │ ~36px
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ BLOCK CHAIN (horizontal scroll) │
|
||||
│ │ ~80-100px
|
||||
│ [IN] → [Comp] → [OD] → [Rev] → [OUT] │
|
||||
│ ↑ selected │
|
||||
│ ───────────────────────────────────── │
|
||||
│ ⬡ BYPASS ⬡ DRIVE ⬡ TONE ⬡ GAIN │
|
||||
│ PARAMETER KNOBS / SLIDERS │ flex
|
||||
│ │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ FOOTSWITCH BAR │
|
||||
│ [1 PlexiC] [2 ModHi] [3 Clean] [4 Soul] │ ~64-72px
|
||||
│ [5 Metal] [6 Studio] [7 Acous] [8 Twang]│
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.1 Status Bar (36px)
|
||||
|
||||
- **Left:** Green/red connection dot, "CONNECTED" / "OFFLINE" label
|
||||
- **Right:** Current preset name (if loaded), CPU %, audio signal meter
|
||||
- Background: `#141418` (panel) with `#2A2A32` bottom border
|
||||
- Same as current, but more compact
|
||||
|
||||
### 2.2 Block Chain (80-100px)
|
||||
|
||||
- Horizontal scrolling flex container with overflow-x: auto
|
||||
- Left terminators: **IN** chip (blue) — fixed, always first
|
||||
- Right terminator: **OUT** chip (blue) — fixed, always last
|
||||
- Blocks between: **amber** chips, min 48px height (target 64px)
|
||||
- Gap between blocks shown as a connector line (`→` or border-based line)
|
||||
- Scrollable via horizontal swipe or footswitch left/right buttons
|
||||
- Visual distinction between bypassed (opacity 0.4) and active blocks
|
||||
|
||||
**Block Chip Spec:**
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ ██ Compressor │ ← green LED when active, dim when bypassed
|
||||
│ ODRIVE │ ← subtype badge (small)
|
||||
│ ▾ │ ← expand indicator (tap to select)
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
#### Interaction:
|
||||
- **Tap:** Select block → parameter panel updates below
|
||||
- **Long press / drag handle:** Reorder (drag-and-drop within chain)
|
||||
- **Double tap:** Toggle bypass
|
||||
- **Swipe left on block:** Delete (with undo toast)
|
||||
|
||||
### 2.3 Parameters Panel (flex, fills remaining space)
|
||||
|
||||
When a block is selected, the panel below the chain shows its editable parameters.
|
||||
Each param renders as either a **big knob** (continuous, like drive, tone, rate) or
|
||||
a **big slider** (linear range, like volume, mix, feedback) or a **toggle** (bypass, enable).
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ PARAMETERS — Compressor │ ~24px section label
|
||||
│ │
|
||||
│ ┌──────┐ ┌──────┐ ┌──────┐ │
|
||||
│ │ ╭──╮ │ │ ╭──╮ │ │ ╭──╮ │ │
|
||||
│ │ ╰──╯ │ │ ╰──╯ │ │ ╰──╯ │ │ knobs 52-64px
|
||||
│ │ -24 │ │ 3:1 │ │ 5ms │ │
|
||||
│ │THRESH│ │RATIO │ │ATTACK│ │
|
||||
│ └──────┘ └──────┘ └──────┘ │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────┐ │
|
||||
│ │ ○ MASTER VOLUME ▓▓▓▓░░│ │ slider
|
||||
│ └───────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [○ BYPASS BLOCK] [✕ DELETE] [↻ RESET] │ action buttons
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
When no block is selected, this panel shows **master volume**, **global bypass**,
|
||||
and **active preset** info (like current RigScreen).
|
||||
|
||||
### 2.4 Footswitch Bar (64-72px)
|
||||
|
||||
- **Fixed at bottom** — never scrolls away
|
||||
- **4 footswitch buttons** shown at a time (with bank left/right pagination for 8+)
|
||||
- Each button = big rounded rect with **scribble strip** (number + short label)
|
||||
- Colors match preset/bank colors (amber, red, blue, green)
|
||||
|
||||
```
|
||||
┌──────────┬──────────┬──────────┬──────────┐
|
||||
│ 1 │ 2 │ 3 │ 4 │
|
||||
│ Plexi │ Modern │ Clean │ Southern │ ← scribble strip
|
||||
│ Crunch │ Hi-Gain │ Chimey │ Blues │
|
||||
├──────────┼──────────┼──────────┼──────────┤
|
||||
│ ◀ │ │ │ ▶│ ← bank nav
|
||||
└──────────┴──────────┴──────────┴──────────┘
|
||||
```
|
||||
|
||||
#### Footswitch Interactions:
|
||||
- **Tap:** Load that preset
|
||||
- **Long press:** Save current chain to that slot
|
||||
- **Bank nav (◀ ▶):** Switch between banks (pages of 4)
|
||||
- **Scribble strip:** Shows preset name (truncated to ~8-10 chars) rendered in amber/green
|
||||
|
||||
---
|
||||
|
||||
## 3. Component Tree
|
||||
|
||||
```
|
||||
App
|
||||
├── <style>{CSS}</style>
|
||||
├── Shell (100vh, flex column)
|
||||
│ ├── StatusBar
|
||||
│ │ ├── ConnectionDot
|
||||
│ │ ├── PresetLabel
|
||||
│ │ └── SystemStats (CPU + signal)
|
||||
│ ├── ChainPanel (flex: 1)
|
||||
│ │ ├── BlockChain (horizontal scroll)
|
||||
│ │ │ ├── ChainTerminator("IN", fixed, blue)
|
||||
│ │ │ ├── BlockChip × N (draggable, amber)
|
||||
│ │ │ │ ├── BypassLED
|
||||
│ │ │ │ ├── BlockName
|
||||
│ │ │ │ ├── TypeBadge
|
||||
│ │ │ │ └── DragHandle
|
||||
│ │ │ ├── ChainConnector × N-1 (→ line)
|
||||
│ │ │ └── ChainTerminator("OUT", fixed, blue)
|
||||
│ │ └── ParamsPanel
|
||||
│ │ ├── (if block selected) KnobGrid
|
||||
│ │ │ └── BigKnob × N
|
||||
│ │ ├── (if block selected) SliderList
|
||||
│ │ │ └── BigSlider × N
|
||||
│ │ ├── (if block selected) BlockActions
|
||||
│ │ │ └── BypassBtn / DeleteBtn / ResetBtn
|
||||
│ │ └── (if none selected) MasterVolume + PresetInfo
|
||||
│ │ ├── VolumeSlider (existing)
|
||||
│ │ └── PresetSummary
|
||||
│ └── FootswitchBar (fixed 64-72px)
|
||||
│ ├── BankLeftBtn (◀)
|
||||
│ ├── FootswitchBtn × 4
|
||||
│ │ └── ScribbleStrip (number + label)
|
||||
│ └── BankRightBtn (▶)
|
||||
```
|
||||
|
||||
### State Shape (React)
|
||||
|
||||
```js
|
||||
const [selectedBlock, setSelectedBlock] = useState(null); // block id or null
|
||||
const [blocks, setBlocks] = useState([]); // current block chain
|
||||
const [activePreset, setActivePreset] = useState(null);
|
||||
const [footswitchBank, setFootswitchBank] = useState(0); // 0-based
|
||||
const [params, setParams] = useState({}); // fetched from API when block selected
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Color & Typography (Existing Design Tokens)
|
||||
|
||||
Use the existing `T` object from App.jsx unchanged. No new colors needed —
|
||||
the amber/blue/green palette maps directly to Helix-style needs:
|
||||
|
||||
| Token | Hex | Usage |
|
||||
|-------|-----|-------|
|
||||
| `T.amber` | `#E8A030` | Active block, selected state, knobs, scribble strip text |
|
||||
| `T.amberDim` | `#7A5218` | Block chip background |
|
||||
| `T.blue` | `#3A7BA8` | IN/OUT terminators, connection dots |
|
||||
| `T.blueDim` | `#1E4060` | Terminator backgrounds |
|
||||
| `T.green` | `#3AB87A` | Bypass LED on, signal OK |
|
||||
| `T.bg` | `#0A0A0C` | Page background |
|
||||
| `T.panel` | `#141418` | Card/section backgrounds |
|
||||
| `T.surface` | `#1C1C22` | Input fields, button surfaces |
|
||||
| `T.border` | `#2A2A32` | Lines, dividers |
|
||||
|
||||
Typography unchanged: **Inter** for body/UI text, **JetBrains Mono** for values/labels.
|
||||
|
||||
---
|
||||
|
||||
## 5. API Integration
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
Mount → GET /api/state
|
||||
├── connected, master_volume, bypass
|
||||
└── current_preset (name, bank, program)
|
||||
|
||||
On mount of preset: GET /api/presets/{bank}/{program}
|
||||
└── chain[].blocks → populates BlockChain
|
||||
|
||||
On block select: GET /api/block-params/{fx_type}
|
||||
└── params[] → renders knobs/sliders
|
||||
|
||||
Param change: PATCH /api/block-params { id, key, value }
|
||||
Toggle bypass: PATCH /api/blocks { id, enabled }
|
||||
Master volume: POST /api/volume { volume }
|
||||
Global bypass: POST /api/bypass/toggle
|
||||
Load preset: POST /api/presets/{bank}/{program}/activate
|
||||
Save preset: PUT /api/presets/{bank}/{program}
|
||||
```
|
||||
|
||||
### Backend Gap: Blocks in `/api/state`
|
||||
|
||||
The current `_gather_state()` does NOT return `blocks[]` — but the UI needs the
|
||||
current block chain to render. Options ranked by preference:
|
||||
|
||||
1. **Extend `/api/state` to include `blocks`** — Add the current preset's chain
|
||||
to the state response. Most efficient: single fetch on mount + WebSocket push.
|
||||
Requires adding ~5 lines to `_gather_state()`.
|
||||
2. **Load preset chain separately** — On mount, fetch `GET /api/presets/{bank}/{program}`
|
||||
to get the chain. Needs bank/program from state. Works without backend change
|
||||
but adds an extra round trip.
|
||||
3. **WebSocket-first** — `/ws` already pushes state on connect. Extend the push
|
||||
to include blocks.
|
||||
|
||||
**Recommendation:** Option 1 (backend change) — it's trivial and eliminates an
|
||||
extra HTTP round trip on every page load.
|
||||
|
||||
---
|
||||
|
||||
## 6. Drag-and-Drop Reordering
|
||||
|
||||
For reordering blocks in the chain, use the **HTML5 Drag and Drop API**
|
||||
(native, no library needed in modern React):
|
||||
|
||||
```jsx
|
||||
// BlockChip
|
||||
<div
|
||||
draggable
|
||||
onDragStart={(e) => e.dataTransfer.setData('text/plain', blockId)}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const draggedId = e.dataTransfer.getData('text/plain');
|
||||
reorderBlocks(draggedId, blockId); // swap or insert
|
||||
}}
|
||||
>
|
||||
```
|
||||
|
||||
After reorder: `PATCH /api/blocks/reorder { ids: [new order] }` — a new endpoint
|
||||
to persist the sequence. For MVP, keep the reorder local-only (state-only) and
|
||||
save with the preset.
|
||||
|
||||
---
|
||||
|
||||
## 7. Responsive Behavior
|
||||
|
||||
| Viewport | Layout |
|
||||
|----------|--------|
|
||||
| < 480px (phone) | Single column, chain scrolls horizontally, param panel below, footswitch at bottom |
|
||||
| 480-768px (tablet) | Same layout, more knobs visible per row |
|
||||
| > 768px (desktop) | Optional: chain + params side-by-side. Footswitch always at bottom. |
|
||||
|
||||
The current app is capped at `maxWidth: 440px` — Stage UI should use **full width**
|
||||
on desktop (for the pedal's browser UI) while remaining touch-friendly on tablets.
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementation Phases
|
||||
|
||||
### Phase 1: Block Chain View
|
||||
- [ ] Extract block rendering into `ChainPanel` + `BlockChain` + `BlockChip` components
|
||||
- [ ] Build horizontal scroll layout with IN/OUT terminators
|
||||
- [ ] Wire block select → highlight state
|
||||
- [ ] Bypass toggle on double-tap
|
||||
- [ ] Fetch and display blocks from `/api/state` (requires backend change #1)
|
||||
|
||||
### Phase 2: Parameters Panel
|
||||
- [ ] Build `ParamsPanel` component with conditional rendering
|
||||
- [ ] Adapt existing `Knob` component for bigger stage-friendly size
|
||||
- [ ] Add `BigSlider` component for linear params
|
||||
- [ ] Wire param change → `PATCH /api/block-params`
|
||||
- [ ] Show master volume / preset info when no block selected
|
||||
|
||||
### Phase 3: Footswitch Bar
|
||||
- [ ] Build `FootswitchBar` component
|
||||
- [ ] Scribble strip rendering (number + truncated name)
|
||||
- [ ] Bank pagination (◀ ▶)
|
||||
- [ ] Tap → load preset via API
|
||||
- [ ] Long press → save current chain as preset
|
||||
|
||||
### Phase 4: Drag-and-Drop Reorder
|
||||
- [ ] Implement HTML5 DnD on `BlockChip`
|
||||
- [ ] Reorder state on drop
|
||||
- [ ] Persist to API
|
||||
|
||||
### Phase 5: Polish & Legal Distinctiveness
|
||||
- [ ] Custom block icon SVGs per FX type
|
||||
- [ ] Scribble strip font rendering optimization
|
||||
- [ ] Touch sensitivity tuning
|
||||
- [ ] Build to `dist/` and verify backend serves at `/`
|
||||
|
||||
---
|
||||
|
||||
## 9. Size & Layout Constants
|
||||
|
||||
```css
|
||||
:root {
|
||||
--block-height: 64px;
|
||||
--footswitch-height: 68px;
|
||||
--statusbar-height: 36px;
|
||||
--knob-size: 56px;
|
||||
--slider-height: 36px;
|
||||
--min-tap-target: 48px;
|
||||
--border-radius-block: 8px;
|
||||
--border-radius-footswitch: 6px;
|
||||
--chain-gap: 8px;
|
||||
--connector-width: 16px;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Existing Components to Reuse
|
||||
|
||||
| Component | Status | Changes Needed |
|
||||
|-----------|--------|----------------|
|
||||
| `Knob` | Already exists (SVG arc, touch drag) | Increase default size to 56px, adjustable sizing |
|
||||
| `VUMeter` | Already exists (vertical + horizontal) | None — fits in status bar |
|
||||
| `FXBlock` | Currently vertical card | Rewrite as horizontal `BlockChip` |
|
||||
| `T` design tokens | Already defined | Use unchanged |
|
||||
| CSS styles | Currently in tagged template | Extract into separate `.module.css` or keep inline |
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Backend Changes Required
|
||||
|
||||
### 1. Add `blocks` to `/api/state`
|
||||
|
||||
In `_gather_state()`, add after line 1163:
|
||||
|
||||
```python
|
||||
# Current block chain from active preset
|
||||
if preset:
|
||||
state["blocks"] = [
|
||||
{
|
||||
"id": f"block_{i}",
|
||||
"fx_type": b.fx_type.value,
|
||||
"name": b.fx_type.value.replace("_", " ").title(),
|
||||
"enabled": b.enabled,
|
||||
"bypass": b.bypass,
|
||||
"params": dict(b.params),
|
||||
"nam_model_path": b.nam_model_path,
|
||||
"ir_file_path": b.ir_file_path,
|
||||
}
|
||||
for i, b in enumerate(preset.chain)
|
||||
]
|
||||
else:
|
||||
state["blocks"] = []
|
||||
```
|
||||
|
||||
### 2. (Optional) `/api/blocks/reorder` endpoint
|
||||
|
||||
For persistable drag-and-drop ordering:
|
||||
|
||||
```python
|
||||
@app.put("/api/blocks/reorder")
|
||||
async def reorder_blocks(data: dict):
|
||||
"""Reorder blocks in the current preset chain."""
|
||||
new_ids = data.get("ids", [])
|
||||
# Map old block order to new order based on block ids
|
||||
pm = self.deps.presets
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*End of spec. Ready for handoff to implementation.*
|
||||
Reference in New Issue
Block a user