diff --git a/src/App.jsx b/src/App.jsx index 6a910e2..271f757 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,6 @@ import { useState, useCallback, useRef, useEffect } from 'react' import AudioCapture from './components/AudioCapture' import ProgressionBanner from './components/ProgressionBanner' -import ProgressionSuggestions from './components/ProgressionSuggestions' import Fretboard from './components/Fretboard' import BassFretboard from './components/BassFretboard' import Tuner from './components/Tuner' @@ -60,6 +59,36 @@ export default function App() { const [showDrumView, setShowDrumView] = useState(false) const [monoColor, setMonoColor] = useState(() => loadStored('wtf_monoColor', false)) + // ── Jam view (task L-50, one-screen.md §1.1) — pure UI/layout state ────────── + // Layer 1: CSS lock — at xl the page root becomes h-screen overflow-hidden and + // everything below the dashboard is unmounted. Layer 2: best-effort browser + // fullscreen (Promise-caught — a refusal leaves layer 1 fully working). The + // toggle never starts/stops listening and never touches audio state. + const [jamView, setJamView] = useState(false) + + function enterJamView() { + setJamView(true) + document.documentElement.requestFullscreen?.()?.catch(() => {}) + } + function exitJamView() { + setJamView(false) + if (document.fullscreenElement) document.exitFullscreen?.()?.catch(() => {}) + } + + // Escape and the native fullscreen exit (any means) both restore normal flow — + // one state, never half-exited. Listeners active only while jamView. + useEffect(() => { + if (!jamView) return + const onKey = (e) => { if (e.key === 'Escape') exitJamView() } + const onFsChange = () => { if (!document.fullscreenElement) setJamView(false) } + window.addEventListener('keydown', onKey) + document.addEventListener('fullscreenchange', onFsChange) + return () => { + window.removeEventListener('keydown', onKey) + document.removeEventListener('fullscreenchange', onFsChange) + } + }, [jamView]) // eslint-disable-line react-hooks/exhaustive-deps + // ── Jam Guide → Fretboard cross-link (D-03) ────────────────────────────────── // When a Roadmap station is tapped, JamGuide reports its {rootPc, quality} // here and the main Fretboard highlights that chord's guide tones (3rd/7th). @@ -436,7 +465,7 @@ export default function App() { } return ( -
+
{/* ── Header ── */}
@@ -576,6 +605,21 @@ export default function App() {
)} + + {/* ── Jam view toggle (L-50, one-screen.md §1.1) — layout-only ── */} +
- {/* ── Instrument + progressions row ── */} -
-
- {instrument === 'guitar' && } - {instrument === 'bass' && } - {instrument === 'piano' && } -
- -
-
- -
-
-
- - - {/* ── Jam Guide band — always open, right below the instrument row (L-40, - D-40 §1: CurrentJamPanel's old slot; the loop shows ONCE, in the - banner above). Follows the one global instrument selector. ── */} + {/* ── The jam dashboard grid (task L-50, one-screen.md §1/§6): JamGuide + owns the two-column layout — LEFT: compact instrument view (chosen + here, passed as the mainView slot) + licks strip + the related- + progressions slot (null until L-51); RIGHT: the suggested-voicings + rail. ProgressionSuggestions is unmounted (file kept) — its job + split into the rail + RelatedProgressions per the user directive. + Follows the one global instrument selector. ── */} + {instrument === 'guitar' && } + {instrument === 'bass' && } + {instrument === 'piano' && } + + } + relatedSlot={null} + fill={jamView} /> + {/* ── Below the dashboard — the learning / behind-the-scenes area (page + scroll in normal mode; UNMOUNTED in jam view, one-screen.md §1.1/§3: + conditional mount, not `hidden`, so collapsed chrome can't leak + height). ── */} + {!jamView && ( + <> {/* ── Loop station ── */} + + )} ) } diff --git a/src/components/BassFretboard.jsx b/src/components/BassFretboard.jsx index fa99e58..14b975d 100644 --- a/src/components/BassFretboard.jsx +++ b/src/components/BassFretboard.jsx @@ -33,7 +33,10 @@ function noteColor(isChordTone, isPenta, isScale, mono = false) { return null } -export default function BassFretboard({ keyInfo, currentChord, monoColor = false }) { +// `compact` (task L-50, one-screen.md §2): trimmed card chrome (p-3, legend +// merged onto the heading line) + a natural-width cap on the SVG (max-width = +// its viewBox width, so it never renders above scale 1.0). +export default function BassFretboard({ keyInfo, currentChord, monoColor = false, compact = false }) { const { root, mode } = keyInfo ?? {} if (!root) return null @@ -44,19 +47,40 @@ export default function BassFretboard({ keyInfo, currentChord, monoColor = false ? new Set(getChordTones(currentChord).map(n => NOTES.indexOf(n))) : new Set() + const heading = ( +

+ Bass — {root} {mode} + {currentChord && / {currentChord}} +

+ ) + + const legend = ( + // Critic mechanical fix (L-50 gate): non-compact keeps HEAD's exact class + // string so the non-compact render stays byte-identical to the committed one. +
+ Chord tone + Pentatonic + Scale +
+ ) + return ( -
-

- Bass — {root} {mode} - {currentChord && / {currentChord}} -

+
+ {compact ? ( +
+ {heading} + {legend} +
+ ) : ( + heading + )}
{/* Fretboard background */}
-
- Chord tone - Pentatonic - Scale -
+ {!compact && legend}
) } diff --git a/src/components/Fretboard.jsx b/src/components/Fretboard.jsx index 4b31ee4..353947d 100644 --- a/src/components/Fretboard.jsx +++ b/src/components/Fretboard.jsx @@ -37,7 +37,11 @@ function noteColor(isChordTone, isPenta, isScale, mono = false) { return null } -export default function Fretboard({ keyInfo, currentChord, pentatonicOnly = false, monoColor = false, jamFocusChord = null }) { +// `compact` (task L-50, one-screen.md §2): trimmed card chrome (p-3, legend +// merged onto the heading line) + a natural-width cap on the SVG (max-width = +// its viewBox width, so it never renders above scale 1.0). No fret reduction, +// no transform scaling — the notes stay at their designed size. +export default function Fretboard({ keyInfo, currentChord, pentatonicOnly = false, monoColor = false, jamFocusChord = null, compact = false }) { const { root, mode } = keyInfo ?? {} if (!root) return null @@ -78,20 +82,49 @@ export default function Fretboard({ keyInfo, currentChord, pentatonicOnly = fals const focusLabel = pc => pc === focusThird ? '3' : pc === focusSeventh ? focusSeventhLabel : null + const heading = ( +

+ Fretboard — {root} {mode} + {currentChord && / {currentChord}} + {hasFocus && ◎ guide tones} +

+ ) + + const legend = ( + // Critic mechanical fix (L-50 gate): non-compact keeps HEAD's exact class + // string so the non-compact render stays byte-identical to the committed one. +
+ Chord tone + Pentatonic + Scale + {hasFocus && ( + + + Guide tones (3 / {focusSeventhLabel}) + + )} +
+ ) + return ( -
-

- Fretboard — {root} {mode} - {currentChord && / {currentChord}} - {hasFocus && ◎ guide tones} -

+
+ {compact ? ( +
+ {heading} + {legend} +
+ ) : ( + heading + )}
{/* Fretboard background */}
-
- Chord tone - Pentatonic - Scale - {hasFocus && ( - - - Guide tones (3 / {focusSeventhLabel}) - - )} -
+ {!compact && legend}
) } diff --git a/src/components/JamGuide.jsx b/src/components/JamGuide.jsx index 78ac67b..aa573a7 100644 --- a/src/components/JamGuide.jsx +++ b/src/components/JamGuide.jsx @@ -10,17 +10,24 @@ import { ExploreSection, VoicingsSection, LevelChips } from './ExplorePanel' import { pianoVoicingChain } from '../lib/piano' import { parseChord } from '../lib/voicings' -// ─── JamGuide.jsx — the jam BAND + the Knowledge Center DOCK ───────────────── +// ─── JamGuide.jsx — the jam-grid OWNER + the Knowledge Center DOCK ─────────── // -// Task L-40 (per docs/design/integrated-glance.md §5–§6.1) split the old -// four-section bottom dock in two: +// Task L-50 (per docs/design/one-screen.md §6) promoted the default export from +// "band" to the two-column jam dashboard grid: // -// default export `JamGuide` — the always-open, zero-chrome jam band, mounted -// by App directly below the instrument row (CurrentJamPanel's old slot). -// Body: GlanceRail voicings + LicksStrip for the matched loop; heard-live -// single gallery when no loop is matched; a slim one-line hint when nothing -// is heard. The loop itself renders ONCE, in ProgressionBanner (D-40 §2) — -// RoadmapTrack is unmounted (file retired in place, deletion backlogged). +// default export `JamGuide` — renders the xl: two-column flex region. +// LEFT (flex-1): the `mainView` slot (App keeps choosing Fretboard / +// BassFretboard / Piano — JamGuide never imports them), the LicksStrip, +// and the `relatedSlot` (RelatedProgressions, mounted by App — null until +// L-51). RIGHT (500px): the suggested-voicings rail — GlanceRail / +// BassGuideRows / the heard-live fallback — inside the design's ONE +// justified internal scroller (height-bounded, NOT sticky, §1). Below xl +// the columns stack in jam-following order via display:contents + order +// classes: mainView → rail → licks → related (§7; rail unbounds). +// The `fill` prop (jam view, §1.1) swaps the rail's viewport-calc bound +// for h-full and makes the left column a flex stack whose related slot +// absorbs the remainder. The loop itself renders ONCE, in +// ProgressionBanner (D-40 §2). // named export `KnowledgeDock` — the bottom collapsible browse/study area: // Explore / Voicings / Licks & Techniques + the shared level filter (the // old dock minus its jam section, which IS the band now). @@ -149,7 +156,7 @@ function lickFitsContext(lick, context) { return wanted.length > 0 && tokens.some(t => wanted.includes(t)) } -// ─── JamGuide — the always-open jam band (default export) ───────────────────── +// ─── JamGuide — the jam dashboard grid (default export) ─────────────────────── // // Props: // detectedProgression : string[] | null — the live detected loop (chord names) @@ -158,12 +165,16 @@ function lickFitsContext(lick, context) { // currentChord : string | undefined — most recent committed chord // onFocusChord : fn({rootPc,quality}|null) — Fretboard guide-tone link (D-03) // instrument : 'guitar' | 'piano' | 'bass' — App's global selector +// mainView : JSX slot — the compact instrument view (App-chosen; L-50) +// relatedSlot : JSX slot — RelatedProgressions (App-mounted; null until L-51) +// fill : boolean — jam view (one-screen.md §1.1): the grid fills +// App's h-screen column; rail bound becomes h-full // The band shows ALL levels — a glance surface filters nothing (D-40 §5); the // level filter lives in the KnowledgeDock only. const ALL_LEVELS = { foundation: true, intermediate: true } -export default function JamGuide({ detectedProgression, keyInfo, chordHistory = [], currentChord, onFocusChord, instrument = 'guitar' }) { +export default function JamGuide({ detectedProgression, keyInfo, chordHistory = [], currentChord, onFocusChord, instrument = 'guitar', mainView = null, relatedSlot = null, fill = false }) { // Style labels straight from the KB registry, via each style's meta. const styles = useMemo( () => Object.entries(kb).map(([id, style]) => ({ id, label: style?.meta?.label ?? id })), @@ -343,96 +354,140 @@ export default function JamGuide({ detectedProgression, keyInfo, chordHistory = // re-sorting with the jam (D-31 §2.4). const contextStation = stationVoicings[canonicalPos >= 0 ? canonicalPos : 0] ?? null - return ( -
- - {/* ── Micro-header — a line, not a button (D-40 §1: zero chrome) ── */} -

