mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
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:
committed by
GitHub
parent
bd56177e2c
commit
f520f7951a
@@ -18,6 +18,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Added
|
## 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
|
### Lossless Albums — rail on Home + dedicated page + sidebar entry
|
||||||
|
|
||||||
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#506](https://github.com/Psychotoxical/psysonic/pull/506)**
|
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#506](https://github.com/Psychotoxical/psysonic/pull/506)**
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"npmDepsHash": "sha256-3zwfk7gxSYXoiOwNyPHP+K9P7yAksGKXQvwtvic/IdQ="
|
"npmDepsHash": "sha256-5u9O1O7q936/Ik7/X++pK2Bg65+GYh0RdL4YVvasNSQ="
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+10
@@ -22,6 +22,7 @@
|
|||||||
"@fontsource-variable/rubik": "^5.2.8",
|
"@fontsource-variable/rubik": "^5.2.8",
|
||||||
"@fontsource-variable/space-grotesk": "^5.2.10",
|
"@fontsource-variable/space-grotesk": "^5.2.10",
|
||||||
"@fontsource-variable/unbounded": "^5.2.8",
|
"@fontsource-variable/unbounded": "^5.2.8",
|
||||||
|
"@fontsource/opendyslexic": "^5.2.5",
|
||||||
"@tanstack/react-virtual": "^3.13.24",
|
"@tanstack/react-virtual": "^3.13.24",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||||
@@ -668,6 +669,15 @@
|
|||||||
"url": "https://github.com/sponsors/ayuhito"
|
"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": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"@fontsource-variable/rubik": "^5.2.8",
|
"@fontsource-variable/rubik": "^5.2.8",
|
||||||
"@fontsource-variable/space-grotesk": "^5.2.10",
|
"@fontsource-variable/space-grotesk": "^5.2.10",
|
||||||
"@fontsource-variable/unbounded": "^5.2.8",
|
"@fontsource-variable/unbounded": "^5.2.8",
|
||||||
|
"@fontsource/opendyslexic": "^5.2.5",
|
||||||
"@tanstack/react-virtual": "^3.13.24",
|
"@tanstack/react-virtual": "^3.13.24",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||||
|
|||||||
@@ -587,6 +587,7 @@ export const deTranslation = {
|
|||||||
languageNb: 'Norwegisch',
|
languageNb: 'Norwegisch',
|
||||||
languageRu: 'Russisch',
|
languageRu: 'Russisch',
|
||||||
font: 'Schriftart',
|
font: 'Schriftart',
|
||||||
|
fontHintOpenDyslexic: 'Legasthenie-freundlich · keine RU/ZH-Unterstützung',
|
||||||
theme: 'Design',
|
theme: 'Design',
|
||||||
appearance: 'Darstellung',
|
appearance: 'Darstellung',
|
||||||
servers: 'Server',
|
servers: 'Server',
|
||||||
|
|||||||
@@ -590,6 +590,7 @@ export const enTranslation = {
|
|||||||
languageRu: 'Russian',
|
languageRu: 'Russian',
|
||||||
languageEs: 'Spanish',
|
languageEs: 'Spanish',
|
||||||
font: 'Font',
|
font: 'Font',
|
||||||
|
fontHintOpenDyslexic: 'Dyslexia-friendly · no RU/ZH support',
|
||||||
theme: 'Theme',
|
theme: 'Theme',
|
||||||
appearance: 'Appearance',
|
appearance: 'Appearance',
|
||||||
servers: 'Servers',
|
servers: 'Servers',
|
||||||
|
|||||||
@@ -580,6 +580,7 @@ export const esTranslation = {
|
|||||||
languageRu: 'Ruso',
|
languageRu: 'Ruso',
|
||||||
languageEs: 'Español',
|
languageEs: 'Español',
|
||||||
font: 'Fuente',
|
font: 'Fuente',
|
||||||
|
fontHintOpenDyslexic: 'Compatible con dislexia · sin soporte RU/ZH',
|
||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
appearance: 'Apariencia',
|
appearance: 'Apariencia',
|
||||||
servers: 'Servidores',
|
servers: 'Servidores',
|
||||||
|
|||||||
@@ -577,6 +577,7 @@ export const frTranslation = {
|
|||||||
languageNb: 'Norvégien',
|
languageNb: 'Norvégien',
|
||||||
languageRu: 'Russe',
|
languageRu: 'Russe',
|
||||||
font: 'Police',
|
font: 'Police',
|
||||||
|
fontHintOpenDyslexic: 'Adapté à la dyslexie · sans prise en charge RU/ZH',
|
||||||
theme: 'Thème',
|
theme: 'Thème',
|
||||||
appearance: 'Apparence',
|
appearance: 'Apparence',
|
||||||
servers: 'Serveurs',
|
servers: 'Serveurs',
|
||||||
|
|||||||
@@ -577,6 +577,7 @@ export const nbTranslation = {
|
|||||||
languageNb: 'Norsk',
|
languageNb: 'Norsk',
|
||||||
languageRu: 'Russisk',
|
languageRu: 'Russisk',
|
||||||
font: 'Skrifttype',
|
font: 'Skrifttype',
|
||||||
|
fontHintOpenDyslexic: 'Dyslexivennlig · ingen RU/ZH-støtte',
|
||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
appearance: 'Utseende',
|
appearance: 'Utseende',
|
||||||
servers: 'Tjenere',
|
servers: 'Tjenere',
|
||||||
|
|||||||
@@ -576,6 +576,7 @@ export const nlTranslation = {
|
|||||||
languageNb: 'Noors',
|
languageNb: 'Noors',
|
||||||
languageRu: 'Russisch',
|
languageRu: 'Russisch',
|
||||||
font: 'Lettertype',
|
font: 'Lettertype',
|
||||||
|
fontHintOpenDyslexic: 'Dyslexie-vriendelijk · geen RU/ZH-ondersteuning',
|
||||||
theme: 'Thema',
|
theme: 'Thema',
|
||||||
appearance: 'Weergave',
|
appearance: 'Weergave',
|
||||||
servers: 'Servers',
|
servers: 'Servers',
|
||||||
|
|||||||
@@ -611,6 +611,7 @@ export const ruTranslation = {
|
|||||||
languageRu: 'Русский',
|
languageRu: 'Русский',
|
||||||
languageRu2: 'Русский 2',
|
languageRu2: 'Русский 2',
|
||||||
font: 'Шрифт',
|
font: 'Шрифт',
|
||||||
|
fontHintOpenDyslexic: 'Подходит для дислексии · без поддержки RU/ZH',
|
||||||
theme: 'Тема',
|
theme: 'Тема',
|
||||||
appearance: 'Оформление',
|
appearance: 'Оформление',
|
||||||
servers: 'Серверы',
|
servers: 'Серверы',
|
||||||
|
|||||||
@@ -571,6 +571,7 @@ export const zhTranslation = {
|
|||||||
languageNb: '挪威',
|
languageNb: '挪威',
|
||||||
languageRu: '俄语',
|
languageRu: '俄语',
|
||||||
font: '字体',
|
font: '字体',
|
||||||
|
fontHintOpenDyslexic: '阅读障碍友好 · 不支持俄文/中文',
|
||||||
theme: '主题',
|
theme: '主题',
|
||||||
appearance: '外观',
|
appearance: '外观',
|
||||||
servers: '服务器',
|
servers: '服务器',
|
||||||
|
|||||||
+15
-3
@@ -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)',
|
'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)',
|
'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)',
|
'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;
|
] as const;
|
||||||
@@ -3832,6 +3833,7 @@ export default function Settings() {
|
|||||||
{ id: 'jetbrains-mono', label: 'JetBrains Mono' },
|
{ id: 'jetbrains-mono', label: 'JetBrains Mono' },
|
||||||
{ id: 'golos-text', label: 'Golos Text' },
|
{ id: 'golos-text', label: 'Golos Text' },
|
||||||
{ id: 'unbounded', label: 'Unbounded' },
|
{ id: 'unbounded', label: 'Unbounded' },
|
||||||
|
{ id: 'opendyslexic', label: 'OpenDyslexic' },
|
||||||
] as { id: FontId; label: string }[]).find(f => f.id === fontStore.font)?.label ?? fontStore.font
|
] as { id: FontId; label: string }[]).find(f => f.id === fontStore.font)?.label ?? fontStore.font
|
||||||
}</span>
|
}</span>
|
||||||
<ChevronDown size={14} style={{ color: 'var(--text-muted)', transform: fontPickerOpen ? 'rotate(180deg)' : 'none', transition: 'transform 0.2s' }} />
|
<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: 'jetbrains-mono', label: 'JetBrains Mono', stack: "'JetBrains Mono Variable', monospace" },
|
||||||
{ id: 'golos-text', label: 'Golos Text', stack: "'Golos Text Variable', sans-serif" },
|
{ id: 'golos-text', label: 'Golos Text', stack: "'Golos Text Variable', sans-serif" },
|
||||||
{ id: 'unbounded', label: 'Unbounded', stack: "'Unbounded 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 => (
|
).map(f => (
|
||||||
<button
|
<button
|
||||||
key={f.id}
|
key={f.id}
|
||||||
className={`btn ${fontStore.font === f.id ? 'btn-primary' : 'btn-ghost'}`}
|
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); }}
|
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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
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 {
|
interface FontState {
|
||||||
font: FontId;
|
font: FontId;
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
@import '@fontsource-variable/jetbrains-mono/wght-italic.css';
|
@import '@fontsource-variable/jetbrains-mono/wght-italic.css';
|
||||||
@import '@fontsource-variable/golos-text';
|
@import '@fontsource-variable/golos-text';
|
||||||
@import '@fontsource-variable/unbounded';
|
@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 ─── */
|
/* ─── Catppuccin Mocha Variables ─── */
|
||||||
:root,
|
:root,
|
||||||
@@ -6516,6 +6522,11 @@ input[type="range"]:hover::-webkit-slider-thumb {
|
|||||||
--font-display: 'Unbounded Variable', sans-serif;
|
--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
|
Middle Earth — Epic Fantasy / Cinematic
|
||||||
───────────────────────────────────────────────────────────── */
|
───────────────────────────────────────────────────────────── */
|
||||||
|
|||||||
Reference in New Issue
Block a user