From 6e60ced477ac82c9730578e207f1416f89d86a28 Mon Sep 17 00:00:00 2001 From: Jens Mohrmann Date: Sun, 13 Sep 2026 18:52:27 +0200 Subject: [PATCH] Dark mode contrast, tuner display and misc UI fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- src/App.css | 30 ++++++++++-- src/components/Controls.tsx | 26 ++++------ src/components/PianoKeys.tsx | 2 +- src/components/SheetMusic.tsx | 51 ++++++++++++++++---- src/components/TuningMeter.css | 81 ++++++++++++++++++++------------ src/components/TuningMeter.tsx | 48 ++++++++++--------- src/context/SettingsContext.tsx | 36 -------------- src/context/SettingsProvider.tsx | 27 +++++++++++ src/context/settings-context.ts | 10 ++++ src/context/useSettings.ts | 10 ++++ src/main.tsx | 2 +- src/music/NoteUtils.ts | 4 +- src/styles/variables.css | 6 +++ 13 files changed, 212 insertions(+), 121 deletions(-) delete mode 100644 src/context/SettingsContext.tsx create mode 100644 src/context/SettingsProvider.tsx create mode 100644 src/context/settings-context.ts create mode 100644 src/context/useSettings.ts diff --git a/src/App.css b/src/App.css index cf606ad..aa0ce02 100644 --- a/src/App.css +++ b/src/App.css @@ -203,7 +203,18 @@ button { .pitch-readout { display: flex; flex-direction: column; + justify-content: center; min-width: 100px; + /* Fixed height: the note display (3rem note + Hz line ≈ 70px) and the + "Listening..." placeholder differ in height — without a reserved height + everything below this element jumps up and down whenever a note + starts or stops sounding. */ + height: 70px; +} + +.pitch-readout .placeholder { + font-size: 0.9rem; + color: var(--color-text-muted); } .detected-note { @@ -285,10 +296,18 @@ button { .control-select { padding: var(--spacing-sm); border-radius: var(--radius-sm); - border: 1px solid #ddd; + border: 1px solid color-mix(in srgb, var(--color-text-main) 20%, transparent); font-family: inherit; font-size: 0.9rem; - background-color: var(--color-bg); + background-color: var(--color-surface); + color: var(--color-text-main); +} + +.control-input { + color: var(--color-text-main); + background-color: var(--color-surface); + border: 1px solid color-mix(in srgb, var(--color-text-main) 20%, transparent); + border-radius: var(--radius-sm); } .control-button { @@ -330,7 +349,9 @@ button { width: 44px; height: 24px; border-radius: 99px; - background-color: #ddd; + /* Themed track: dark in dark mode, gray in light mode + (was hardcoded #ddd — glowed in dark mode) */ + background-color: color-mix(in srgb, var(--color-text-main) 25%, transparent); position: relative; cursor: pointer; transition: background-color 0.2s; @@ -347,7 +368,8 @@ button { .switch-thumb { width: 18px; height: 18px; - background-color: white; + /* Themed: contrasts with both the off-track and the active track */ + background-color: var(--color-surface); border-radius: 50%; position: absolute; left: 3px; diff --git a/src/components/Controls.tsx b/src/components/Controls.tsx index f28d5a4..229f9b7 100644 --- a/src/components/Controls.tsx +++ b/src/components/Controls.tsx @@ -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 = ({ 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' }} />
@@ -179,7 +179,7 @@ export const Controls: React.FC = ({ 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' }} />
@@ -204,14 +204,12 @@ export const Controls: React.FC = ({ currentPitch }) => {
- {settings.showTuningMeter && currentPitch && ( -
- -
- )} - {settings.showTuningMeter && !currentPitch && ( -
- Listening... + {settings.showTuningMeter && ( +
+
)}
@@ -266,11 +264,7 @@ export const Controls: React.FC = ({ 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' }} />