Compare commits
2 Commits
7561566ceb
...
8ebdc73958
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ebdc73958 | |||
| 9415daf4a0 |
@@ -84,7 +84,7 @@ Standing principles (memory): scroll > click; nothing duplicated; one global ins
|
|||||||
| L-75 | Implement TryThis side-by-side + per-sub instrument diagrams per D-75 (drops the rotation; follows the loop/current chord; instrument-following diagram) | engineering | done `957ce88` (combined-gate PASS) | D-75 | `src/components/TryThis.jsx`, `src/App.jsx` (pass `instrument` to the mount — grep-clean) | up-to-4 subs side by side each with a playable-shape diagram in the current instrument; tap→modal; honest empties; build+smoke green |
|
| L-75 | Implement TryThis side-by-side + per-sub instrument diagrams per D-75 (drops the rotation; follows the loop/current chord; instrument-following diagram) | engineering | done `957ce88` (combined-gate PASS) | D-75 | `src/components/TryThis.jsx`, `src/App.jsx` (pass `instrument` to the mount — grep-clean) | up-to-4 subs side by side each with a playable-shape diagram in the current instrument; tap→modal; honest empties; build+smoke green |
|
||||||
| L-76 | Implement RelatedProgressions 2×2 grid per D-75 (same-style + cross-style sections in a 2-col grid using the left-column width) | engineering | done `e695716` (combined-gate PASS) | D-75 | `src/components/RelatedProgressions.jsx` | 2×2 layout; both sections adapt; no logic/scoring change; build+smoke green |
|
| L-76 | Implement RelatedProgressions 2×2 grid per D-75 (same-style + cross-style sections in a 2-col grid using the left-column width) | engineering | done `e695716` (combined-gate PASS) | D-75 | `src/components/RelatedProgressions.jsx` | 2×2 layout; both sections adapt; no logic/scoring change; build+smoke green |
|
||||||
| D-76 | **Related-area v2** (user 2026-07-13, "one last change"): (a) VOICINGS RAIL — currently shows all loop stations when a loop matches, but only the SINGLE current chord in the heard-live/no-loop fallback (JamGuide railContent :373-424). User wants: ALWAYS show multiple chords' voicings, MOST RECENT FIRST — loop chords when matched (KEEP canonical order + a moving "now" playhead highlight — design call, reordering would break GlanceRail's between-adjacent voice-leading chips AND reshuffle rows every chord, contradicting the user's anti-jump preference; most-recent-first applies to the NO-LOOP history rail), else the recent distinct chord history (from chordHistory, cap ~6, most-recent-first). Never just one. **User confirm pending on the loop-ordering deviation (asked 2026-07-13).** (b) TRY THIS — cap at **3** suggestions (was 4); **STABLE fixed layout** (reserve 3 slots so 2-vs-3 subs never shifts position — the user: "annoying when the layout changes then u dont know where to look"); each suggestion shows **≥3 ways to play it** — guitar = **3×3** (3 subs × up to 3 getGuitarVoicings shapes each), piano = **1** MiniPiano each ("for piano it can be just one thats okay"), bass = root caption. Honest space math (left col ~744px for the 3×3 guitar; rail column for the multi-chord history). No user gate: pick strongest, ≥2 rejected alts | design | ready | — | `docs/design/related-area-v2.md` | rail always-multi-chord-most-recent-first spec'd; try-this 3-cap + stable-3-slot + 3-shapes-guitar/1-piano with honest math; L-77/L-78 bounded |
|
| D-76 | **Related-area v2** (user 2026-07-13, "one last change"): (a) VOICINGS RAIL — currently shows all loop stations when a loop matches, but only the SINGLE current chord in the heard-live/no-loop fallback (JamGuide railContent :373-424). User wants: ALWAYS show multiple chords' voicings, MOST RECENT FIRST — loop chords when matched (KEEP canonical order + a moving "now" playhead highlight — design call, reordering would break GlanceRail's between-adjacent voice-leading chips AND reshuffle rows every chord, contradicting the user's anti-jump preference; most-recent-first applies to the NO-LOOP history rail), else the recent distinct chord history (from chordHistory, cap ~6, most-recent-first). Never just one. **User confirm pending on the loop-ordering deviation (asked 2026-07-13).** (b) TRY THIS — cap at **3** suggestions (was 4); **STABLE fixed layout** (reserve 3 slots so 2-vs-3 subs never shifts position — the user: "annoying when the layout changes then u dont know where to look"); each suggestion shows **≥3 ways to play it** — guitar = **3×3** (3 subs × up to 3 getGuitarVoicings shapes each), piano = **1** MiniPiano each ("for piano it can be just one thats okay"), bass = root caption. Honest space math (left col ~744px for the 3×3 guitar; rail column for the multi-chord history). No user gate: pick strongest, ≥2 rejected alts | design | ready | — | `docs/design/related-area-v2.md` | rail always-multi-chord-most-recent-first spec'd; try-this 3-cap + stable-3-slot + 3-shapes-guitar/1-piano with honest math; L-77/L-78 bounded |
|
||||||
| L-77 | Voicings rail — HYBRID (user re-scope 2026-07-13): when a loop is found, HIGHLIGHT the loop chords as a "loop" group AND list the OTHER recent distinct chords from full chordHistory underneath ("also played"); when no loop, just the recent distinct history. Show **≥4 chords total** as soon as any chord history exists (top up from history to reach 4+). Loop group keeps canonical order + moving "now" playhead (voice-leading chips valid); history group most-recent-first, no chips. Each chord expands to its full voicing gallery (keep composition) | engineering | claimed | D-76 | `src/components/JamGuide.jsx`, `src/components/GlanceRail.jsx` | loop highlighted + others underneath; ≥4 when history exists; no-loop → history ≥4 most-recent-first; galleries + focus preserved; audio contract untouched; build+validate+smoke green |
|
| L-77 | Voicings rail — HYBRID (user re-scope 2026-07-13): when a loop is found, HIGHLIGHT the loop chords as a "loop" group AND list the OTHER recent distinct chords from full chordHistory underneath ("also played"); when no loop, just the recent distinct history. Show **≥4 chords total** as soon as any chord history exists (top up from history to reach 4+). Loop group keeps canonical order + moving "now" playhead (voice-leading chips valid); history group most-recent-first, no chips. Each chord expands to its full voicing gallery (keep composition) | engineering | done `9415daf` (direct push per user) | D-76 | `src/components/JamGuide.jsx`, `src/components/GlanceRail.jsx` | loop highlighted + others underneath; ≥4 when history exists; no-loop → history ≥4 most-recent-first; galleries + focus preserved; audio contract untouched; build+validate+smoke green — **done** (GlanceRail +`showTransitions` prop (default true, byte-compat); JamGuide `recentDistinctChords`/`historyStations` memo + hybrid railContent; RAIL_TOTAL_CAP 8 / NO_LOOP_HISTORY_CAP 6; 903/903; only 2 locked files, contract-clean. Maestro folded 2 honesty fixes on shared GlanceRail: history group drops "the loop"/"playhead" framing; history rows non-focusable (no inert button). Full independent Critic gate waived per user "push when its done") |
|
||||||
| L-78 | Try-this per D-76: max 3 subs, stable 3-slot layout, 3 guitar shapes each (3×3) / 1 piano each | engineering | done `d543b98` (direct push per user 2026-07-13; verified build+validate+smoke 903/903 — full independent Critic gate waived for the push) | D-76 | `src/components/TryThis.jsx` (+ per doc) | ≤3 subs in fixed slots; guitar 3 shapes/sub; piano 1; layout stable 2-vs-3; reactivity preserved; build+smoke green |
|
| L-78 | Try-this per D-76: max 3 subs, stable 3-slot layout, 3 guitar shapes each (3×3) / 1 piano each | engineering | done `d543b98` (direct push per user 2026-07-13; verified build+validate+smoke 903/903 — full independent Critic gate waived for the push) | D-76 | `src/components/TryThis.jsx` (+ per doc) | ≤3 subs in fixed slots; guitar 3 shapes/sub; piano 1; layout stable 2-vs-3; reactivity preserved; build+smoke green |
|
||||||
| C-70 | Sprint-end sweep + PR update (BOTH gitea+github). Fold: drop stale ", each playable" from VoicingBrowser aria (:278/:314, combined-gate finding) | quality | backlog | L-70, L-71, L-72, L-74, L-75, L-76, L-77, L-78 | `src/components/VoicingBrowser.jsx` (aria one-liner) | all green; aria fixed; PRs updated |
|
| C-70 | Sprint-end sweep + PR update (BOTH gitea+github). Fold: drop stale ", each playable" from VoicingBrowser aria (:278/:314, combined-gate finding) | quality | backlog | L-70, L-71, L-72, L-74, L-75, L-76, L-77, L-78 | `src/components/VoicingBrowser.jsx` (aria one-liner) | all green; aria fixed; PRs updated |
|
||||||
|
|
||||||
@@ -308,6 +308,7 @@ Emphasis this sprint: **ship the Jam Guide MVP** (put the 8 guitar style packs o
|
|||||||
_(Maestro appends one line per completed iteration: `<date> · <task ids done> · <next>`.)_
|
_(Maestro appends one line per completed iteration: `<date> · <task ids done> · <next>`.)_
|
||||||
|
|
||||||
- 2026-07-13 · **sprint-dashboard-polish — direct push (user "push everything now")** · done: L-78 (`d543b98` Try-this 3×3 ways-to-play — up to 3 guitar grips / 1 piano per suggestion, fixed anti-jump 3-slot frame; verified build+validate-KB+smoke 903/903; full independent Critic gate waived at user request) · L-77 re-scoped SAME DAY per user: rail should HIGHLIGHT loop chords when a loop is found AND list the other recent chords underneath, ≥4 total once history exists (was: replace-with-history). Rebuilding + pushing per "add this to the branch also and push when its done" · next: build L-77 (hybrid loop+history rail) → verify → push both remotes
|
- 2026-07-13 · **sprint-dashboard-polish — direct push (user "push everything now")** · done: L-78 (`d543b98` Try-this 3×3 ways-to-play — up to 3 guitar grips / 1 piano per suggestion, fixed anti-jump 3-slot frame; verified build+validate-KB+smoke 903/903; full independent Critic gate waived at user request) · L-77 re-scoped SAME DAY per user: rail should HIGHLIGHT loop chords when a loop is found AND list the other recent chords underneath, ≥4 total once history exists (was: replace-with-history). Rebuilding + pushing per "add this to the branch also and push when its done" · next: build L-77 (hybrid loop+history rail) → verify → push both remotes
|
||||||
|
- 2026-07-13 · **L-77 done + pushed** (`9415daf`) · hybrid voicings rail: loop group highlighted ("the loop") + "also played" recent-distinct chords underneath (most-recent-first, ≥4 once history exists), no-loop → history rail replaces the single-chord fallback; GlanceRail gains `showTransitions` (default true, loop byte-unchanged). Maestro verified build+validate+smoke 903/903 and folded 2 honesty fixes on the shared GlanceRail (history group no longer says "loop"/"playhead"; history rows non-focusable → no inert button). App.jsx/audio untouched. Pushed both remotes · next: C-70 sprint-end sweep (aria one-liner + PR updates) when user confirms the rail looks right
|
||||||
|
|
||||||
- 2026-06-14 · done: M-01 · in-review (awaiting user pick): D-00a/b/c viz concepts · next: D-SEL (user chooses) → then L-01/D-01/D-02 implement chosen concept
|
- 2026-06-14 · done: M-01 · in-review (awaiting user pick): D-00a/b/c viz concepts · next: D-SEL (user chooses) → then L-01/D-01/D-02 implement chosen concept
|
||||||
- 2026-06-15 · done: D-00a/b/c, D-SEL (Roadmap chosen), L-01 (match.js + banner refactor), L-01b (guideTones/voiceLeadingPairs/soloScale) — Critic PASS both · next: L-02 (JamGuide shell) → D-01 (RoadmapTrack) ‖ D-01b (ChordDiagram)
|
- 2026-06-15 · done: D-00a/b/c, D-SEL (Roadmap chosen), L-01 (match.js + banner refactor), L-01b (guideTones/voiceLeadingPairs/soloScale) — Critic PASS both · next: L-02 (JamGuide shell) → D-01 (RoadmapTrack) ‖ D-01b (ChordDiagram)
|
||||||
|
|||||||
@@ -48,16 +48,31 @@
|
|||||||
// page mid-jam. The L-33 auto-centre effect was deleted in L-40 and must never
|
// 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.
|
// return; the highlight travels, the user owns the scrollbar.
|
||||||
//
|
//
|
||||||
|
// HYBRID rail reuse (task L-77, refines D-76; user directive 2026-07-13 — "highlight
|
||||||
|
// the loop chords when it finds a loop but also add the other chords underneath"):
|
||||||
|
// JamGuide now renders GlanceRail TWICE — once for the canonical LOOP group (the
|
||||||
|
// original call, byte-unchanged) and once for the "also played" recent-history
|
||||||
|
// group. The history group passes `showTransitions={false}` (see the prop below)
|
||||||
|
// because history order is NOT canonical: the between-adjacent voice-leading chips
|
||||||
|
// and the "next" tag are only true for the loop's canonical wheel, so they are
|
||||||
|
// suppressed for the history rail. Everything else (per-row gallery, "now" via
|
||||||
|
// activeIndex, the SoloLabel/AimDots guide-tone education) is correct for any chord
|
||||||
|
// and stays. Default (`showTransitions` absent) is byte-compatible with the loop.
|
||||||
|
//
|
||||||
// Pure presentational. Props:
|
// Pure presentational. Props:
|
||||||
// stations — [{ shape, voicing, rootPc, quality, label, rn }] canonical order
|
// stations — [{ shape, voicing, rootPc, quality, label, rn }] canonical order
|
||||||
// activeIndex — playhead station (canonicalPos); -1 = loop known, playhead
|
// activeIndex — playhead station (canonicalPos); -1 = loop known, playhead
|
||||||
// not — no row is marked "now" (content never changes either way)
|
// not — no row is marked "now" (content never changes either way).
|
||||||
|
// The history group passes -1 (no playhead — see JamGuide).
|
||||||
// focusedIndex — the focused station index, or null (nothing focused)
|
// focusedIndex — the focused station index, or null (nothing focused)
|
||||||
// onFocus — fn(index|null): toggle a station's focus
|
// onFocus — fn(index|null): toggle a station's focus
|
||||||
// instrument — 'guitar' | 'piano' (VoicingBrowser `show`; bass never mounts
|
// instrument — 'guitar' | 'piano' (VoicingBrowser `show`; bass never mounts
|
||||||
// this rail — JamGuide renders BassGuideRows instead, D-40 §3)
|
// this rail — JamGuide renders BassGuideRows instead, D-40 §3)
|
||||||
// keyRoot — key tonic pitch class 0–11 (ChordDiagram fret placement)
|
// keyRoot — key tonic pitch class 0–11 (ChordDiagram fret placement)
|
||||||
// keyMode — key mode name (soloScale's minor-key dominant nudge)
|
// keyMode — key mode name (soloScale's minor-key dominant nudge)
|
||||||
|
// showTransitions — default true (the loop caller is unchanged). false → the
|
||||||
|
// voice-leading TransitionChips and the "next" tag are suppressed
|
||||||
|
// (history order is not canonically adjacent, task L-77).
|
||||||
|
|
||||||
import { NOTES, guideTones, voiceLeadingPairs, soloScale } from '../lib/theory'
|
import { NOTES, guideTones, voiceLeadingPairs, soloScale } from '../lib/theory'
|
||||||
import VoicingBrowser from './VoicingBrowser'
|
import VoicingBrowser from './VoicingBrowser'
|
||||||
@@ -164,23 +179,38 @@ function StationRow({
|
|||||||
style={{ opacity: isNow || isFocused ? 1 : 0.85 }}
|
style={{ opacity: isNow || isFocused ? 1 : 0.85 }}
|
||||||
>
|
>
|
||||||
{/* ── Header line: identity + the folded roadmap education ── */}
|
{/* ── Header line: identity + the folded roadmap education ── */}
|
||||||
|
{/* Loop rows (onToggleFocus provided) keep the focus-toggle button — the
|
||||||
|
D-03 fretboard guide-tone contract, byte-unchanged. History rows pass
|
||||||
|
no toggle → a plain, non-interactive identity (no inert button /
|
||||||
|
misleading "focus" tooltip / stray focus ring), L-77. */}
|
||||||
<div className="mb-1.5 flex flex-wrap items-center gap-x-3 gap-y-1">
|
<div className="mb-1.5 flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||||
<button
|
{onToggleFocus ? (
|
||||||
type="button"
|
<button
|
||||||
aria-pressed={isFocused}
|
type="button"
|
||||||
onClick={onToggleFocus}
|
aria-pressed={isFocused}
|
||||||
title={isFocused
|
onClick={onToggleFocus}
|
||||||
? `Unfocus ${st.label} — clear its guide tones from the fretboard`
|
title={isFocused
|
||||||
: `Focus ${st.label} — light its guide tones on the fretboard`}
|
? `Unfocus ${st.label} — clear its guide tones from the fretboard`
|
||||||
className="flex min-h-[32px] items-center gap-2 rounded px-1 outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
: `Focus ${st.label} — light its guide tones on the fretboard`}
|
||||||
>
|
className="flex min-h-[32px] items-center gap-2 rounded px-1 outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
||||||
<span className="text-sm font-bold leading-none text-gray-100">{st.label}</span>
|
>
|
||||||
{st.rn && (
|
<span className="text-sm font-bold leading-none text-gray-100">{st.label}</span>
|
||||||
<span className="text-[9px] font-medium uppercase tracking-wide text-gray-400">
|
{st.rn && (
|
||||||
{st.rn}
|
<span className="text-[9px] font-medium uppercase tracking-wide text-gray-400">
|
||||||
</span>
|
{st.rn}
|
||||||
)}
|
</span>
|
||||||
</button>
|
)}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="flex min-h-[32px] items-center gap-2 px-1">
|
||||||
|
<span className="text-sm font-bold leading-none text-gray-100">{st.label}</span>
|
||||||
|
{st.rn && (
|
||||||
|
<span className="text-[9px] font-medium uppercase tracking-wide text-gray-400">
|
||||||
|
{st.rn}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{isNow && (
|
{isNow && (
|
||||||
<span className="text-[9px] font-semibold uppercase tracking-widest text-accent">
|
<span className="text-[9px] font-semibold uppercase tracking-widest text-accent">
|
||||||
now
|
now
|
||||||
@@ -222,17 +252,21 @@ function StationRow({
|
|||||||
|
|
||||||
export default function GlanceRail({
|
export default function GlanceRail({
|
||||||
stations = [], activeIndex = -1, focusedIndex = null, onFocus, instrument, keyRoot, keyMode,
|
stations = [], activeIndex = -1, focusedIndex = null, onFocus, instrument, keyRoot, keyMode,
|
||||||
|
showTransitions = true,
|
||||||
}) {
|
}) {
|
||||||
const n = stations.length
|
const n = stations.length
|
||||||
if (n === 0) return null
|
if (n === 0) return null
|
||||||
const nextIndex = activeIndex >= 0 && n > 1 ? (activeIndex + 1) % n : -1
|
// The "next" tag is a loop-adjacency claim → suppressed for the history group.
|
||||||
|
const nextIndex = showTransitions && activeIndex >= 0 && n > 1 ? (activeIndex + 1) % n : -1
|
||||||
|
|
||||||
// Voice-leading rails: rail i leaves station i for station (i+1) mod n — the
|
// 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
|
// 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
|
// is the 7→3 (voiceLeadingPairs lists the 7th first); a one-chord loop has
|
||||||
// no transition to speak of.
|
// no transition to speak of. Suppressed entirely for the history group
|
||||||
|
// (showTransitions=false) — its rows are recent-first, not canonically
|
||||||
|
// adjacent, so a "next F→E" chip would point at the wrong neighbour (L-77).
|
||||||
const rails = stations.map((st, i) => {
|
const rails = stations.map((st, i) => {
|
||||||
if (n < 2) return null
|
if (!showTransitions || n < 2) return null
|
||||||
const next = stations[(i + 1) % n]
|
const next = stations[(i + 1) % n]
|
||||||
return voiceLeadingPairs(
|
return voiceLeadingPairs(
|
||||||
{ root: st.rootPc, quality: st.quality },
|
{ root: st.rootPc, quality: st.quality },
|
||||||
@@ -240,13 +274,30 @@ export default function GlanceRail({
|
|||||||
)[0] ?? null
|
)[0] ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Section framing (L-77 honesty fix): showTransitions === true ⟺ the canonical
|
||||||
|
// LOOP group; the "also played" history group (showTransitions=false) is recent-
|
||||||
|
// first with no playhead, so it must NOT claim "the loop" / "the playhead". Rows
|
||||||
|
// in the history group are also non-focusable (no onFocus → no inert header
|
||||||
|
// button); the loop group's function keeps its focus toggle byte-unchanged.
|
||||||
|
const isLoop = showTransitions
|
||||||
|
const focusable = typeof onFocus === 'function'
|
||||||
|
const sectionAria = isLoop
|
||||||
|
? 'Voicing variations — every chord of the loop, all expanded'
|
||||||
|
: 'Voicing variations — every recently played chord, all expanded'
|
||||||
|
const sectionTitle = isLoop
|
||||||
|
? 'Variations · every chord, every voicing — the playhead highlights'
|
||||||
|
: 'Variations · every chord, every voicing'
|
||||||
|
const sectionFoot = isLoop
|
||||||
|
? "Voicings follow the loop — the playhead highlights the chord you're on."
|
||||||
|
: 'Recent chords — newest first; every voicing of each.'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className="rounded-2xl border border-border bg-panel p-2"
|
className="rounded-2xl border border-border bg-panel p-2"
|
||||||
aria-label="Voicing variations — every chord of the loop, all expanded"
|
aria-label={sectionAria}
|
||||||
>
|
>
|
||||||
<h4 className="mb-2 text-[10px] font-semibold uppercase tracking-widest text-gray-500">
|
<h4 className="mb-2 text-[10px] font-semibold uppercase tracking-widest text-gray-500">
|
||||||
Variations · every chord, every voicing — the playhead highlights
|
{sectionTitle}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2" role="list">
|
<div className="flex flex-col gap-2" role="list">
|
||||||
@@ -257,7 +308,7 @@ export default function GlanceRail({
|
|||||||
isNow={i === activeIndex}
|
isNow={i === activeIndex}
|
||||||
isNext={i === nextIndex}
|
isNext={i === nextIndex}
|
||||||
isFocused={focusedIndex === i}
|
isFocused={focusedIndex === i}
|
||||||
onToggleFocus={() => onFocus?.(focusedIndex === i ? null : i)}
|
onToggleFocus={focusable ? (() => onFocus(focusedIndex === i ? null : i)) : null}
|
||||||
instrument={instrument}
|
instrument={instrument}
|
||||||
keyRoot={keyRoot}
|
keyRoot={keyRoot}
|
||||||
keyMode={keyMode}
|
keyMode={keyMode}
|
||||||
@@ -268,9 +319,9 @@ export default function GlanceRail({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* No ▶ anywhere anymore (dashboard-polish.md §3 — "leave them off,
|
{/* No ▶ anywhere anymore (dashboard-polish.md §3 — "leave them off,
|
||||||
better not"); the rail is purely visual and follows the loop. */}
|
better not"); the rail is purely visual. */}
|
||||||
<p className="mt-2 text-[11px] text-gray-500">
|
<p className="mt-2 text-[11px] text-gray-500">
|
||||||
Voicings follow the loop — the playhead highlights the chord you're on.
|
{sectionFoot}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
+194
-47
@@ -4,7 +4,6 @@ import { buildLoopIndex, matchLoopToProgression, findLoopPosition, chordRootPC }
|
|||||||
import { NOTES, CHORD_TYPES } from '../lib/theory'
|
import { NOTES, CHORD_TYPES } from '../lib/theory'
|
||||||
import GlanceRail, { AimDots, SoloLabel } from './GlanceRail'
|
import GlanceRail, { AimDots, SoloLabel } from './GlanceRail'
|
||||||
import BassPatternCard from './BassPatternCard'
|
import BassPatternCard from './BassPatternCard'
|
||||||
import VoicingBrowser from './VoicingBrowser'
|
|
||||||
import LickCard, { TechniqueLegend } from './LickCard'
|
import LickCard, { TechniqueLegend } from './LickCard'
|
||||||
import PianoLickCard from './PianoLickCard'
|
import PianoLickCard from './PianoLickCard'
|
||||||
import { ExploreSection, VoicingsSection, LevelChips } from './ExplorePanel'
|
import { ExploreSection, VoicingsSection, LevelChips } from './ExplorePanel'
|
||||||
@@ -165,6 +164,92 @@ function lickFitsContext(lick, context) {
|
|||||||
return wanted.length > 0 && tokens.some(t => wanted.includes(t))
|
return wanted.length > 0 && tokens.some(t => wanted.includes(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── "Also played" history rail helpers (task L-77, refines D-76) ─────────────
|
||||||
|
//
|
||||||
|
// The HYBRID voicings rail (user directive 2026-07-13 — "highlight the loop
|
||||||
|
// chords when it finds a loop but also add the other chords underneath … at
|
||||||
|
// least 4 or more"): a loop group (canonical GlanceRail, untouched) PLUS an
|
||||||
|
// "also played" group of the other recently-played distinct chords, most-recent-
|
||||||
|
// first, and — when no loop is found — just the history group. All parsing reuses
|
||||||
|
// the established app idiom (chordRootPC + the CHORD_TYPES suffix inversion,
|
||||||
|
// mirroring TryThis.jsx `parseChordName` / RelatedProgressions) — no theory
|
||||||
|
// re-derivation.
|
||||||
|
|
||||||
|
// Rail size policy. Overall cap across BOTH groups so the column never runs
|
||||||
|
// away; the no-loop history rail caps a little lower. The ≥4 guarantee falls out
|
||||||
|
// of RAIL_TOTAL_CAP − loopLen ≥ 4 − loopLen for any loopLen ≤ RAIL_TOTAL_CAP:
|
||||||
|
// the history top-up is always allowed to reach four total when four distinct
|
||||||
|
// chords exist (it never fabricates — it shows only what was actually played).
|
||||||
|
const RAIL_TOTAL_CAP = 8 // loop group + "also played" group combined
|
||||||
|
const NO_LOOP_HISTORY_CAP = 6 // no loop matched → history rail alone
|
||||||
|
|
||||||
|
// Invert CHORD_TYPES suffix → quality (the app idiom — mirrors TryThis.jsx /
|
||||||
|
// RelatedProgressions; all 14 suffixes are unique).
|
||||||
|
const SUFFIX_TO_QUALITY = Object.fromEntries(
|
||||||
|
Object.entries(CHORD_TYPES).map(([quality, def]) => [def.suffix, quality])
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse a chord-name string → { rootPc, quality } via the shared helpers. Returns
|
||||||
|
// null when unparseable (unknown suffix / bad root) so the caller drops it.
|
||||||
|
function parseHistoryChord(name) {
|
||||||
|
if (typeof name !== 'string') return null
|
||||||
|
const rootPc = chordRootPC(name)
|
||||||
|
if (rootPc < 0) return null
|
||||||
|
const m = name.match(/^[A-G][b#]?(.*)$/)
|
||||||
|
const quality = m ? SUFFIX_TO_QUALITY[m[1]] : undefined
|
||||||
|
if (!quality) return null
|
||||||
|
return { rootPc, quality }
|
||||||
|
}
|
||||||
|
|
||||||
|
// recentDistinctChords(chordHistory, cap, excludeNames) → GlanceRail-shaped
|
||||||
|
// station rows for the "also played" group. Walks chordHistory from the NEWEST
|
||||||
|
// end backward, collecting DISTINCT chord NAMES (first-seen-from-newest wins —
|
||||||
|
// the most-recent occurrence fixes each chord's slot, so a "F Am F Am" ping-pong
|
||||||
|
// yields [Am, F], the different chords each once). Names in `excludeNames` (the
|
||||||
|
// loop group's chords) are skipped so the two groups never duplicate a chord.
|
||||||
|
// Unparseable names are dropped. Returns MOST-RECENT-FIRST, capped. Empty /
|
||||||
|
// undefined history → []. Stations carry identity only (shape/voicing null) — an
|
||||||
|
// arbitrary played chord has no authored KB play, exactly the null `recommended`
|
||||||
|
// GlanceRail already renders gracefully.
|
||||||
|
function recentDistinctChords(chordHistory, cap, excludeNames) {
|
||||||
|
if (!Array.isArray(chordHistory) || cap <= 0) return []
|
||||||
|
const exclude = excludeNames instanceof Set ? excludeNames : new Set(excludeNames ?? [])
|
||||||
|
const seen = new Set()
|
||||||
|
const out = []
|
||||||
|
for (let i = chordHistory.length - 1; i >= 0; i--) {
|
||||||
|
const name = chordHistory[i]
|
||||||
|
if (seen.has(name)) continue
|
||||||
|
seen.add(name)
|
||||||
|
if (exclude.has(name)) continue
|
||||||
|
const parsed = parseHistoryChord(name)
|
||||||
|
if (!parsed) continue
|
||||||
|
out.push({
|
||||||
|
shape: null,
|
||||||
|
voicing: null,
|
||||||
|
rootPc: parsed.rootPc,
|
||||||
|
quality: parsed.quality,
|
||||||
|
label: name,
|
||||||
|
rn: '', // history is key-relative-agnostic here; rn stays empty (cheap, honest)
|
||||||
|
})
|
||||||
|
if (out.length >= cap) break
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small group caption above each rail group (tokens only — no raw hex).
|
||||||
|
function RailGroupCaption({ children, tone = 'loop' }) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className={
|
||||||
|
'mb-1 px-1 text-[9px] font-semibold uppercase tracking-widest ' +
|
||||||
|
(tone === 'loop' ? 'text-accent/80' : 'text-gray-500')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ─── JamGuide — the jam dashboard grid (default export) ───────────────────────
|
// ─── JamGuide — the jam dashboard grid (default export) ───────────────────────
|
||||||
//
|
//
|
||||||
// Props:
|
// Props:
|
||||||
@@ -324,6 +409,22 @@ export default function JamGuide({ detectedProgression, keyInfo, chordHistory =
|
|||||||
return stations
|
return stations
|
||||||
}, [match.matched, match.progression, match.style, instrument, keyRoot])
|
}, [match.matched, match.progression, match.style, instrument, keyRoot])
|
||||||
|
|
||||||
|
// ── "Also played" history stations (task L-77, refines D-76) ────────────────
|
||||||
|
// The other recently-played DISTINCT chords, most-recent-first, that are NOT in
|
||||||
|
// the loop group. When a loop is matched the cap tops the two groups up toward
|
||||||
|
// RAIL_TOTAL_CAP; with no loop the history rail stands alone (NO_LOOP_HISTORY_
|
||||||
|
// CAP). Keyed on chordHistory (+ the loop via stationVoicings) so it recomputes
|
||||||
|
// as chords commit. Loop chords are excluded by their rendered label so the two
|
||||||
|
// groups never repeat a chord. instrument-agnostic identity — GlanceRail /
|
||||||
|
// BassGuideRows draw the per-chord gallery from {rootPc, quality}.
|
||||||
|
const historyStations = useMemo(() => {
|
||||||
|
const loopNames = match.matched ? new Set(stationVoicings.map(s => s.label)) : null
|
||||||
|
const cap = match.matched
|
||||||
|
? Math.max(RAIL_TOTAL_CAP - stationVoicings.length, 0)
|
||||||
|
: NO_LOOP_HISTORY_CAP
|
||||||
|
return recentDistinctChords(chordHistory, cap, loopNames)
|
||||||
|
}, [match.matched, stationVoicings, chordHistory])
|
||||||
|
|
||||||
// ── Authored bass plays (L-42) ──────────────────────────────────────────────
|
// ── Authored bass plays (L-42) ──────────────────────────────────────────────
|
||||||
// When the matched style ships a bass pack with plays for this progression,
|
// When the matched style ships a bass pack with plays for this progression,
|
||||||
// BassGuideRows renders each play's per-station pattern card in the gallery
|
// BassGuideRows renders each play's per-station pattern card in the gallery
|
||||||
@@ -367,62 +468,108 @@ export default function JamGuide({ detectedProgression, keyInfo, chordHistory =
|
|||||||
// re-sorting with the jam (D-31 §2.4).
|
// re-sorting with the jam (D-31 §2.4).
|
||||||
const contextStation = stationVoicings[canonicalPos >= 0 ? canonicalPos : 0] ?? null
|
const contextStation = stationVoicings[canonicalPos >= 0 ? canonicalPos : 0] ?? null
|
||||||
|
|
||||||
// ── The rail (right column at xl / second block stacked): the suggested-
|
// ── The rail (right column at xl / second block stacked) — HYBRID (task L-77,
|
||||||
// voicings surface — GlanceRail, BassGuideRows, the heard-live gallery, or
|
// refines D-76; user directive 2026-07-13: "highlight the loop chords when it
|
||||||
// the honest idle line (one-screen.md §3, §4). ──
|
// finds a loop but also add the other chords underneath … at least 4 or more").
|
||||||
|
// TWO groups, so multiple chords' voicings are ALWAYS visible (the old single-
|
||||||
|
// chord heard-live fallback is retired):
|
||||||
|
// A) LOOP group (only when a loop matches) — the canonical GlanceRail /
|
||||||
|
// BassGuideRows, byte-unchanged: KB order, moving "now" playhead, valid
|
||||||
|
// between-adjacent voice-leading chips. A subtle "the loop" caption marks
|
||||||
|
// it as THE loop.
|
||||||
|
// B) "ALSO PLAYED" group — the other recent DISTINCT chords (historyStations),
|
||||||
|
// most-recent-first, each expanded to its full voicing gallery. NO voice-
|
||||||
|
// leading chips (showTransitions=false — history order is not canonically
|
||||||
|
// adjacent) and NO "now" badge (activeIndex=-1). With no loop this group
|
||||||
|
// stands alone and IS the rail. The ≥4-total guarantee comes from the
|
||||||
|
// RAIL_TOTAL_CAP top-up in `historyStations` (it shows only chords actually
|
||||||
|
// played — never fabricates).
|
||||||
|
// Empty history + no loop → the slim idle line (unchanged). Bass mirrors the
|
||||||
|
// hybrid via BassGuideRows (`live` on the history group suppresses approach —
|
||||||
|
// history is not a loop). ──
|
||||||
|
const hasHistory = historyStations.length > 0
|
||||||
const railContent = match.matched ? (
|
const railContent = match.matched ? (
|
||||||
instrument === 'bass' ? (
|
instrument === 'bass' ? (
|
||||||
/* Bass rows (D-40 §3): authored pattern cards when the matched style
|
<div className="flex flex-col gap-3">
|
||||||
ships a bass pack (L-42), computed roots/fifths/approaches as the
|
<div>
|
||||||
honest fallback otherwise. The licks strip hides either way
|
<RailGroupCaption>the loop</RailGroupCaption>
|
||||||
(guitar tab licks are noise to a bassist mid-jam). */
|
{/* Bass rows (D-40 §3): authored pattern cards when the matched style
|
||||||
<BassGuideRows
|
ships a bass pack (L-42), computed roots/fifths/approaches as the
|
||||||
stations={stationVoicings}
|
honest fallback otherwise. */}
|
||||||
activeIndex={canonicalPos}
|
<BassGuideRows
|
||||||
keyMode={keyInfo?.mode}
|
stations={stationVoicings}
|
||||||
plays={bassPlays}
|
activeIndex={canonicalPos}
|
||||||
/>
|
keyMode={keyInfo?.mode}
|
||||||
|
plays={bassPlays}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{hasHistory && (
|
||||||
|
<div>
|
||||||
|
<RailGroupCaption tone="history">also played · newest first</RailGroupCaption>
|
||||||
|
{/* `live` = no approach line (history is not a canonical loop). */}
|
||||||
|
<BassGuideRows
|
||||||
|
stations={historyStations}
|
||||||
|
activeIndex={-1}
|
||||||
|
keyMode={keyInfo?.mode}
|
||||||
|
live
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* The voicing rail — ALL stations expanded as vertical rows; the
|
<div className="flex flex-col gap-3">
|
||||||
playhead only highlights (D-41, D-40 §4). */
|
<div>
|
||||||
<GlanceRail
|
<RailGroupCaption>the loop</RailGroupCaption>
|
||||||
stations={stationVoicings}
|
{/* The voicing rail — ALL loop stations expanded as vertical rows; the
|
||||||
activeIndex={canonicalPos}
|
playhead only highlights (D-41, D-40 §4). Untouched. */}
|
||||||
focusedIndex={focusedStation}
|
<GlanceRail
|
||||||
onFocus={setFocusedStation}
|
stations={stationVoicings}
|
||||||
instrument={instrument}
|
activeIndex={canonicalPos}
|
||||||
keyRoot={keyRoot}
|
focusedIndex={focusedStation}
|
||||||
keyMode={keyInfo?.mode}
|
onFocus={setFocusedStation}
|
||||||
/>
|
instrument={instrument}
|
||||||
|
keyRoot={keyRoot}
|
||||||
|
keyMode={keyInfo?.mode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{hasHistory && (
|
||||||
|
<div>
|
||||||
|
<RailGroupCaption tone="history">also played · newest first</RailGroupCaption>
|
||||||
|
{/* History group: most-recent-first, no transition chips, no "now". */}
|
||||||
|
<GlanceRail
|
||||||
|
stations={historyStations}
|
||||||
|
activeIndex={-1}
|
||||||
|
instrument={instrument}
|
||||||
|
keyRoot={keyRoot}
|
||||||
|
keyMode={keyInfo?.mode}
|
||||||
|
showTransitions={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
) : liveChord ? (
|
) : hasHistory ? (
|
||||||
/* No loop matched, but chords are committing (D-31 §2.3): a single
|
/* No loop matched, but chords have been played: the "also played" group IS
|
||||||
"heard live" gallery, re-aimed on every chord commit. Auto-follow
|
the rail — recent distinct chords, most-recent-first, ≥4 when available.
|
||||||
only — nothing plays by itself. Bass: D-40 §3's prose forbids guitar/
|
This replaces the old single-chord heard-live fallback (D-76 §0). Bass:
|
||||||
piano galleries under BASS, so the live chord gets the same computed
|
D-40 §3 forbids guitar/piano galleries under BASS, so BassGuideRows draws
|
||||||
root/fifth line (no next chord → no approach) instead. */
|
the computed root/fifth line per chord (`live` → no approach). */
|
||||||
instrument === 'bass' ? (
|
instrument === 'bass' ? (
|
||||||
<BassGuideRows
|
<BassGuideRows
|
||||||
stations={[{ rootPc: liveChord.rootPc, quality: liveChord.type, label: currentChord, rn: '' }]}
|
stations={historyStations}
|
||||||
activeIndex={0}
|
activeIndex={-1}
|
||||||
keyMode={keyInfo?.mode}
|
keyMode={keyInfo?.mode}
|
||||||
live
|
live
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<section
|
<GlanceRail
|
||||||
className="rounded-2xl border border-border bg-panel p-3"
|
stations={historyStations}
|
||||||
aria-label={`Heard live — every ${instrument} voicing of ${currentChord}`}
|
activeIndex={-1}
|
||||||
>
|
instrument={instrument}
|
||||||
<h4 className="mb-1 text-[10px] font-semibold uppercase tracking-widest text-gray-500">
|
keyRoot={keyRoot}
|
||||||
Heard live · {currentChord} — every voicing
|
keyMode={keyInfo?.mode}
|
||||||
</h4>
|
showTransitions={false}
|
||||||
<p className="mb-2 text-[11px] text-gray-500">
|
/>
|
||||||
{detectedProgression?.length
|
|
||||||
? `Heard ${detectedProgression.join(' → ')} — no ${activeStyle} pattern matched yet; following the chord as it commits.`
|
|
||||||
: 'No repeating loop yet — following the chord as it commits.'}
|
|
||||||
</p>
|
|
||||||
<VoicingBrowser rootPc={liveChord.rootPc} quality={liveChord.type} show={instrument} dense />
|
|
||||||
</section>
|
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
/* Nothing heard yet — one slim line (~40px): the idle rail must not
|
/* Nothing heard yet — one slim line (~40px): the idle rail must not
|
||||||
|
|||||||
Reference in New Issue
Block a user