- Jam Guide — {headerLabel} - {match.matched && keyInfo?.root ? ` · in ${keyInfo.root} ${keyInfo.mode}` : ''} -

- - {match.matched ? ( - instrument === 'bass' ? ( - /* Bass rows (D-40 §3): authored pattern cards when the matched style - ships a bass pack (L-42), computed roots/fifths/approaches as the - honest fallback otherwise. The licks strip hides either way - (guitar tab licks are noise to a bassist mid-jam). */ - - ) : ( -
- {/* The voicing rail — ALL stations expanded as vertical rows; the - playhead only highlights (D-41, D-40 §4). */} - - -
- ) - ) : liveChord ? ( - /* No loop matched, but chords are committing (D-31 §2.3): a single - "heard live" gallery, re-aimed on every chord commit. Auto-follow - only — nothing plays by itself. Bass: D-40 §3's prose forbids guitar/ - piano galleries under BASS, so the live chord gets the same computed - root/fifth line (no next chord → no approach) instead. */ - instrument === 'bass' ? ( - - ) : ( -
-
-

- Heard live · {currentChord} — every voicing -

-

- {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.'} -

- -
- {/* No station rn without a loop — context sort falls back to the - live chord's quality key (e.g. a "dom7" lick fits a live G7). */} - -
- ) - ) : ( - /* Nothing heard yet — one slim line (~40px, D-40 §1): the idle band - must not waste main-module space. */ -

+ // ── The rail (right column at xl / second block stacked): the suggested- + // voicings surface — GlanceRail, BassGuideRows, the heard-live gallery, or + // the honest idle line (one-screen.md §3, §4). ── + const railContent = match.matched ? ( + instrument === 'bass' ? ( + /* Bass rows (D-40 §3): authored pattern cards when the matched style + ships a bass pack (L-42), computed roots/fifths/approaches as the + honest fallback otherwise. The licks strip hides either way + (guitar tab licks are noise to a bassist mid-jam). */ + + ) : ( + /* The voicing rail — ALL stations expanded as vertical rows; the + playhead only highlights (D-41, D-40 §4). */ + + ) + ) : liveChord ? ( + /* No loop matched, but chords are committing (D-31 §2.3): a single + "heard live" gallery, re-aimed on every chord commit. Auto-follow + only — nothing plays by itself. Bass: D-40 §3's prose forbids guitar/ + piano galleries under BASS, so the live chord gets the same computed + root/fifth line (no next chord → no approach) instead. */ + instrument === 'bass' ? ( + + ) : ( +

+

+ Heard live · {currentChord} — every voicing +

+

{detectedProgression?.length - ? `Heard ${detectedProgression.join(' → ')} — no ${activeStyle} pattern matched yet; voicings follow the next chord that commits.` - : 'Play a few bars — voicings and licks for your loop land here.'} + ? `Heard ${detectedProgression.join(' → ')} — no ${activeStyle} pattern matched yet; following the chord as it commits.` + : 'No repeating loop yet — following the chord as it commits.'}

- )} + +
+ ) + ) : ( + /* Nothing heard yet — one slim line (~40px): the idle rail must not + waste dashboard space. */ +

+ {detectedProgression?.length + ? `Heard ${detectedProgression.join(' → ')} — no ${activeStyle} pattern matched yet; voicings follow the next chord that commits.` + : 'Play a few bars — voicings and licks for your loop land here.'} +

+ ) + + // ── The licks strip (left column). Matched loops sort by the playhead + // station; heard-live falls back to the live chord's quality key (e.g. a + // "dom7" lick fits a live G7). Bass hides it (guitar tab licks are noise + // to a bassist mid-jam); LicksStrip also hides itself when empty. ── + const licksStrip = instrument !== 'bass' && match.matched ? ( + + ) : instrument !== 'bass' && liveChord ? ( + + ) : null + + // ── The grid (one-screen.md §1, §6): left flex-1 / right 500px at xl; + // stacked below xl in jam-following order (mainView → rail → licks → + // related) via display:contents on the left wrapper + order classes — one + // mount per surface, no duplicates (§7). The rail wrapper is the design's + // ONE justified internal scroller: height-bounded in normal mode, h-full + // in jam view (`fill`), NOT sticky (§1). ── + return ( +
+ {/* LEFT — instrument view · licks · related progressions */} +
+ {mainView != null && ( +
+ {mainView} +
+ )} + {licksStrip != null && ( +
+ {licksStrip} +
+ )} + {relatedSlot != null && ( +
+ {relatedSlot} +
+ )} +
+ + {/* RIGHT — the suggested-voicings rail (the one contained scroller) */} +
+ {/* Micro-header — a line, not a button (D-40 §1: zero chrome) */} +

