feat(a11y): add a Text size control and an OpenDyslexic font option (#4210)

* feat(a11y): add a Text size control and an OpenDyslexic font option

Text size: a Theme > Font & Layout control (Default / Larger) that scales the whole UI via CSS zoom, so the many hard-coded px sizes scale too (density only moves the root font-size). Stored globally so it persists across theme switches; applied early in the boot script to avoid a flash. OpenDyslexic: a dyslexia-friendly self-hosted font (SIL OFL 1.1), bundled as woff2 alongside Fira Code/Inter and wired into the Font select. Reuses the existing density/font pattern end to end; no new colours, spacing, or component styles.

* fix(a11y): keep modals on-screen at Larger text size

Inline vh heights on .modal-content overrode the ui-scale-125 max-height
compensation, so Cookbook (and the email/doc/skills/PDF modals) overflowed
the viewport at 125% — pushing the header and close button off-screen.
Let the compensation own those heights.

* fix(a11y): keep PDF export modal at its original 86vh on Default size
This commit is contained in:
Tom
2026-06-22 12:53:46 +01:00
committed by GitHub
parent d36879bd50
commit 91b4171b3f
12 changed files with 182 additions and 8 deletions
+27
View File
@@ -39,6 +39,7 @@ const FONT_MAP = {
mono: "'Fira Code', monospace",
sans: "system-ui, -apple-system, 'Segoe UI', sans-serif",
serif: "Georgia, 'Times New Roman', serif",
opendyslexic: "'OpenDyslexic', sans-serif",
};
const DEFAULT_FONT = 'mono';
const DEFAULT_DENSITY = 'comfortable';
@@ -387,6 +388,20 @@ export function applyFontDensity(font, density) {
if (d !== 'comfortable') document.documentElement.classList.add('density-' + d);
}
// UI text-size scale (accessibility). Global and independent of the active
// theme, so the chosen size persists across theme switches. Stored as a plain
// percentage string ('100' | '110' | '125' | '150').
const UI_SCALE_KEY = 'odysseus-ui-scale';
const DEFAULT_UI_SCALE = '100';
export function applyUiScale(scale) {
const s = scale || DEFAULT_UI_SCALE;
// Only one non-default scale ('125'). Remove any legacy classes too so an
// older stored value can't leave a stale zoom applied.
document.documentElement.classList.remove('ui-scale-110', 'ui-scale-125', 'ui-scale-140');
if (s === '125') document.documentElement.classList.add('ui-scale-125');
}
const _BG_CLASSES = ['bg-pattern-dots',
'bg-pattern-synapse', 'bg-pattern-rain', 'bg-pattern-constellations',
'bg-pattern-perlin-flow',
@@ -1133,6 +1148,18 @@ export function initThemeUI() {
const s = getSaved(); if (s) _saveFull(s.name, s.colors);
});
}
const textSizeSelect = document.getElementById('theme-text-size-select');
if (textSizeSelect) {
const nts = textSizeSelect.cloneNode(true); textSizeSelect.parentNode.replaceChild(nts, textSizeSelect);
let initScale = DEFAULT_UI_SCALE;
try { initScale = localStorage.getItem(UI_SCALE_KEY) || DEFAULT_UI_SCALE; } catch (e) {}
nts.value = initScale;
applyUiScale(initScale);
nts.addEventListener('change', () => {
applyUiScale(nts.value);
try { localStorage.setItem(UI_SCALE_KEY, nts.value); } catch (e) {}
});
}
if (patternSelect) {
const np = patternSelect.cloneNode(true); patternSelect.parentNode.replaceChild(np, patternSelect);
np.value = _initPattern;