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 && (
)}
@@ -257,21 +328,15 @@ function App() {
-
)}
@@ -316,16 +381,45 @@ function App() {
)
}
- {
- settings.zenMode && (
- setSettings(s => ({ ...s, zenMode: false }))}
- >
- Exit Zen Mode
-
- )
- }
+ {/* Floating Zen Mode Button (Toggle) */}
+ setSettings(s => ({ ...s, zenMode: !s.zenMode }))}
+ title="Keyboard Shortcut: Z"
+ >
+ {settings.zenMode ? "Exit Zen Mode" : "Zen Mode"}
+
+
+ {/* Help Popup */}
+ {showHelp && (
+ setShowHelp(false)}>
+
e.stopPropagation()}>
+
setShowHelp(false)}>
+
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 }
-
-
-
- onUpdateSettings({ ...settings, gameMode: 'sight_reading' })}
- style={{ flex: 1 }}
- >
- Sight Reading
-
- onUpdateSettings({ ...settings, gameMode: 'ear_training' })}
- style={{ flex: 1 }}
- >
- Ear Training
-
-
-
+
{/* 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