Dark mode contrast, tuner display and misc UI fixes

- color-scheme per theme so native controls (selects, checkboxes, number
  inputs, sliders) render dark-native instead of black-on-black.
- Themed .control-select text color; new .control-input class replaces
  hardcoded white-on-black inline input styles.
- Tuner widget stays mounted when enabled: idle state (dimmed, needle parked,
  '—') instead of appearing/disappearing. Fully theme-var based (was
  hardcoded white — invisible in light mode).
- Remove CSS transitions on per-frame values (tuner needle, level meter):
  transitions interrupted 60x/sec are left stuck by WebKit/Safari; the
  display EMA from the detection loop replaces them.
- VexFlow notation: resolve CSS var() to concrete colors (SVG attributes
  can't resolve var(), silently fell back to black) and override VexFlow's
  hardcoded #444 ledger-line default; re-render on theme changes
  (useLayoutEffect so children read the updated attribute in time).
- Themed switch toggles (were hardcoded #ddd / white — glowed in dark mode).
- Fixed-height pitch readout so the layout stops jumping when a note starts
  or stops sounding.
- Split SettingsContext into context/Provider/hook files (fast-refresh);
  memoized provider functions; typed webkitAudioContext; prefer-const;
  removed 'as any' casts. ESLint now passes clean.
This commit is contained in:
2026-09-13 18:52:27 +02:00
parent 079d574b66
commit 6e60ced477
13 changed files with 212 additions and 121 deletions

View File

@@ -4,7 +4,7 @@ import { INSTRUMENT_DEFINITIONS } from '../music/InstrumentConfigs';
import { getTempoMarking } from '../music/TempoMarkings';
import { TuningMeter } from './TuningMeter';
import { useSettings } from '../context/SettingsContext';
import { useSettings } from '../context/useSettings';
import { type Difficulty, type RhythmSettings } from '../types/SettingsTypes';
@@ -167,7 +167,7 @@ export const Controls: React.FC<ControlsProps> = ({ currentPitch }) => {
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' }}
style={{ width: '100%', padding: '4px' }}
/>
</div>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: '4px' }}>
@@ -179,7 +179,7 @@ export const Controls: React.FC<ControlsProps> = ({ currentPitch }) => {
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' }}
style={{ width: '100%', padding: '4px' }}
/>
</div>
</div>
@@ -204,14 +204,12 @@ export const Controls: React.FC<ControlsProps> = ({ currentPitch }) => {
<div className="switch-thumb" />
</button>
</div>
{settings.showTuningMeter && currentPitch && (
<div style={{ paddingTop: '8px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
<TuningMeter cents={currentPitch.cents} noteName={currentPitch.note} />
</div>
)}
{settings.showTuningMeter && !currentPitch && (
<div style={{ paddingTop: '8px', borderTop: '1px solid rgba(255,255,255,0.1)', fontSize: '11px', opacity: 0.5, textAlign: 'center' }}>
Listening...
{settings.showTuningMeter && (
<div style={{ paddingTop: '8px', borderTop: '1px solid color-mix(in srgb, var(--color-text-main) 10%, transparent)' }}>
<TuningMeter
cents={currentPitch ? currentPitch.cents : null}
noteName={currentPitch ? currentPitch.note : null}
/>
</div>
)}
</div>
@@ -266,11 +264,7 @@ export const Controls: React.FC<ControlsProps> = ({ currentPitch }) => {
style={{
width: '48px',
textAlign: 'center',
padding: '2px',
borderRadius: '4px',
border: '1px solid rgba(255,255,255,0.2)',
background: 'rgba(0,0,0,0.2)',
color: 'white'
padding: '2px'
}}
/>
<button