+ Suggested voicings — {headerLabel} + {match.matched && keyInfo?.root ? ` · in ${keyInfo.root} ${keyInfo.mode}` : ''} +

+ {railContent} +
) } diff --git a/src/components/Piano.jsx b/src/components/Piano.jsx index 731c09a..e63a7ff 100644 --- a/src/components/Piano.jsx +++ b/src/components/Piano.jsx @@ -37,7 +37,10 @@ function keyColor(isChordTone, isPenta, isScale, isBlack, mono = false) { : { fill: '#f5f5f5', text: '#6b7280' } } -export default function Piano({ keyInfo, currentChord, monoColor = false }) { +// `compact` (task L-50, one-screen.md §2): trimmed card chrome (p-3, legend +// merged onto the heading line) + a natural-width cap on the SVG (max-width = +// its viewBox width, so it never renders above scale 1.0). +export default function Piano({ keyInfo, currentChord, monoColor = false, compact = false }) { const { root, mode } = keyInfo ?? {} if (!root) return null @@ -51,15 +54,36 @@ export default function Piano({ keyInfo, currentChord, monoColor = false }) { const svgW = totalWhite * KEY_W + 2 const svgH = KEY_H + 20 // +20 for octave labels + const heading = ( +

+ Piano — {root} {mode} + {currentChord && / {currentChord}} +

+ ) + + const legend = ( + // Critic mechanical fix (L-50 gate): non-compact keeps HEAD's exact class + // string so the non-compact render stays byte-identical to the committed one. +
+ Chord tone + Pentatonic + Scale +
+ ) + return ( -
-

- Piano — {root} {mode} - {currentChord && / {currentChord}} -

+
+ {compact ? ( +
+ {heading} + {legend} +
+ ) : ( + heading + )}
- + {/* White keys */} {Array.from({ length: OCTAVES }, (_, oct) => @@ -132,11 +156,7 @@ export default function Piano({ keyInfo, currentChord, monoColor = false }) {
-
- Chord tone - Pentatonic - Scale -
+ {!compact && legend}
) }