feat(settings): OpenDyslexic font option for dyslexic readers (#507)

* feat(settings): OpenDyslexic font option for dyslexic readers

Next step on the accessibility track. The first pass was on the colour
side — WCAG contrast audits across every theme and dedicated colour-
vision-deficiency variants for the protanopia / deuteranopia / tritan-
opia palettes. Typography is the other axis: some users with dyslexia
find a font with a heavier weighted baseline and asymmetric glyph
shapes (b/d, p/q never mirror, italic forms differentiated rather
than slanted-regular) easier to track than a typical sans.

Adds OpenDyslexic to the existing Fontsource font picker. SIL OFL
licensed, freely redistributable, and the de-facto open-source
standard for this use case. Non-variable axis, ships as four discrete
weight/style files (regular, bold, italic, bold-italic) — the Settings
picker grew an optional `hint` field on font entries so this one row
can carry a "dyslexia-friendly · no RU/ZH support" subtitle without
bloating the other 14 entries.

Latin + Latin-extended only. Cyrillic and CJK locales (RU, ZH) fall
back to the system font when this is selected; the subtitle calls out
that limitation upfront. i18n: hint string in all 8 locales
(settings.fontHintOpenDyslexic).

Accessibility is intentional product positioning here — it's an
underserved corner of the Subsonic-client ecosystem.

* chore(nix): sync npmDepsHash with package-lock.json

* docs: changelog entry for PR #507

Logs the OpenDyslexic font option in v1.46.0 "## Added".

* docs(settings): contributor entry for PR #507

Adds the OpenDyslexic accessibility bullet to Psychotoxical's
contributions list.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Frank Stellmacher
2026-05-07 22:15:38 +02:00
committed by GitHub
parent bd56177e2c
commit f520f7951a
15 changed files with 56 additions and 5 deletions
+15 -3
View File
@@ -373,6 +373,7 @@ const CONTRIBUTORS = [
'Home: "Because you listened" recommendation rail — Last.fm-anchored similar-artist surfacing with round-robin anchor rotation per server (PR #489)',
'Song Info: absolute file path on Navidrome via native /api/song/{id} — Subsonic only ever returned a relative path (or none on Navidrome), the native endpoint surfaces the full server-side location (PR #504)',
'Home: Lossless Albums rail + dedicated /lossless-albums page with infinite scroll and header parity (selection mode, enqueue, offline, download ZIPs), streaming load via per-fetch onProgress, sidebar entry default visible, detection via Navidrome native bit_depth-sorted song cursor with always-lossless suffix allowlist (PR #506)',
'Accessibility: OpenDyslexic font option in the Settings picker — bundled locally via @fontsource/opendyslexic, asymmetric glyph shapes for easier b/d, p/q tracking, Latin-only with translated subtitle in all 8 locales calling out the dyslexia-friendly intent and the Cyrillic/CJK fallback (PR #507)',
],
},
] as const;
@@ -3832,6 +3833,7 @@ export default function Settings() {
{ id: 'jetbrains-mono', label: 'JetBrains Mono' },
{ id: 'golos-text', label: 'Golos Text' },
{ id: 'unbounded', label: 'Unbounded' },
{ id: 'opendyslexic', label: 'OpenDyslexic' },
] as { id: FontId; label: string }[]).find(f => f.id === fontStore.font)?.label ?? fontStore.font
}</span>
<ChevronDown size={14} style={{ color: 'var(--text-muted)', transform: fontPickerOpen ? 'rotate(180deg)' : 'none', transition: 'transform 0.2s' }} />
@@ -3854,15 +3856,25 @@ export default function Settings() {
{ id: 'jetbrains-mono', label: 'JetBrains Mono', stack: "'JetBrains Mono Variable', monospace" },
{ id: 'golos-text', label: 'Golos Text', stack: "'Golos Text Variable', sans-serif" },
{ id: 'unbounded', label: 'Unbounded', stack: "'Unbounded Variable', sans-serif" },
] as { id: FontId; label: string; stack: string }[]
{ id: 'opendyslexic', label: 'OpenDyslexic', stack: "'OpenDyslexic', sans-serif", hint: t('settings.fontHintOpenDyslexic') },
] as { id: FontId; label: string; stack: string; hint?: string }[]
).map(f => (
<button
key={f.id}
className={`btn ${fontStore.font === f.id ? 'btn-primary' : 'btn-ghost'}`}
style={{ justifyContent: 'flex-start', fontFamily: f.stack }}
style={{
justifyContent: 'flex-start',
fontFamily: f.stack,
...(f.hint ? { flexDirection: 'column', alignItems: 'flex-start', gap: '2px', paddingTop: '8px', paddingBottom: '8px' } : null),
}}
onClick={() => { fontStore.setFont(f.id); setFontPickerOpen(false); }}
>
{f.label}
<span>{f.label}</span>
{f.hint && (
<span style={{ fontSize: 11, color: 'var(--text-muted)', fontFamily: 'var(--font-sans)' }}>
{f.hint}
</span>
)}
</button>
))}
</div>