From f520f7951a56fc6375439f61ed8e01eb0f3f5b9c Mon Sep 17 00:00:00 2001 From: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com> Date: Thu, 7 May 2026 22:15:38 +0200 Subject: [PATCH] feat(settings): OpenDyslexic font option for dyslexic readers (#507) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> --- CHANGELOG.md | 9 +++++++++ nix/upstream-sources.json | 2 +- package-lock.json | 10 ++++++++++ package.json | 1 + src/locales/de.ts | 1 + src/locales/en.ts | 1 + src/locales/es.ts | 1 + src/locales/fr.ts | 1 + src/locales/nb.ts | 1 + src/locales/nl.ts | 1 + src/locales/ru.ts | 1 + src/locales/zh.ts | 1 + src/pages/Settings.tsx | 18 +++++++++++++++--- src/store/fontStore.ts | 2 +- src/styles/theme.css | 11 +++++++++++ 15 files changed, 56 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6bbbded..573ca3f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)** diff --git a/nix/upstream-sources.json b/nix/upstream-sources.json index 3eff282e..9c19a1f5 100644 --- a/nix/upstream-sources.json +++ b/nix/upstream-sources.json @@ -1,3 +1,3 @@ { - "npmDepsHash": "sha256-3zwfk7gxSYXoiOwNyPHP+K9P7yAksGKXQvwtvic/IdQ=" + "npmDepsHash": "sha256-5u9O1O7q936/Ik7/X++pK2Bg65+GYh0RdL4YVvasNSQ=" } diff --git a/package-lock.json b/package-lock.json index 7b26491d..36221c48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 52dd16d6..acf8a184 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/locales/de.ts b/src/locales/de.ts index 38a2def7..73af0b74 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -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', diff --git a/src/locales/en.ts b/src/locales/en.ts index 85539d50..5d27075a 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -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', diff --git a/src/locales/es.ts b/src/locales/es.ts index 6b70426c..2dd6447c 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -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', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index c22fca92..34d8e6b0 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -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', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index 4e247261..79690cdc 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -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', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index cd25392a..745c057a 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -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', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 1f2507f9..96acb8f4 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -611,6 +611,7 @@ export const ruTranslation = { languageRu: 'Русский', languageRu2: 'Русский 2', font: 'Шрифт', + fontHintOpenDyslexic: 'Подходит для дислексии · без поддержки RU/ZH', theme: 'Тема', appearance: 'Оформление', servers: 'Серверы', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index ffcdab2e..84065055 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -571,6 +571,7 @@ export const zhTranslation = { languageNb: '挪威', languageRu: '俄语', font: '字体', + fontHintOpenDyslexic: '阅读障碍友好 · 不支持俄文/中文', theme: '主题', appearance: '外观', servers: '服务器', diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index bb2078c6..953cd5ac 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -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 } @@ -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 => ( ))} diff --git a/src/store/fontStore.ts b/src/store/fontStore.ts index 00042820..18718bcc 100644 --- a/src/store/fontStore.ts +++ b/src/store/fontStore.ts @@ -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; diff --git a/src/styles/theme.css b/src/styles/theme.css index 2396273d..e6ddb11b 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -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 ───────────────────────────────────────────────────────────── */