// 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
Voicings follow the loop — the playhead highlights the chord you're on.