Skip to content

Commit 6ae9c81

Browse files
committed
feat(ui,a11y): Phase 4 - toggle 'Testo piu' grande' (SPA) + hint mobile
Ultima fase del piano di semplificazione UI, focalizzata su accessibilita' per utenti 45-70 anni con vista non ottimale. ## Scelte pragmatiche vs piano originale - SKIP 4.1 (design tokens espliciti in packages/shared/design/tokens.ts): introdurrebbe classi semantiche (text-body-base, ecc.) parallele alle utility Tailwind gia' usate ovunque nel codebase, creando duplicazione e incoerenza. Rimandato a quando ci sara' un refactor mirato. - SEMPLIFICA 4.2: no colonna backend user_preferences.large_text (sarebbe over-engineering per una preferenza cosmetica per-dispositivo). Persistenza locale con localStorage / (mobile: delega al sistema). ## SPA - Toggle 'Testo piu' grande' vero - src/lib/textScale.ts: helper isLargeText / setLargeText / applyLargeText / initLargeText. Persist in localStorage 'accanto.largeText'. - index.css: regola 'html.accanto-large { font-size: 18px }'. La UI Tailwind e' rem-based -> tutta la scala visiva cresce di ~12.5% proporzionalmente (16 -> 18px). - main.tsx: initLargeText() prima del render per evitare flash a font piccolo se attivo. - AccountPage: toggle nel gruppo 'Profilo' (checkbox con label). Setter applica subito la class su <html>. ## Mobile - hint su system settings - iOS Dynamic Type e Android Font Size gia' scalano nativamente i <Text> di React Native (nessun allowFontScaling={false} nel codebase, verificato con grep). - Fare un toggle app-level reinventerebbe la ruota + creerebbe una seconda scala scollegata da quella di sistema. - AccountScreen (Profilo): aggiunto sotto Lingua un piccolo blocco 'Dimensione del testo' con hint 'usa le impostazioni del dispositivo, Accanto rispetta la tua scelta.' ## i18n (IT/EN/ES) - account.textScale.title - account.textScale.toggle (SPA) - account.textScale.hint (SPA) - account.textScale.mobileHint (mobile) ## Verifica - frontend build OK. - mobile tsc --noEmit exit=0. - test manuale: toggle su/off cambia la dimensione visiva di tutti gli elementi che usano rem (praticamente tutta la UI Tailwind). ## Phase 1-4 recap - P1 (dea35d4): welcome + quick action + security banner - P1.5 (1d7801c): fix bypass 2FA middleware + banner reattivo - P2 (1fbd651): Account in 4 accordion + deep-link - P3 (39c6a2f): empty state onboarding + chip audit + AI history unificata - P4 (questo): toggle testo grande SPA + hint mobile
1 parent 39c6a2f commit 6ae9c81

8 files changed

Lines changed: 106 additions & 0 deletions

File tree

frontend/src/index.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ body {
1010
font-family: 'Inter', system-ui, -apple-system, sans-serif;
1111
}
1212

