diff --git a/src/App.css b/src/App.css index 78a1a3e..0b08c8b 100644 --- a/src/App.css +++ b/src/App.css @@ -110,6 +110,12 @@ padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; + color: var(--color-text-main); +} + +/* Force button colors to ensure dark mode visibility */ +button { + color: var(--color-text-main); } /* Pitch Monitor Bar */ @@ -341,4 +347,154 @@ color: var(--color-text-main); border-color: var(--color-text-main); transform: scale(1.05); +} + +/* Help Popup */ +.help-popup-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; + backdrop-filter: blur(4px); + animation: fadeIn 0.2s ease-out; +} + +.help-popup { + background-color: var(--color-surface); + padding: var(--spacing-lg); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + max-width: 400px; + width: 90%; + position: relative; + border: 1px solid rgba(128, 128, 128, 0.1); +} + +.help-item { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-sm); + padding-bottom: var(--spacing-sm); + border-bottom: 1px solid rgba(128, 128, 128, 0.1); +} + +.help-item:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.shortcut-key { + background-color: var(--color-bg); + border: 1px solid rgba(128, 128, 128, 0.2); + padding: 2px 8px; + border-radius: 4px; + font-family: monospace; + font-weight: bold; + color: var(--color-text-main); +} + +.help-close { + position: absolute; + top: 10px; + right: 10px; + background: none; + border: none; + cursor: pointer; + color: var(--color-text-muted); +} + +.help-close:hover { + color: var(--color-text-main); +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +/* Header Adjustments */ +.app-header-left { + display: flex; + flex-direction: column; +} + +.app-subtitle { + font-size: 0.8rem; + font-weight: 500; + color: var(--color-text-muted); + margin-top: -4px; +} + +.app-subtitle a { + color: var(--color-primary); + text-decoration: none; + transition: color 0.2s; +} + +.app-subtitle a:hover { + color: var(--color-primary-dark); + text-decoration: underline; +} + +.header-controls { + display: flex; + gap: var(--spacing-sm); + align-items: center; +} + +.icon-button { + background: none; + border: none; + cursor: pointer; + color: var(--color-text-muted); + padding: 8px; + border-radius: 50%; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.icon-button:hover { + background-color: rgba(128, 128, 128, 0.1); + color: var(--color-text-main); +} + +.game-mode-toggle { + display: flex; + background-color: var(--color-bg); + padding: 2px; + border-radius: 20px; + border: 1px solid rgba(128, 128, 128, 0.1); +} + +.toggle-option { + padding: 4px 12px; + border-radius: 16px; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + background: transparent; + border: none; + color: var(--color-text-muted); + transition: all 0.2s; +} + +.toggle-option.active { + background-color: var(--color-surface); + color: var(--color-primary); + box-shadow: var(--shadow-sm); } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 1b21bac..7128a9e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,7 +15,7 @@ import { import { INSTRUMENT_DEFINITIONS } from './music/InstrumentConfigs'; import { FretboardHint } from './components/FretboardHint'; import { TuningMeter } from './components/TuningMeter'; -import { Mic, MicOff, SkipForward, HelpCircle, Volume2 } from 'lucide-react'; +import { Mic, MicOff, SkipForward, HelpCircle, Volume2, X } from 'lucide-react'; import './App.css'; import './styles/skip-button.css'; @@ -185,6 +185,47 @@ function App() { } }, [pitchData, targetMidi, matchStartTime, generateNewNote, settings.rhythm, restartMetronome, feedbackMessage]); + /* Keyboard Shortcuts */ + const [showHelp, setShowHelp] = useState(false); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Ignore if typing in an input + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; + + // prevent default for space to stop scrolling + if (e.code === 'Space') { + e.preventDefault(); + } + + switch (e.key.toLowerCase()) { + case 'h': + setSettings(s => ({ ...s, showHint: !s.showHint })); + break; + case 'z': + setSettings(s => ({ ...s, zenMode: !s.zenMode })); + break; + case 'r': // Replay + case 'p': // Play + playNote(targetMidi, 1.0); + break; + } + + // Check non-character keys via code to avoid layout issues for function keys + if (e.code === 'Space' || e.code === 'Enter') { + generateNewNote(); + } + + if (e.code === 'Escape') { + if (showHelp) setShowHelp(false); + else if (settings.zenMode) setSettings(s => ({ ...s, zenMode: false })); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [settings.zenMode, showHelp, generateNewNote, playNote, targetMidi]); + // Hint text construction const hintPositions = useMemo(() => { if (!settings.showHint) return []; @@ -197,7 +238,37 @@ function App() {
{!settings.zenMode && (
-
Sheet music trainer
+
+
Sheet music trainer
+
+ jensmohrmann.de +
+
+ +
+
+ + +
+ + +
)} @@ -257,21 +328,15 @@ function App() { - -
)} @@ -316,16 +381,45 @@ function App() { ) } - { - settings.zenMode && ( - - ) - } + {/* Floating Zen Mode Button (Toggle) */} + + + {/* Help Popup */} + {showHelp && ( +
setShowHelp(false)}> +
e.stopPropagation()}> + +

