From d543b986dc158db3db60fdf0b28ff8b7b405453a Mon Sep 17 00:00:00 2001 From: vadimwit Date: Mon, 13 Jul 2026 18:46:10 +0100 Subject: [PATCH] =?UTF-8?q?feat(trythis):=203x3=20ways-to-play=20=E2=80=94?= =?UTF-8?q?=20up=20to=203=20guitar=20grips=20/=201=20piano=20per=20suggest?= =?UTF-8?q?ion=20(task=20L-78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Try-this now fills its space: each of up to 3 substitutions renders as a row with a left identity block (chord chip -> modal, category tag, why) and a right "ways to play it" block that follows the global instrument — guitar shows up to 3 genuinely different grips (getGuitarVoicings), piano one MiniPiano, bass a root caption. A FIXED 3-slot frame reserves height for all three rows (present -> SubRow, absent -> dashed EmptySlot) so suggestions #1/#2 never shift when a chord yields 2 vs 3 subs (the anti-jump ask). Honest: add9's 2-shape roots show 2 grips, not padded fakes. Pure parseChordName/subsForChord/pickSubject preserved (per-chord reactivity intact). Design tokens only. Verified: build + validate-KB + smoke 903/903 green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/TryThis.jsx | 168 +++++++++++++++++++++++-------------- 1 file changed, 106 insertions(+), 62 deletions(-) diff --git a/src/components/TryThis.jsx b/src/components/TryThis.jsx index 90a9c28..4ef167a 100644 --- a/src/components/TryThis.jsx +++ b/src/components/TryThis.jsx @@ -5,16 +5,23 @@ import { pianoVoicing } from '../lib/piano' import ChordDiagram from './ChordDiagram' import MiniPiano from './MiniPiano' -// ─── TryThis — side-by-side substitution cards, each with a mini diagram (L-75) ─ +// ─── TryThis — stable 3-slot rows-of-shapes substitution rail (L-78 / D-76) ───── // -// For the chord under the playhead, in the detected key, show ALL curated -// substitutions (from theory.js `suggestSubstitutions`, the L-73 engine) SIDE BY -// SIDE — one card per idea — each carrying a mini instrument diagram of that -// chord (guitar ChordDiagram / piano MiniPiano / bass root caption), following -// the global instrument. Reverses L-74's rotating one-at-a-time card: the user -// saw the rotation and asked for all-visible ("put them next to each other, we -// have enough space"). Spec: docs/design/related-area-layout.md §1–3. -// Sibling of RelatedProgressions; same micro-header + chip idiom. +// For the chord under the playhead, in the detected key, show up to THREE curated +// substitutions (from theory.js `suggestSubstitutions`, the L-73 engine) as +// stacked ROWS — turned 90° from L-75's cards-across. Each row = a left identity +// block (tappable chord chip + category tag + why) and a right "ways to play it" +// block that follows the global instrument: +// guitar → up to 3 ChordDiagram thumbs (the 3×3 grid), the genuinely different +// grips from getGuitarVoicings(label).slice(0, 3) +// piano → ONE MiniPiano ("for piano it can be just one thats okay") +// bass → a root · {note} caption (no honest compact bass-chord renderer) +// +// The layout is a FIXED 3-slot frame: present subs render SubRow, absent slots +// render a subtle EmptySlot that holds the exact row height — so sub #1 and #2 +// never shift position whether the chord yields 2 or 3 subs (the user's anti-jump +// ask: "annoying when the layout changes then u dont know where to look"). +// Spec: docs/design/related-area-v2.md §2. Sibling of RelatedProgressions. // // Props: // loop : string[] | null — the detected repeating progression (chord names) @@ -54,6 +61,14 @@ const CATEGORY_TAG = { secondary_dominant: 'V7', } +// Per-instrument uniform row height (§2.2/§3). All 3 slots — filled or empty — +// share the current instrument's height so the frame never reflows. +const ROW_MIN_H = { + guitar: 'min-h-[100px]', + piano: 'min-h-[72px]', + bass: 'min-h-[64px]', +} + // Compute the substitution set for a chord NAME at loop index `pos` (−1 when the // chord is not a loop station). The next station's root pc gates Rule D (secondary // dominant of the next chord). Returns { name, pos, subs } or null when the name @@ -89,22 +104,26 @@ function pickSubject(loopArr, keyInfo, currentChord) { return null } -// The mini diagram drawn inside a SubCard, chosen by the global instrument (§2): -// guitar → first resolved guitar shape (absolute frets) via ChordDiagram thumb; -// no shape available → no diagram (honest, chip + why only). -// piano → pianoVoicing for the chord, rootPc spread back in so the "R" badge -// lands correctly (VoicingBrowser:317-319 caveat), MiniPiano size mini. -// bass → no diagram (no honest compact bass-chord renderer); a small -// root · {note} caption instead. -function SubDiagram({ sub, instrument }) { +// The "ways to play" block on the right of a SubRow, chosen by the global +// instrument (§2.3): +// guitar → up to 3 ChordDiagram thumbs (the 3×3), each captioned with its shape +// name so the three read as genuinely different grips. Fewer than 3 +// shapes exist (e.g. add9 → 2 for most roots) → show what exists, never +// pad with fakes (honest, §2.4). None → nothing (chip + why carry it). +// piano → ONE MiniPiano; rootPc spread back in so the "R" badge lands right. +// bass → root · {note} caption (no honest compact bass-chord renderer). +function WaysToPlay({ sub, instrument }) { if (instrument === 'piano') { return ( - +
+ +
) } + if (instrument === 'bass') { return ( @@ -112,39 +131,71 @@ function SubDiagram({ sub, instrument }) { ) } - // guitar (default): first resolved shape; omit the label — the chip names it. - const shape = getGuitarVoicings(sub.label)[0] - if (!shape) return null - return + + // guitar (default): up to 3 genuinely different grips, left-aligned. Omit the + // ChordDiagram label (the chip names the chord) — caption the shape name below. + const shapes = getGuitarVoicings(sub.label).slice(0, 3) + if (!shapes.length) return null + return ( +
+ {shapes.map((shape, i) => ( +
+ + + {shape.label} + +
+ ))} +
+ ) } -// One substitution card — chip (tappable → modal) + mini diagram + tag + why. -function SubCard({ sub, instrument, onChordClick }) { +// One substitution ROW — left identity (chip → modal + tag + why) | right ways. +function SubRow({ sub, instrument, onChordClick }) { const isCircle = CIRCLE_CATEGORIES.has(sub.category) const tag = CATEGORY_TAG[sub.category] ?? sub.category return ( -
- - -
- +
+ {/* Left identity block (~180px) */} +
+
+ + + {tag} + {isCircle && } + +
+

