Files
pi-multifx-pedal-ui/docs/helix-stadium-ux-spec.md
T

398 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.*