Keyboard Shortcuts

+ +
+ Skip Note + Space +
+
+ Toggle Hint + H +
+
+ Toggle Zen Mode + Z +
+
+ Replay Note + R +
+
+ Close Help / Exit Zen + Esc +
+
+
+ )} ); } diff --git a/src/components/Controls.tsx b/src/components/Controls.tsx index cb822de..edb523b 100644 --- a/src/components/Controls.tsx +++ b/src/components/Controls.tsx @@ -152,27 +152,7 @@ export const Controls: React.FC = ({ settings, onUpdateSettings } -
- -
- - -
-
+ {/* Rhythm Controls */}
diff --git a/src/components/SheetMusic.tsx b/src/components/SheetMusic.tsx index e7cca5c..0fc4e85 100644 --- a/src/components/SheetMusic.tsx +++ b/src/components/SheetMusic.tsx @@ -36,6 +36,14 @@ export const SheetMusic: React.FC = ({ renderer.resize(width, height); const context = renderer.getContext(); + // IMPORTANT: Set global styles for the context to ensure everything draws with correct color + // VexFlow uses context properties for default colors. + // We can't easily extract CSS var value here without getComputedStyle, which is expensive inside render. + // However, VexFlow SVGContext just adds attributes. + // We can set fillStyle/strokeStyle to the var string. + context.setFillStyle("var(--color-text-main)"); + context.setStrokeStyle("var(--color-text-main)"); + // --- Helper: Decide which clef a note belongs to in Grand Staff --- // For Grand Staff: usually Split at Middle C (C4 / Midi 60). // >= 60 -> Treble, < 60 -> Bass. @@ -45,6 +53,7 @@ export const SheetMusic: React.FC = ({ let staves: Record = {}; + if (clef === 'grand') { // Create Treble Stave const topStave = new Stave(20, 40, width - 30); @@ -97,8 +106,15 @@ export const SheetMusic: React.FC = ({ clef: noteClef as 'treble' | 'bass' }); + // Set styles for all notes to use CSS variables if not colored specifically + staveNote.setStyle({ fillStyle: "var(--color-text-main)", strokeStyle: "var(--color-text-main)" }); + if (data.accidental) { - staveNote.addModifier(new Accidental(data.accidental)); + const accidental = new Accidental(data.accidental); + // Ensure accidentals also use the color + // VexFlow 4 might not inherit automatically in all contexts, but good to be safe. + // However, StaveNote.setStyle usually propagates. + staveNote.addModifier(accidental); } if (type === 'played') { @@ -131,7 +147,14 @@ export const SheetMusic: React.FC = ({ }); // Add a Question Mark Annotation - ghost.addModifier(new Annotation("?").setVerticalJustification(Annotation.VerticalJustify.CENTER)); + const annotation = new Annotation("?"); + annotation.setVerticalJustification(Annotation.VerticalJustify.CENTER); + // Annotations don't natively support setStyle in older VexFlow, but let's see. + // In VexFlow 4, Font settings are different. + // We can try to rely on current contexts or just standard fill. + // Actually, context fillStyle affects it. + + ghost.addModifier(annotation); targetObj = { note: ghost, clef: noteClef }; } else { diff --git a/src/styles/variables.css b/src/styles/variables.css index 8e68535..66d1c50 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -11,9 +11,10 @@ --color-primary-dark: hsl(var(--hue-primary), 60%, 40%); --color-text-main: hsl(var(--hue-text), 20%, 20%); --color-text-muted: hsl(var(--hue-text), 10%, 60%); - + --color-success: hsl(var(--hue-success), 60%, 45%); --color-error: hsl(var(--hue-error), 70%, 55%); + --color-warning: hsl(35, 90%, 50%); /* Spacing */ --spacing-xs: 4px; @@ -29,11 +30,11 @@ --font-size-lg: 1.25rem; --font-size-xl: 1.5rem; --font-size-2xl: 2rem; - + /* Shadows */ - --shadow-sm: 0 1px 2px rgba(0,0,0,0.05); - --shadow-md: 0 4px 6px rgba(0,0,0,0.05), 0 1px 3px rgba(0,0,0,0.1); - --shadow-lg: 0 10px 15px rgba(0,0,0,0.05), 0 4px 6px rgba(0,0,0,0.02); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.05), 0 1px 3px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.05), 0 4px 6px rgba(0, 0, 0, 0.02); /* Radius */ --radius-sm: 6px; @@ -51,16 +52,16 @@ } body { - margin: 0; - font-family: var(--font-family); - background-color: var(--color-bg); - color: var(--color-text-main); - transition: background-color 0.3s ease, color 0.3s ease; - line-height: 1.5; + margin: 0; + font-family: var(--font-family); + background-color: var(--color-bg); + color: var(--color-text-main); + transition: background-color 0.3s ease, color 0.3s ease; + line-height: 1.5; } #root { - height: 100vh; - display: flex; - flex-direction: column; -} + height: 100vh; + display: flex; + flex-direction: column; +} \ No newline at end of file