+ {sub.why} +

- - {tag} - {isCircle && } - + {/* Right "ways to play" block */} +
+ +
+
+ ) +} -

- {sub.why} -

+// A reserved-but-empty slot — holds the exact SubRow height so the present subs +// never move when the chord yields fewer than 3 (§2.2, the anti-jump). +function EmptySlot({ instrument }) { + return ( + ) } @@ -157,16 +208,7 @@ export default function TryThis({ loop, keyInfo, currentChord, onChordClick, ins if (!subject) return null const subjectChord = subject.name - const subs = subject.subs - - // Piano keyboards are up to ~199px wide (C-anchored window), so 4 cannot share a - // 720px row — lay them out 2×2. Guitar (75px cell) and bass (no diagram) sit - // 4-across in one flex-wrap row; with 1–3 subs the cards grow to fill (§1.1/§3). - const isPiano = instrument === 'piano' - const listClass = isPiano - ? 'grid grid-cols-1 sm:grid-cols-2 gap-2' - : 'flex flex-wrap gap-2' - const cardBasis = isPiano ? '' : 'basis-[168px] grow min-w-[152px]' + const subs = subject.subs.slice(0, 3) // cap at 3 (§2.1) return (
-
- {subs.map((sub, i) => ( -
- -
- ))} + {/* Fixed 3-slot frame — a present sub → SubRow, an absent one → EmptySlot, + so sub #1/#2 hold their position whether there are 2 or 3 subs. */} +
+ {[0, 1, 2].map(i => + subs[i] + ? + : + )}
)