feat: easy-read font option (Atkinson Hyperlegible)

New Settings → Appearance toggle, "Easy-Read Font". When on, swaps the
UI sans-serif to Atkinson Hyperlegible — a typeface designed by the
Braille Institute for legibility (distinct b/d/p/q, clear 1/I/l). Helps
dyslexic readers and anyone in a long reading session.

- Bundled via @fontsource/atkinson-hyperlegible (400 + 700), pinned exact
  — no runtime font fetch, works offline.
- Toggle adds an html.font-readable class; CSS overrides --font-ui and
  nudges letter-spacing (+0.012em) and line-height (1.6 -> 1.65) per
  evidence-based dyslexia guidance.
- Persisted to localStorage (wrystr_easy_read_font), applied on <html>
  at startup. Independent of theme — pairs with any, Reader included.
- Code blocks keep font-mono; articles keep their reading serif.

Follow-up to the Reader theme: colour was ~60-70% of the dyslexia-
friendly benefit, the font is most of the rest.
This commit is contained in:
Jure
2026-05-21 17:30:59 +02:00
parent 5cd11667ae
commit 5c490dbb56
7 changed files with 72 additions and 2 deletions
+12 -2
View File
@@ -1,13 +1,14 @@
{
"name": "vega",
"version": "0.12.16",
"version": "0.12.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vega",
"version": "0.12.16",
"version": "0.12.17",
"dependencies": {
"@fontsource/atkinson-hyperlegible": "5.2.8",
"@nostr-dev-kit/ndk": "^3.0.3",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/react-virtual": "3.13.24",
@@ -1015,6 +1016,15 @@
}
}
},
"node_modules/@fontsource/atkinson-hyperlegible": {
"version": "5.2.8",
"resolved": "https://registry.npmjs.org/@fontsource/atkinson-hyperlegible/-/atkinson-hyperlegible-5.2.8.tgz",
"integrity": "sha512-HciLcJ5DIK/OVOdo71EbEN4NnvDFlp6/SpAxtcbWf2aAdcsOuPqITxj5KNEXb48qSPSdnnZdGGnSJChPKi3/bA==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+1
View File
@@ -12,6 +12,7 @@
"test:run": "vitest run"
},
"dependencies": {
"@fontsource/atkinson-hyperlegible": "5.2.8",
"@nostr-dev-kit/ndk": "^3.0.3",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/react-virtual": "3.13.24",
+6
View File
@@ -96,6 +96,7 @@ function App() {
const showDebugPanel = useUIStore((s) => s.showDebugPanel);
const toggleDebugPanel = useUIStore((s) => s.toggleDebugPanel);
const fontSize = useUIStore((s) => s.fontSize);
const easyReadFont = useUIStore((s) => s.easyReadFont);
const themeId = useUIStore((s) => s.themeId);
const [onboardingDone, setOnboardingDone] = useState(
() => !!localStorage.getItem("wrystr_pubkey")
@@ -117,6 +118,11 @@ function App() {
if (theme) applyTheme(theme);
}, [themeId]);
// Apply easy-read font class on <html>
useEffect(() => {
document.documentElement.classList.toggle("font-readable", easyReadFont);
}, [easyReadFont]);
// Intercept external link clicks and open in system browser via Tauri opener
useEffect(() => {
const handler = (e: MouseEvent) => {
+31
View File
@@ -434,6 +434,36 @@ function FontSizeSection() {
);
}
function EasyReadFontSection() {
const { easyReadFont, setEasyReadFont } = useUIStore();
return (
<section>
<h2 className="text-text text-[11px] font-medium uppercase tracking-widest mb-2 text-text-dim">
Easy-Read Font
</h2>
<div className="flex flex-wrap items-center gap-2">
<button
onClick={() => setEasyReadFont(!easyReadFont)}
className={`px-3 py-1.5 text-[11px] border transition-colors ${
easyReadFont
? "border-accent text-accent"
: "border-border text-text-muted hover:text-text hover:border-accent/40"
}`}
>
{easyReadFont ? "On" : "Off"}
</button>
</div>
<p className="text-text-dim text-[10px] mt-2 px-1">
Switches the UI to Atkinson Hyperlegible a font designed by the
Braille Institute for legibility. Helps dyslexic readers and anyone
reading long sessions; slightly wider letter-spacing and line-height
applied per evidence-based guidance.
</p>
</section>
);
}
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -515,6 +545,7 @@ export function SettingsView() {
<div className="flex-1 overflow-y-auto p-4 space-y-8">
<ThemeSection />
<FontSizeSection />
<EasyReadFontSection />
<WalletSection />
<NotificationSection />
<ExperimentalSection />
+12
View File
@@ -46,6 +46,18 @@ body {
overflow: hidden;
}
/* Easy-read font — Atkinson Hyperlegible — toggleable in Settings.
Designed by the Braille Institute for legibility; helps dyslexic readers
and anyone reading long sessions. Letter-spacing and line-height nudged
per evidence-based guidance. */
html.font-readable {
--font-ui: "Atkinson Hyperlegible", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
html.font-readable body {
letter-spacing: 0.012em;
line-height: 1.65;
}
/* Allow text selection in article editor */
.article-editor textarea,
.article-editor input {
+2
View File
@@ -4,6 +4,8 @@ import { debug } from "./lib/debug";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";
import "@fontsource/atkinson-hyperlegible/400.css";
import "@fontsource/atkinson-hyperlegible/700.css";
import { useUserStore } from "./stores/user";
// Error boundary to catch React error #31 and show details instead of blank screen
+8
View File
@@ -31,6 +31,7 @@ interface UIState {
feedLanguageFilter: string | null;
followsTab: "followers" | "following";
fontSize: number;
easyReadFont: boolean;
themeId: string;
setView: (view: View) => void;
setFollowsTab: (tab: "followers" | "following") => void;
@@ -44,6 +45,7 @@ interface UIState {
goBack: () => void;
setFeedLanguageFilter: (filter: string | null) => void;
setFontSize: (size: number) => void;
setEasyReadFont: (on: boolean) => void;
setTheme: (id: string) => void;
toggleSidebar: () => void;
toggleHelp: () => void;
@@ -52,6 +54,7 @@ interface UIState {
const SIDEBAR_KEY = "wrystr_sidebar_collapsed";
const FONT_SIZE_KEY = "wrystr_font_size";
const EASY_READ_FONT_KEY = "wrystr_easy_read_font";
const THEME_KEY = "wrystr_theme";
const SCRIPT_FILTER_KEY = "wrystr_script_filter";
@@ -73,6 +76,7 @@ export const useUIStore = create<UIState>((set, _get) => ({
feedLanguageFilter: localStorage.getItem(SCRIPT_FILTER_KEY) || null,
followsTab: "followers",
fontSize: parseInt(localStorage.getItem(FONT_SIZE_KEY) || "14", 10),
easyReadFont: localStorage.getItem(EASY_READ_FONT_KEY) === "true",
themeId: localStorage.getItem(THEME_KEY) || "midnight",
setView: (currentView) => set({ currentView }),
setFeedTab: (feedTab) => set({ feedTab }),
@@ -115,6 +119,10 @@ export const useUIStore = create<UIState>((set, _get) => ({
localStorage.setItem(FONT_SIZE_KEY, String(fontSize));
set({ fontSize });
},
setEasyReadFont: (easyReadFont) => {
localStorage.setItem(EASY_READ_FONT_KEY, String(easyReadFont));
set({ easyReadFont });
},
setTheme: (themeId) => {
localStorage.setItem(THEME_KEY, themeId);
set({ themeId });