mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-09-02 09:34:33 +02:00
UI improvements
This commit is contained in:
40
src/App.css
40
src/App.css
@@ -10,7 +10,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--spacing-md) var(--spacing-xl);
|
padding: var(--spacing-sm) var(--spacing-xl);
|
||||||
background-color: var(--color-surface);
|
background-color: var(--color-surface);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
@@ -183,22 +183,49 @@ button {
|
|||||||
background-color: var(--color-surface);
|
background-color: var(--color-surface);
|
||||||
padding: var(--spacing-md);
|
padding: var(--spacing-md);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-md);
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls-container {
|
.controls-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-xl);
|
flex-direction: column;
|
||||||
align-items: flex-end;
|
gap: var(--spacing-lg);
|
||||||
flex-wrap: wrap;
|
width: 100%;
|
||||||
justify-content: center;
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
width: 100%;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile: Stack everything */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
|
||||||
|
.settings-grid,
|
||||||
|
.tools-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-group {
|
.control-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-label {
|
.control-label {
|
||||||
@@ -441,7 +468,6 @@ button {
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
margin-top: -4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-subtitle a {
|
.app-subtitle a {
|
||||||
|
|||||||
38
src/App.tsx
38
src/App.tsx
@@ -48,7 +48,8 @@ function App() {
|
|||||||
zenMode: false,
|
zenMode: false,
|
||||||
gameMode: 'sight_reading',
|
gameMode: 'sight_reading',
|
||||||
customMinFret: 0,
|
customMinFret: 0,
|
||||||
customMaxFret: 12
|
customMaxFret: 12,
|
||||||
|
autoPlaySightReading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const [matchStartTime, setMatchStartTime] = useState<number | null>(null);
|
const [matchStartTime, setMatchStartTime] = useState<number | null>(null);
|
||||||
@@ -154,14 +155,16 @@ function App() {
|
|||||||
|
|
||||||
// Audio Playback trigger
|
// Audio Playback trigger
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (settings.gameMode === 'ear_training' && !revealed) {
|
const shouldAutoPlay = settings.gameMode === 'ear_training' || (settings.gameMode === 'sight_reading' && settings.autoPlaySightReading);
|
||||||
|
|
||||||
|
if (shouldAutoPlay && !revealed) {
|
||||||
// Add a small delay to ensure state settles or allow UI to update
|
// Add a small delay to ensure state settles or allow UI to update
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
playNote(targetMidi, 1.0); // Play for 1 second
|
playNote(targetMidi, 1.0); // Play for 1 second
|
||||||
}, 100);
|
}, 100);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [targetMidi, settings.gameMode, revealed, playNote]);
|
}, [targetMidi, settings.gameMode, settings.autoPlaySightReading, revealed, playNote]);
|
||||||
|
|
||||||
// Initial note, and whenever difficulty/tuning/gamemode changes
|
// Initial note, and whenever difficulty/tuning/gamemode changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -309,9 +312,6 @@ function App() {
|
|||||||
<header className="app-header">
|
<header className="app-header">
|
||||||
<div className="app-header-left">
|
<div className="app-header-left">
|
||||||
<div className="logo">Sheet music trainer</div>
|
<div className="logo">Sheet music trainer</div>
|
||||||
<div className="app-subtitle">
|
|
||||||
<a href="http://jensmohrmann.de" target="_blank" rel="noopener noreferrer">jensmohrmann.de</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="header-controls">
|
<div className="header-controls">
|
||||||
@@ -368,18 +368,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{settings.gameMode === 'ear_training' && !settings.zenMode && (
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '16px' }}>
|
|
||||||
<button
|
|
||||||
className="control-button"
|
|
||||||
onClick={() => playNote(targetMidi, 1.0)}
|
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '8px 16px' }}
|
|
||||||
>
|
|
||||||
<Volume2 size={24} />
|
|
||||||
Play Note
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{settings.showHint && (
|
{settings.showHint && (
|
||||||
<div className="hint-card">
|
<div className="hint-card">
|
||||||
@@ -404,6 +393,16 @@ function App() {
|
|||||||
<HelpCircle size={18} />
|
<HelpCircle size={18} />
|
||||||
{settings.showHint ? "Hide Hint" : "Show Hint"}
|
{settings.showHint ? "Hide Hint" : "Show Hint"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="hint-button"
|
||||||
|
onClick={() => playNote(targetMidi, 1.0)}
|
||||||
|
title="Keyboard Shortcut: P or R"
|
||||||
|
>
|
||||||
|
<Volume2 size={18} />
|
||||||
|
Play Note
|
||||||
|
</button>
|
||||||
|
|
||||||
<button className="skip-button" onClick={generateNewNote} title="Keyboard Shortcut: Space">
|
<button className="skip-button" onClick={generateNewNote} title="Keyboard Shortcut: Space">
|
||||||
<SkipForward size={18} />
|
<SkipForward size={18} />
|
||||||
Skip Note
|
Skip Note
|
||||||
@@ -448,6 +447,9 @@ function App() {
|
|||||||
{!settings.zenMode && (
|
{!settings.zenMode && (
|
||||||
<footer className="settings-footer">
|
<footer className="settings-footer">
|
||||||
<Controls settings={settings} onUpdateSettings={setSettings} />
|
<Controls settings={settings} onUpdateSettings={setSettings} />
|
||||||
|
<div className="app-subtitle">
|
||||||
|
<a href="http://jensmohrmann.de" target="_blank" rel="noopener noreferrer">jensmohrmann.de</a>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface AppSettings {
|
|||||||
gameMode: 'sight_reading' | 'ear_training';
|
gameMode: 'sight_reading' | 'ear_training';
|
||||||
customMinFret?: number;
|
customMinFret?: number;
|
||||||
customMaxFret?: number;
|
customMaxFret?: number;
|
||||||
|
autoPlaySightReading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ControlsProps {
|
interface ControlsProps {
|
||||||
@@ -77,213 +78,253 @@ export const Controls: React.FC<ControlsProps> = ({ settings, onUpdateSettings }
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="controls-container">
|
<div className="controls-container">
|
||||||
<div className="control-group">
|
{/* Top Section: Settings Grid (2 Columns) */}
|
||||||
<label className="control-label">
|
<div className="settings-grid">
|
||||||
<Music size={18} />
|
{/* Row 1, Col 1 */}
|
||||||
<span>Instrument</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={settings.instrument}
|
|
||||||
onChange={handleInstrumentChange}
|
|
||||||
className="control-select"
|
|
||||||
>
|
|
||||||
{Object.values(INSTRUMENT_DEFINITIONS).map(def => (
|
|
||||||
<option key={def.id} value={def.id}>{def.displayName}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentInstrumentDef.showTuning && (
|
|
||||||
<div className="control-group">
|
<div className="control-group">
|
||||||
<label className="control-label">
|
<label className="control-label">
|
||||||
<Guitar size={18} />
|
<Music size={18} />
|
||||||
<span>Tuning</span>
|
<span>Instrument</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={settings.tuningId}
|
value={settings.instrument}
|
||||||
onChange={handleTuningChange}
|
onChange={handleInstrumentChange}
|
||||||
className="control-select"
|
className="control-select"
|
||||||
>
|
>
|
||||||
{availableTunings.map(id => (
|
{Object.values(INSTRUMENT_DEFINITIONS).map(def => (
|
||||||
<option key={id} value={id}>{TUNINGS[id].name}</option>
|
<option key={def.id} value={def.id}>{def.displayName}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="control-group">
|
{/* Row 1, Col 2 (Conditional) */}
|
||||||
<label className="control-label">
|
{currentInstrumentDef.showTuning ? (
|
||||||
<Settings size={18} />
|
<div className="control-group">
|
||||||
<span>Note Set</span>
|
<label className="control-label">
|
||||||
</label>
|
<Guitar size={18} />
|
||||||
<select
|
<span>Tuning</span>
|
||||||
value={settings.difficulty}
|
</label>
|
||||||
onChange={handleDifficultyChange}
|
<select
|
||||||
className="control-select"
|
value={settings.tuningId}
|
||||||
>
|
onChange={handleTuningChange}
|
||||||
{currentInstrumentDef.ranges.map(r => (
|
className="control-select"
|
||||||
<option key={r.id} value={r.id}>{r.label}</option>
|
>
|
||||||
))}
|
{availableTunings.map(id => (
|
||||||
</select>
|
<option key={id} value={id}>{TUNINGS[id].name}</option>
|
||||||
</div>
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
) : <div />} {/* Spacer to maintain grid flow if needed, or omit to let items flow. User asked for specific rows.
|
||||||
|
If I omit, Note Set goes here. Let's omit for "Dense" feel, or keep empty div for strict rows?
|
||||||
|
The request "Top row: instrument and tuning" implies if tuning is missing, maybe just 1 item?
|
||||||
|
I'll output an empty div if I want to FORCE strict placement, but generally flow is better.
|
||||||
|
However, "Second row: note set..." suggests structure.
|
||||||
|
I will use an empty div if tuning is hidden to push Note Set to next row?
|
||||||
|
Actually, grid-template-columns: 1fr 1fr.
|
||||||
|
If Tuning is hidden, Note Set becomes Item 2.
|
||||||
|
I'll forgo complexity and just render what's available. */}
|
||||||
|
|
||||||
{/* Custom Fret Range Inputs */}
|
{/* Row 2, Col 1 */}
|
||||||
{currentInstrumentDef.ranges.find(r => r.id === settings.difficulty)?.type === 'custom_fret' && (
|
|
||||||
<div className="control-group">
|
<div className="control-group">
|
||||||
<label className="control-label">
|
<label className="control-label">
|
||||||
<span>Fret Range</span>
|
<Settings size={18} />
|
||||||
|
<span>Note Set</span>
|
||||||
</label>
|
</label>
|
||||||
<div style={{ display: 'flex', gap: '8px', width: '100%' }}>
|
<select
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: '4px' }}>
|
value={settings.difficulty}
|
||||||
<span style={{ fontSize: '12px', opacity: 0.7 }}>Min</span>
|
onChange={handleDifficultyChange}
|
||||||
<input
|
className="control-select"
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="24"
|
|
||||||
value={settings.customMinFret ?? 0}
|
|
||||||
onChange={(e) => onUpdateSettings({ ...settings, customMinFret: parseInt(e.target.value) || 0 })}
|
|
||||||
className="control-input"
|
|
||||||
style={{ width: '100%', padding: '4px', borderRadius: '4px', border: '1px solid rgba(255,255,255,0.2)', background: 'rgba(0,0,0,0.2)', color: 'white' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
||||||
<span style={{ fontSize: '12px', opacity: 0.7 }}>Max</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="24"
|
|
||||||
value={settings.customMaxFret ?? 12}
|
|
||||||
onChange={(e) => onUpdateSettings({ ...settings, customMaxFret: parseInt(e.target.value) || 0 })}
|
|
||||||
className="control-input"
|
|
||||||
style={{ width: '100%', padding: '4px', borderRadius: '4px', border: '1px solid rgba(255,255,255,0.2)', background: 'rgba(0,0,0,0.2)', color: 'white' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="control-group">
|
|
||||||
<label className="control-label">
|
|
||||||
<span>Key</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={settings.keySignature}
|
|
||||||
onChange={(e) => onUpdateSettings({ ...settings, keySignature: e.target.value })}
|
|
||||||
className="control-select"
|
|
||||||
>
|
|
||||||
<optgroup label="Major Keys">
|
|
||||||
<option value="C">C Major</option>
|
|
||||||
<option value="G">G Major</option>
|
|
||||||
<option value="D">D Major</option>
|
|
||||||
<option value="A">A Major</option>
|
|
||||||
<option value="E">E Major</option>
|
|
||||||
<option value="F">F Major</option>
|
|
||||||
<option value="Bb">Bb Major</option>
|
|
||||||
<option value="Eb">Eb Major</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="Minor Keys">
|
|
||||||
<option value="Am">A Minor</option>
|
|
||||||
<option value="Em">E Minor</option>
|
|
||||||
<option value="Dm">D Minor</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Rhythm Controls */}
|
|
||||||
<div className="control-group rhythm-group" style={{ borderTop: '1px solid rgba(255,255,255,0.1)', paddingTop: '12px', marginTop: '12px', width: '100%' }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '12px', marginBottom: '8px' }}>
|
|
||||||
<label className="control-label" style={{ marginBottom: 0 }}>
|
|
||||||
<span>Metronome / Timer</span>
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
className={`switch-button ${settings.rhythm.active ? 'active' : ''}`}
|
|
||||||
onClick={() => updateRhythm({ active: !settings.rhythm.active })}
|
|
||||||
title={settings.rhythm.active ? "Turn Off" : "Turn On"}
|
|
||||||
>
|
>
|
||||||
<div className="switch-thumb" />
|
{currentInstrumentDef.ranges.map(r => (
|
||||||
</button>
|
<option key={r.id} value={r.id}>{r.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{settings.rhythm.active && (
|
{/* Row 2, Col 2 */}
|
||||||
<div className="rhythm-details" style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div className="control-group">
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<label className="control-label">
|
||||||
<button
|
<span>Key signature</span>
|
||||||
className={`control-button small ${settings.rhythm.mode === 'bpm' ? 'active' : ''}`}
|
</label>
|
||||||
onClick={() => updateRhythm({ mode: 'bpm' })}
|
<select
|
||||||
>BPM</button>
|
value={settings.keySignature}
|
||||||
<button
|
onChange={(e) => onUpdateSettings({ ...settings, keySignature: e.target.value })}
|
||||||
className={`control-button small ${settings.rhythm.mode === 'seconds' ? 'active' : ''}`}
|
className="control-select"
|
||||||
onClick={() => updateRhythm({ mode: 'seconds' })}
|
>
|
||||||
>Timer</button>
|
<optgroup label="Major Keys">
|
||||||
</div>
|
<option value="C">C Major</option>
|
||||||
|
<option value="G">G Major</option>
|
||||||
|
<option value="D">D Major</option>
|
||||||
|
<option value="A">A Major</option>
|
||||||
|
<option value="E">E Major</option>
|
||||||
|
<option value="F">F Major</option>
|
||||||
|
<option value="Bb">Bb Major</option>
|
||||||
|
<option value="Eb">Eb Major</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Minor Keys">
|
||||||
|
<option value="Am">A Minor</option>
|
||||||
|
<option value="Em">E Minor</option>
|
||||||
|
<option value="Dm">D Minor</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{settings.rhythm.mode === 'bpm' ? (
|
{/* Row 3: Fret Range (Full Width) */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
{currentInstrumentDef.ranges.find(r => r.id === settings.difficulty)?.type === 'custom_fret' && (
|
||||||
<span style={{ fontSize: '12px', minWidth: '40px' }}>{settings.rhythm.bpm} BPM</span>
|
<div className="control-group" style={{ gridColumn: '1 / -1' }}>
|
||||||
|
<label className="control-label">
|
||||||
|
<span>Fret Range</span>
|
||||||
|
</label>
|
||||||
|
<div style={{ display: 'flex', gap: '8px', width: '100%' }}>
|
||||||
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
|
<span style={{ fontSize: '12px', opacity: 0.7 }}>Min</span>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="number"
|
||||||
min="30"
|
min="0"
|
||||||
max="240"
|
max="24"
|
||||||
step="5"
|
value={settings.customMinFret ?? 0}
|
||||||
value={settings.rhythm.bpm}
|
onChange={(e) => onUpdateSettings({ ...settings, customMinFret: parseInt(e.target.value) || 0 })}
|
||||||
onChange={(e) => updateRhythm({ bpm: Number(e.target.value) })}
|
className="control-input"
|
||||||
style={{ flex: 1 }}
|
style={{ width: '100%', padding: '4px', borderRadius: '4px', border: '1px solid rgba(255,255,255,0.2)', background: 'rgba(0,0,0,0.2)', color: 'white' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<span style={{ fontSize: '12px', opacity: 0.7 }}>Max</span>
|
||||||
<span style={{ fontSize: '12px', minWidth: '40px' }}>{settings.rhythm.seconds}s</span>
|
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="number"
|
||||||
min="1"
|
min="0"
|
||||||
max="60"
|
max="24"
|
||||||
step="1"
|
value={settings.customMaxFret ?? 12}
|
||||||
value={settings.rhythm.seconds}
|
onChange={(e) => onUpdateSettings({ ...settings, customMaxFret: parseInt(e.target.value) || 0 })}
|
||||||
onChange={(e) => updateRhythm({ seconds: Number(e.target.value) })}
|
className="control-input"
|
||||||
style={{ flex: 1 }}
|
style={{ width: '100%', padding: '4px', borderRadius: '4px', border: '1px solid rgba(255,255,255,0.2)', background: 'rgba(0,0,0,0.2)', color: 'white' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<label style={{ fontSize: '12px', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={settings.rhythm.autoAdvance}
|
|
||||||
onChange={(e) => updateRhythm({ autoAdvance: e.target.checked })}
|
|
||||||
/>
|
|
||||||
Auto-Adv
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label style={{ fontSize: '12px', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={settings.rhythm.sound}
|
|
||||||
onChange={(e) => updateRhythm({ sound: e.target.checked })}
|
|
||||||
/>
|
|
||||||
Sound
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="control-group" style={{ borderTop: '1px solid rgba(255,255,255,0.1)', paddingTop: '12px', marginTop: '12px', width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<label className="control-label" style={{ marginBottom: 0 }}>
|
{/* Bottom Section: Tools Grid (3 Columns) */}
|
||||||
<Gauge size={18} />
|
<div className="tools-grid">
|
||||||
<span>Tuning Meter</span>
|
{/* Tool 1: Tuner */}
|
||||||
</label>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', border: '1px solid rgba(128,128,128,0.2)', padding: '12px', borderRadius: '8px' }}>
|
||||||
<button
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
className={`switch-button ${settings.showTuningMeter ? 'active' : ''}`}
|
<label className="control-label" style={{ marginBottom: 0 }}>
|
||||||
onClick={() => onUpdateSettings({ ...settings, showTuningMeter: !settings.showTuningMeter })}
|
<Gauge size={18} />
|
||||||
title={settings.showTuningMeter ? "Hide Tuner" : "Show Tuner"}
|
<span>Tuner</span>
|
||||||
>
|
</label>
|
||||||
<div className="switch-thumb" />
|
<button
|
||||||
</button>
|
className={`switch-button ${settings.showTuningMeter ? 'active' : ''}`}
|
||||||
|
onClick={() => onUpdateSettings({ ...settings, showTuningMeter: !settings.showTuningMeter })}
|
||||||
|
title={settings.showTuningMeter ? "Hide Tuner" : "Show Tuner"}
|
||||||
|
>
|
||||||
|
<div className="switch-thumb" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tool 2: Metronome */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', border: '1px solid rgba(128,128,128,0.2)', padding: '12px', borderRadius: '8px' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<label className="control-label" style={{ marginBottom: 0 }}>
|
||||||
|
<span>Metronome</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
className={`switch-button ${settings.rhythm.active ? 'active' : ''}`}
|
||||||
|
onClick={() => updateRhythm({ active: !settings.rhythm.active })}
|
||||||
|
title={settings.rhythm.active ? "Turn Off" : "Turn On"}
|
||||||
|
>
|
||||||
|
<div className="switch-thumb" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{settings.rhythm.active && (
|
||||||
|
<div className="rhythm-details" style={{ display: 'flex', flexDirection: 'column', gap: '6px', paddingTop: '8px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
|
||||||
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
|
<button
|
||||||
|
className={`control-button small ${settings.rhythm.mode === 'bpm' ? 'active' : ''}`}
|
||||||
|
onClick={() => updateRhythm({ mode: 'bpm' })}
|
||||||
|
style={{ flex: 1, fontSize: '10px' }}
|
||||||
|
>BPM</button>
|
||||||
|
<button
|
||||||
|
className={`control-button small ${settings.rhythm.mode === 'seconds' ? 'active' : ''}`}
|
||||||
|
onClick={() => updateRhythm({ mode: 'seconds' })}
|
||||||
|
style={{ flex: 1, fontSize: '10px' }}
|
||||||
|
>Timer</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{settings.rhythm.mode === 'bpm' ? (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
|
<span style={{ fontSize: '12px', minWidth: '32px' }}>{settings.rhythm.bpm}</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="30"
|
||||||
|
max="240"
|
||||||
|
step="5"
|
||||||
|
value={settings.rhythm.bpm}
|
||||||
|
onChange={(e) => updateRhythm({ bpm: Number(e.target.value) })}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
|
<span style={{ fontSize: '12px', minWidth: '32px' }}>{settings.rhythm.seconds}s</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="60"
|
||||||
|
step="1"
|
||||||
|
value={settings.rhythm.seconds}
|
||||||
|
onChange={(e) => updateRhythm({ seconds: Number(e.target.value) })}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<label style={{ fontSize: '10px', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.rhythm.autoAdvance}
|
||||||
|
onChange={(e) => updateRhythm({ autoAdvance: e.target.checked })}
|
||||||
|
/>
|
||||||
|
Auto
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style={{ fontSize: '10px', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.rhythm.sound}
|
||||||
|
onChange={(e) => updateRhythm({ sound: e.target.checked })}
|
||||||
|
/>
|
||||||
|
Sound
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tool 3: Auto-play */}
|
||||||
|
{settings.gameMode === 'sight_reading' ? (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', border: '1px solid rgba(128,128,128,0.2)', padding: '12px', borderRadius: '8px' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<label className="control-label" style={{ marginBottom: 0, textTransform: 'none' }}>
|
||||||
|
<span>Auto-play</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
className={`switch-button ${settings.autoPlaySightReading ? 'active' : ''}`}
|
||||||
|
onClick={() => onUpdateSettings({ ...settings, autoPlaySightReading: !settings.autoPlaySightReading })}
|
||||||
|
style={{ transform: 'scale(1)' }} /* Reset scale for consistency */
|
||||||
|
>
|
||||||
|
<div className="switch-thumb" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div /> /* Empty placeholder for grid 3rd column if not sight reading? Or just 2 cols? */
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,11 +15,13 @@
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #646cff;
|
color: inherit;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #535bf2;
|
text-decoration: underline;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -46,12 +48,14 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.25s;
|
transition: border-color 0.25s;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
border-color: #646cff;
|
border-color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus,
|
button:focus,
|
||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
outline: 4px auto #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
@@ -59,10 +63,12 @@ button:focus-visible {
|
|||||||
color: #213547;
|
color: #213547;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #747bff;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
/* Warm paper-like bg */
|
/* Warm paper-like bg */
|
||||||
--color-surface: hsl(0, 0%, 100%);
|
--color-surface: hsl(0, 0%, 100%);
|
||||||
|
|
||||||
/* Deep Classic Red */
|
/* Neutral / Black Accent */
|
||||||
--color-primary: hsl(var(--hue-primary), 65%, 40%);
|
--color-primary: hsl(0, 0%, 20%);
|
||||||
--color-primary-dark: hsl(var(--hue-primary), 65%, 30%);
|
--color-primary-dark: hsl(0, 0%, 0%);
|
||||||
|
|
||||||
--color-text-main: hsl(var(--hue-text), 10%, 20%);
|
--color-text-main: hsl(var(--hue-text), 10%, 20%);
|
||||||
--color-text-muted: hsl(var(--hue-text), 5%, 60%);
|
--color-text-muted: hsl(var(--hue-text), 5%, 60%);
|
||||||
@@ -58,6 +58,9 @@
|
|||||||
--color-surface: hsl(30, 20%, 15%);
|
--color-surface: hsl(30, 20%, 15%);
|
||||||
--color-text-main: hsl(30, 10%, 90%);
|
--color-text-main: hsl(30, 10%, 90%);
|
||||||
--color-text-muted: hsl(30, 10%, 60%);
|
--color-text-muted: hsl(30, 10%, 60%);
|
||||||
|
/* In dark mode, 'primary' (black) matches background too much, so we invert or use white */
|
||||||
|
--color-primary: hsl(0, 0%, 90%);
|
||||||
|
--color-primary-dark: hsl(0, 0%, 100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user