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
+9
View File
@@ -18,6 +18,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Added
### Settings — OpenDyslexic font option for dyslexic readers
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#507](https://github.com/Psychotoxical/psysonic/pull/507)**
* Next step on the accessibility track after the colour-side work (WCAG contrast audits across every theme, dedicated colour-vision-deficiency theme variants for protanopia / deuteranopia / tritanopia): a dyslexia-friendly **OpenDyslexic** font option in the existing font picker.
* OpenDyslexic uses a heavier weighted baseline and asymmetric glyph shapes — `b`/`d`, `p`/`q` never mirror, italics are differentiated forms rather than slanted regulars — which many dyslexic readers find easier to track than a typical sans.
* Bundled locally via `@fontsource/opendyslexic` (SIL OFL, freely redistributable). No CDN dependency. The Settings picker grew an optional **subtitle** field on font entries so the OpenDyslexic row carries a "dyslexia-friendly · no RU/ZH support" hint without cluttering the other 14 fonts.
* **Latin + Latin-extended only.** Cyrillic and CJK locales (RU, ZH) fall back to the system font when OpenDyslexic is selected; the subtitle calls that out upfront. Subtitle text is translated in all 8 locales.
### Lossless Albums — rail on Home + dedicated page + sidebar entry
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#506](https://github.com/Psychotoxical/psysonic/pull/506)**
+1 -1
View File
@@ -1,3 +1,3 @@
{
"npmDepsHash": "sha256-3zwfk7gxSYXoiOwNyPHP+K9P7yAksGKXQvwtvic/IdQ="
"npmDepsHash": "sha256-5u9O1O7q936/Ik7/X++pK2Bg65+GYh0RdL4YVvasNSQ="
}
+10
View File
@@ -22,6 +22,7 @@
"@fontsource-variable/rubik": "^5.2.8",
"@fontsource-variable/space-grotesk": "^5.2.10",
"@fontsource-variable/unbounded": "^5.2.8",
"@fontsource/opendyslexic": "^5.2.5",
"@tanstack/react-virtual": "^3.13.24",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.7.1",
@@ -668,6 +669,15 @@
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/opendyslexic": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/@fontsource/opendyslexic/-/opendyslexic-5.2.5.tgz",
"integrity": "sha512-NNS9aaPQx2TlaTvb3vTEjw3xz8lKj23mBc+6rM00mSNFDygdoll0/nLMHFtDKKrBT6sMfY6TFFPOR0D9ktdspg==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+1
View File
@@ -26,6 +26,7 @@
"@fontsource-variable/rubik": "^5.2.8",
"@fontsource-variable/space-grotesk": "^5.2.10",
"@fontsource-variable/unbounded": "^5.2.8",
"@fontsource/opendyslexic": "^5.2.5",
"@tanstack/react-virtual": "^3.13.24",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.7.1",
+1
View File
@@ -587,6 +587,7 @@ export const deTranslation = {
languageNb: 'Norwegisch',
languageRu: 'Russisch',
font: 'Schriftart',
fontHintOpenDyslexic: 'Legasthenie-freundlich · keine RU/ZH-Unterstützung',
theme: 'Design',
appearance: 'Darstellung',
servers: 'Server',
+1
View File
@@ -590,6 +590,7 @@ export const enTranslation = {
languageRu: 'Russian',
languageEs: 'Spanish',
font: 'Font',
fontHintOpenDyslexic: 'Dyslexia-friendly · no RU/ZH support',
theme: 'Theme',
appearance: 'Appearance',
servers: 'Servers',
+1
View File
@@ -580,6 +580,7 @@ export const esTranslation = {
languageRu: 'Ruso',
languageEs: 'Español',
font: 'Fuente',
fontHintOpenDyslexic: 'Compatible con dislexia · sin soporte RU/ZH',
theme: 'Tema',
appearance: 'Apariencia',
servers: 'Servidores',
+1
View File
@@ -577,6 +577,7 @@ export const frTranslation = {
languageNb: 'Norvégien',
languageRu: 'Russe',
font: 'Police',
fontHintOpenDyslexic: 'Adapté à la dyslexie · sans prise en charge RU/ZH',
theme: 'Thème',
appearance: 'Apparence',
servers: 'Serveurs',
+1
View File
@@ -577,6 +577,7 @@ export const nbTranslation = {
languageNb: 'Norsk',
languageRu: 'Russisk',
font: 'Skrifttype',
fontHintOpenDyslexic: 'Dyslexivennlig · ingen RU/ZH-støtte',
theme: 'Tema',
appearance: 'Utseende',
servers: 'Tjenere',
+1
View File
@@ -576,6 +576,7 @@ export const nlTranslation = {
languageNb: 'Noors',
languageRu: 'Russisch',
font: 'Lettertype',
fontHintOpenDyslexic: 'Dyslexie-vriendelijk · geen RU/ZH-ondersteuning',
theme: 'Thema',
appearance: 'Weergave',
servers: 'Servers',
+1
View File
@@ -611,6 +611,7 @@ export const ruTranslation = {
languageRu: 'Русский',
languageRu2: 'Русский 2',
font: 'Шрифт',
fontHintOpenDyslexic: 'Подходит для дислексии · без поддержки RU/ZH',
theme: 'Тема',
appearance: 'Оформление',
servers: 'Серверы',
+1
View File
@@ -571,6 +571,7 @@ export const zhTranslation = {
languageNb: '挪威',
languageRu: '俄语',
font: '字体',
fontHintOpenDyslexic: '阅读障碍友好 · 不支持俄文/中文',
theme: '主题',
appearance: '外观',
servers: '服务器',
+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>
+1 -1
View File
@@ -1,7 +1,7 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type FontId = 'inter' | 'outfit' | 'dm-sans' | 'nunito' | 'rubik' | 'space-grotesk' | 'figtree' | 'manrope' | 'plus-jakarta-sans' | 'lexend' | 'geist' | 'jetbrains-mono' | 'golos-text' | 'unbounded';
export type FontId = 'inter' | 'outfit' | 'dm-sans' | 'nunito' | 'rubik' | 'space-grotesk' | 'figtree' | 'manrope' | 'plus-jakarta-sans' | 'lexend' | 'geist' | 'jetbrains-mono' | 'golos-text' | 'unbounded' | 'opendyslexic';
interface FontState {
font: FontId;
+11
View File
@@ -24,6 +24,12 @@
@import '@fontsource-variable/jetbrains-mono/wght-italic.css';
@import '@fontsource-variable/golos-text';
@import '@fontsource-variable/unbounded';
/* OpenDyslexic non-variable font with discrete weights/styles. Latin only,
no Cyrillic / no CJK. Selected via Settings as a dyslexia-friendly option. */
@import '@fontsource/opendyslexic/400.css';
@import '@fontsource/opendyslexic/700.css';
@import '@fontsource/opendyslexic/400-italic.css';
@import '@fontsource/opendyslexic/700-italic.css';
/* ─── Catppuccin Mocha Variables ─── */
:root,
@@ -6516,6 +6522,11 @@ input[type="range"]:hover::-webkit-slider-thumb {
--font-display: 'Unbounded Variable', sans-serif;
}
[data-font='opendyslexic'] {
--font-sans: 'OpenDyslexic', system-ui, sans-serif;
--font-display: 'OpenDyslexic', sans-serif;
}
/*
Middle Earth Epic Fantasy / Cinematic
*/