// GlanceRail — ALL loop stations expanded, always (task D-41, per // docs/design/integrated-glance.md §4; supersedes the L-33 playhead accordion). // // D-51 (docs/design/one-screen.md §4/§6.2) adapts the rail to the dashboard's // 500px right column. Margin-hardening arithmetic, worst case = a classic // Windows scrollbar (~17px) inside the column's overflow-y-auto wrapper: // row interior = 500 − 17 (scrollbar) − 18 (section border + p-2) // − 14 (row border + p-1.5) = 451px // guitar cell 89 (75 SVG + p-1.5 + border) → 4/line (374 ≤ 451; a 5th = 469 ✗) // piano 1-octave 156.4 / 2-octave 279.6 → the mixed pair 279.6+6+156.4 = 442 // fits with ~9px margin even under the scrollbar (26px without) — the fit the // doc calls fragile at the old paddings is now robust. Without the scrollbar // the interior is 468px; every count above is unchanged. // // One VERTICAL ROW per loop station, canonical KB order (the same order the // banner's loop shows after rotation). Every row renders its full voicing // gallery PERMANENTLY — the playhead HIGHLIGHTS the active row (accent ring + // "now" badge + aria-current) and never hides, collapses, or reveals content. // User directive 2026-07-10: "i'd like to see all the chords and their // voicings … so you can follow and potentially learn new ways to play it while // you are playing the loop. scrolling is easier then clicking." // // Row anatomy (D-40 §4): // header — chord label + rn (the focus toggle) · "now" badge / "next" tag · // solo-scale label · aim dots (3rd filled accent, 7th hollow — // RoadmapTrack's GuideDot language, honest "5th" fallback kept) · // transition chip ("next F→E · ½ step down"; the last row wraps: // "loop"). This is where the retired RoadmapTrack's education // folds in (D-40 §2) — theory.js `guideTones` / `voiceLeadingPairs` // / `soloScale`, read-only imports. // gallery — the full VoicingBrowser (show={instrument}, dense) with the // station's OWN voicing passed as `recommended` so it renders as the // badged, accent-bordered first cell INSIDE the browser (no separate // own-cell, no dupe — dashboard-polish.md §1.1/§2.1). Guitar caps at // 4 recommended-first shapes on one line (§1); piano is a 2×2 of // `size="mini"` keyboards (§2). No ▶ anywhere (§3). // // Focus semantics (D-40 §4 — the pin, simplified): with everything always // expanded there is nothing left to hold open, so tapping a row header TOGGLES // that station as focused. The PARENT owns the state and the onFocusChord // emission (the D-03 fretboard guide-tone contract, byte-compatible); a focused // row shows an "aim on fretboard" chip; tap again (or the loop changes) to // clear. This component never emits focus-chord itself and never triggers audio // — the ▶ previews were removed everywhere (dashboard-polish.md §3). // // NO auto-scroll (D-40 §4/§6.2 step 1): the band lives in page flow, where // scrollIntoView's nearest scroller is the DOCUMENT — it would yank the whole // page mid-jam. The L-33 auto-centre effect was deleted in L-40 and must never // return; the highlight travels, the user owns the scrollbar. // // Pure presentational. Props: // stations — [{ shape, voicing, rootPc, quality, label, rn }] canonical order // activeIndex — playhead station (canonicalPos); -1 = loop known, playhead // not — no row is marked "now" (content never changes either way) // focusedIndex — the focused station index, or null (nothing focused) // onFocus — fn(index|null): toggle a station's focus // instrument — 'guitar' | 'piano' (VoicingBrowser `show`; bass never mounts // this rail — JamGuide renders BassGuideRows instead, D-40 §3) // keyRoot — key tonic pitch class 0–11 (ChordDiagram fret placement) // keyMode — key mode name (soloScale's minor-key dominant nudge) import { NOTES, guideTones, voiceLeadingPairs, soloScale } from '../lib/theory' import VoicingBrowser from './VoicingBrowser' const pcName = (pc) => NOTES[((pc % 12) + 12) % 12] // ─── Header atoms (exported — BassGuideRows in JamGuide.jsx composes the same // anatomy for visual parity across instruments, D-40 §3) ────────────────── // Solo-scale label: "solo · G mixolydian" (theory.js snake_case → spaces). export function SoloLabel({ rootPc, quality, keyMode }) { const { name } = soloScale(quality, keyMode) return ( solo · {pcName(rootPc)} {name.replace(/_/g, ' ')} ) } // One guide-tone dot: note name in a small circle + its honest kind label. // Filled accent = the 3rd; hollow = the 7th (or the "5th" fallback — never // badge a 5th as a 7th). Filled text is BLACK on accent (#a855f7 vs black // ≈5.3:1 — AA; white would be ~4.0), matching VoicingBrowser's ▶ hover. function GuideDot({ pc, kind, filled }) { return ( {pcName(pc)} {kind} ) } // The "aim" pair — the RoadmapTrack TARGET lane, folded to one header line. export function AimDots({ rootPc, quality }) { const g = guideTones(rootPc, quality) const seventhKind = g.hasSeventh ? '7th' : '5th' return ( aim ) } // The voice-leading rail, folded to a compact chip: "next F→E · ½ step down"; // a held common tone reads "B holds · common tone"; the last row's chip is the // wrap-around and says "loop" (D-40 §2). No smooth rail (≤2 semitones) → the // caller passes null and no chip renders. function TransitionChip({ pair, wraps }) { if (!pair) return null const held = pair.semitones === 0 const label = held ? `${pcName(pair.from)} holds` : `${pcName(pair.from)}→${pcName(pair.to)}` const motion = held ? 'common tone' : `${Math.abs(pair.semitones) === 1 ? '½' : Math.abs(pair.semitones)} step ${pair.semitones < 0 ? 'down' : 'up'}` return ( {wraps ? 'loop' : 'next'} {label} · {motion} ) } // ─── One always-expanded station row ───────────────────────────────────────── function StationRow({ st, isNow, isNext, isFocused, onToggleFocus, instrument, keyRoot, keyMode, rail, wraps, }) { // The station's own voicing — passed to VoicingBrowser as `recommended` so it // renders as the badged, accent-bordered first cell inside the gallery (guitar: // a shape; piano: a voicing matched by style). The separate own-cell is gone // (dashboard-polish.md §1.1/§2.1 — it centralises the ≤4 rule and kills the old // recommended/gallery duplicate). const recommended = instrument === 'guitar' ? (st.shape ?? null) : (st.voicing ?? null) // Active row: unmistakable (accent ring + tint). Focused-but-not-now rows get // the softer accent border; everything else recedes to the 0.85 opacity floor // (never below AA legibility). const stateClass = isNow ? 'border-accent bg-accent/10 ring-2 ring-accent' : isFocused ? 'border-accent/60 bg-accent/5' : 'border-border bg-surface' return (
{/* ── Header line: identity + the folded roadmap education ── */}
{isNow && ( now )} {isNext && ( next )} {isFocused && ( aim on fretboard )}
{/* ── Gallery (dashboard-polish.md §1/§2): the recommended voicing is now cell #1 INSIDE the browser (badged "play", accent border — "the answer" prominence the old own-cell had, no dupe). Guitar caps at 4 recommended-first cells on one line; piano is a 2×2 of mini keyboards. Full row interior, no separate own-cell. ── */}
) } // ─── The rail ───────────────────────────────────────────────────────────────── export default function GlanceRail({ stations = [], activeIndex = -1, focusedIndex = null, onFocus, instrument, keyRoot, keyMode, }) { const n = stations.length if (n === 0) return null const nextIndex = activeIndex >= 0 && n > 1 ? (activeIndex + 1) % n : -1 // Voice-leading rails: rail i leaves station i for station (i+1) mod n — the // last rail wraps back to station 0 (the loop is a wheel). The headline rail // is the 7→3 (voiceLeadingPairs lists the 7th first); a one-chord loop has // no transition to speak of. const rails = stations.map((st, i) => { if (n < 2) return null const next = stations[(i + 1) % n] return voiceLeadingPairs( { root: st.rootPc, quality: st.quality }, { root: next.rootPc, quality: next.quality }, )[0] ?? null }) return (

Variations · every chord, every voicing — the playhead highlights

{stations.map((st, i) => ( onFocus?.(focusedIndex === i ? null : i)} instrument={instrument} keyRoot={keyRoot} keyMode={keyMode} rail={rails[i]} wraps={i === n - 1} /> ))}
{/* No ▶ anywhere anymore (dashboard-polish.md §3 — "leave them off, better not"); the rail is purely visual and follows the loop. */}

Voicings follow the loop — the playhead highlights the chord you're on.

) }