13+
/* Accessibility: "Testo più grande" (toggle in Account → Profilo).
14+
La class è applicata su <html> a boot da src/lib/textScale.ts leggendo
15+
localStorage 'accanto.largeText'. Alza il font-size base da 16px a 18px
16+
(+12.5%): tutta la UI Tailwind (che usa rem) scala proporzionalmente. */
17+
html.accanto-large {
18+
font-size: 18px;
19+
}
20+
1321
@layer components {
1422
.btn {
1523
@apply inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors disabled:opacity-50;

frontend/src/lib/textScale.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Preferenza "Testo più grande" (accessibility, Phase 4).
3+
*
4+
* Persistita in localStorage con la chiave `accanto.largeText`.
5+
* Applicata come class `accanto-large` su `<html>`; la regola CSS in
6+
* `index.css` porta il font-size base da 16px a 18px. La UI Tailwind
7+
* (rem-based) scala di conseguenza.
8+
*
9+
* Non richiede backend: è una preferenza cosmetica per-dispositivo. Se in
10+
* futuro serve sync tra dispositivi si aggiunge una colonna
11+
* `user_preferences.large_text` e si sostituisce lo storage.
12+
*/
13+
const KEY = 'accanto.largeText';
14+
const CLASS = 'accanto-large';
15+
16+
export function isLargeText(): boolean {
17+
try {
18+
return localStorage.getItem(KEY) === '1';
19+
} catch {
20+
return false;
21+
}
22+
}
23+
24+
export function setLargeText(enabled: boolean): void {
25+
try {
26+
if (enabled) localStorage.setItem(KEY, '1');
27+
else localStorage.removeItem(KEY);
28+
} catch {
29+
/* localStorage bloccato: pazienza, resta solo per la sessione */
30+
}
31+
applyLargeText(enabled);
32+
}
33+
34+
/** Applica/rimuove la class sul documento senza toccare localStorage. */
35+
export function applyLargeText(enabled: boolean): void {
36+
const html = document.documentElement;
37+
if (enabled) html.classList.add(CLASS);
38+
else html.classList.remove(CLASS);
39+
}
40+
41+
/** Applica la preferenza salvata. Da chiamare al bootstrap dell'app. */
42+
export function initLargeText(): void {
43+
applyLargeText(isLargeText());
44+
}

frontend/src/main.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import App from './App';
55
import { AuthProvider } from './auth/AuthContext';
66
import './i18n';
77
import './index.css';
8+
import { initLargeText } from './lib/textScale';
9+
10+
// Applica subito la preferenza "Testo più grande" (evita flash a font
11+
// piccolo se attivo).
12+
initLargeText();
813

914
ReactDOM.createRoot(document.getElementById('root')!).render(
1015
<React.StrictMode>

frontend/src/pages/AccountPage.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import TwoFactorSection from '../components/TwoFactorSection';
1111
import SecurityAuditSection from '../components/SecurityAuditSection';
1212
import WellbeingSection from '../components/WellbeingSection';
1313
import AccordionSection from '../components/AccordionSection';
14+
import { isLargeText, setLargeText } from '../lib/textScale';
1415
import type { TwoFactorStatus } from '@accanto/shared/types';
1516

1617
// Anchor id → gruppo accordion da aprire quando l'URL contiene quell'hash.
@@ -47,6 +48,14 @@ export default function AccountPage() {
4748
const [exportError, setExportError] = useState<string | null>(null);
4849
const [exporting, setExporting] = useState(false);
4950

51+
// Accessibility: toggle "Testo più grande". Init dalla preferenza
52+
// localStorage; toggle immediato (applica la class sul <html>).
53+
const [largeText, setLargeTextState] = useState<boolean>(() => isLargeText());
54+
const onToggleLargeText = (next: boolean) => {
55+
setLargeTextState(next);
56+
setLargeText(next);
57+
};
58+
5059
// Hint numerico su "Sicurezza": count di suggerimenti attivi. Per ora: 1 se
5160
// 2FA disattivata, 0 altrimenti. Errore silenzioso (il hint non è critico).
5261
const [securityHints, setSecurityHints] = useState<number>(0);
@@ -178,6 +187,20 @@ export default function AccountPage() {
178187
<LanguageSwitcher />
179188
</section>
180189

190+
<section className="space-y-2">
191+
<h2 className="text-base font-semibold text-accanto-900">{t('account.textScale.title')}</h2>
192+
<p className="text-sm text-accanto-500">{t('account.textScale.hint')}</p>
193+
<label className="inline-flex items-center gap-2 text-sm text-accanto-700 select-none cursor-pointer">
194+
<input
195+
type="checkbox"
196+
checked={largeText}
197+
onChange={(e) => onToggleLargeText(e.target.checked)}
198+
className="w-4 h-4"
199+
/>
200+
<span>{t('account.textScale.toggle')}</span>
201+
</label>
202+
</section>
203+
181204
<section className="space-y-3">
182205
<h2 className="text-base font-semibold text-accanto-900">{t('account.changePassword')}</h2>
183206
<form onSubmit={submitPassword} className="space-y-3">

mobile/src/screens/AccountScreen.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ export default function AccountScreen() {
7171
<View className="gap-3">
7272
<AccordionSection title={t('account.groups.profile')} defaultOpen>
7373
<LanguageSection />
74+
<View className="gap-1">
75+
<Text className="text-base font-semibold text-accanto-900">
76+
{t('account.textScale.title')}
77+
</Text>
78+
<Text className="text-sm text-accanto-500">
79+
{t('account.textScale.mobileHint')}
80+
</Text>
81+
</View>
7482
<ChangePasswordSection />
7583
</AccordionSection>
7684

packages/shared/src/i18n/locales/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@
8383
"languageSectionTitle": "Language",
8484
"languageSectionHint": "The app will use this language on your devices.",
8585
"languageSaved": "Language updated.",
86+
"textScale": {
87+
"title": "Text size",
88+
"toggle": "Larger text",
89+
"hint": "Increase text size for easier reading.",
90+
"mobileHint": "To change text size use your device settings. Accanto respects your choice."
91+
},
8692
"deleteTitle": "Delete account",
8793
"deleteDescription": "Deletion permanently removes your profile and any circles where you are the only member, along with the related journal, documents, questions and updates. If you share a circle with other people, you must first have them leave or leave the circle yourself.",
8894
"deleteConfirmLabel": "Confirm with your password",

packages/shared/src/i18n/locales/es.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@
8383
"languageSectionTitle": "Idioma",
8484
"languageSectionHint": "La aplicación usará este idioma en tus dispositivos.",
8585
"languageSaved": "Idioma actualizado.",
86+
"textScale": {
87+
"title": "Tamaño del texto",
88+
"toggle": "Texto más grande",
89+
"hint": "Aumenta el tamaño del texto para una lectura más cómoda.",
90+
"mobileHint": "Para cambiar el tamaño del texto usa los ajustes de tu dispositivo. Accanto respeta tu elección."
91+
},
8692
"deleteTitle": "Eliminar cuenta",
8793
"deleteDescription": "La eliminación borra de forma definitiva tu perfil y todos los círculos en los que eres el único miembro, junto con el diario, los documentos, las preguntas y las novedades vinculadas. Si compartes un círculo con otras personas, primero debes hacer que salgan o salir tú.",
8894
"deleteConfirmLabel": "Confirma con tu contraseña",

packages/shared/src/i18n/locales/it.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@
8383
"languageSectionTitle": "Lingua",
8484
"languageSectionHint": "L'app userà questa lingua sui tuoi dispositivi.",
8585
"languageSaved": "Lingua aggiornata.",
86+
"textScale": {
87+
"title": "Dimensione del testo",
88+
"toggle": "Testo più grande",
89+
"hint": "Aumenta la dimensione del testo per una lettura più comoda.",
90+
"mobileHint": "Per cambiare la dimensione del testo usa le impostazioni del tuo dispositivo. Accanto rispetta la tua scelta."
91+
},
8692
"deleteTitle": "Elimina account",
8793
"deleteDescription": "L'eliminazione rimuove definitivamente il tuo profilo e tutti i cerchi di cui sei l'unico membro, insieme a diario, documenti, domande e aggiornamenti collegati. Se condividi un cerchio con altre persone, devi prima farli uscire o uscire tu stesso.",
8894
"deleteConfirmLabel": "Conferma con la password",

0 commit comments

Comments
 (0)