From b73658941396295013595e1491a46a17dcee8b3e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 10:53:46 +0000 Subject: [PATCH 01/10] fix(flights): stop auto-selecting newly detected airlines in the filter Every schedule fetch permanently adds any airline seen in the data to the airport's airline list. When the user had all airlines selected (the default state), the screen then auto-selected the grown list, so carriers the user does not handle kept appearing on the board. Replace the expansion heuristic with reconcileSelectedAirlines: newly detected airlines stay unchecked in the filter until the user opts in, and keys that disappear from the airport list are pruned from the selection. Also consolidate the three drifted copies of the airline alias table (flightScheduleAdapter, airportSettings, airlineOps) into a single shared airlineAliases module, and add regression tests for the reconciliation and for 3-letter canonical names (SAS, DHL) surviving the raw-code filter. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012vEkzMVahsn6d7vyL4xayt --- scripts/test-flight-helpers.cjs | 48 ++++++++++++++++++ src/screens/FlightScreen.tsx | 31 +++++------- src/utils/airlineAliases.ts | 69 ++++++++++++++++++++++++++ src/utils/airlineOps.ts | 58 +--------------------- src/utils/airportSettings.ts | 78 ++++++++++++++---------------- src/utils/flightScheduleAdapter.ts | 52 ++------------------ 6 files changed, 170 insertions(+), 166 deletions(-) create mode 100644 src/utils/airlineAliases.ts diff --git a/scripts/test-flight-helpers.cjs b/scripts/test-flight-helpers.cjs index 2e4f4171..212ca8d5 100644 --- a/scripts/test-flight-helpers.cjs +++ b/scripts/test-flight-helpers.cjs @@ -301,6 +301,54 @@ assert(detectedAirlines.includes('transavia'), 'airport airline discovery should assert(!detectedAirlines.some(key => key.startsWith('compagnia')), 'airport airline discovery should drop generic company placeholders'); assert(!['xue', 'si', 'q1', 'ki', 'jt', 'sconosciuta'].some(key => detectedAirlines.includes(key)), 'airport airline discovery should drop raw unknown airline codes'); +const shortNamedAirlines = airportSettings.extractAirportAirlinesFromSchedule(['SAS', 'DHL', 'Scandinavian Airlines']); +assert(shortNamedAirlines.includes('sas'), 'canonical airlines with 3-letter names must survive the raw-code filter'); +assert(shortNamedAirlines.includes('dhl'), 'DHL must survive the raw-code filter'); +assert(shortNamedAirlines.filter(key => key === 'sas').length === 1, 'SAS and Scandinavian Airlines should collapse into one key'); + +// Regressione "voli che non gestisco": una compagnia appena rilevata nello +// schedule non deve mai finire selezionata da sola nel filtro. +assert( + airportSettings.reconcileSelectedAirlines({ + savedProfileAirlines: ['ryanair', 'easyjet'], + previousSelectedAirlines: ['ryanair', 'easyjet'], + nextAirportAirlines: ['ryanair', 'easyjet', 'british airways'], + }) === null, + 'newly detected airlines must not be auto-selected', +); +assert( + JSON.stringify(airportSettings.reconcileSelectedAirlines({ + savedProfileAirlines: ['ryanair', 'easyjet'], + previousSelectedAirlines: ['ryanair', 'easyjet'], + nextAirportAirlines: ['ryanair', 'volotea'], + })) === JSON.stringify(['ryanair']), + 'airlines removed from the airport list must be pruned from the selection', +); +assert( + JSON.stringify(airportSettings.reconcileSelectedAirlines({ + savedProfileAirlines: [], + previousSelectedAirlines: ['ryanair'], + nextAirportAirlines: ['ryanair'], + })) === JSON.stringify([]), + 'a profile without saved airlines clears the selection', +); +assert( + airportSettings.reconcileSelectedAirlines({ + savedProfileAirlines: [], + previousSelectedAirlines: [], + nextAirportAirlines: ['ryanair'], + }) === null, + 'an already-empty selection needs no reconciliation', +); +assert( + airportSettings.reconcileSelectedAirlines({ + savedProfileAirlines: ['ryanair'], + previousSelectedAirlines: ['ryanair'], + nextAirportAirlines: ['ryanair'], + }) === null, + 'an unchanged selection needs no reconciliation', +); + const merged = adapter.mergeFlightLists([scheduledOnly], [scheduledOnly, delayed], 'departure'); assert(merged.length === 2, 'merge should dedupe cached and fresh flights'); diff --git a/src/screens/FlightScreen.tsx b/src/screens/FlightScreen.tsx index bf70c36a..13a9a51b 100644 --- a/src/screens/FlightScreen.tsx +++ b/src/screens/FlightScreen.tsx @@ -25,7 +25,7 @@ import { getAirlineOps, getAirlineColor, getDepartureGateWindow } from '../utils import { statusToToken, delayToToken } from '../utils/statusColors'; import { fetchAirportScheduleRaw, type FlightScheduleProviderStatus } from '../utils/fr24api'; import { fetchStaffMonitorData, normalizeFlightNumber, type StaffMonitorFlight } from '../utils/staffMonitor'; -import { formatAirportHeader, getAirportAirlines, getAirportInfo, getStoredAirportAirlines } from '../utils/airportSettings'; +import { formatAirportHeader, getAirportAirlines, getAirportInfo, getStoredAirportAirlines, reconcileSelectedAirlines } from '../utils/airportSettings'; import { applyLiveArrivalEtas, applyLiveDepartureStatus, applyLiveOriginDepartures, fetchAdsbAircraft } from '../utils/liveArrivalEta'; import { WIDGET_CACHE_KEY, WIDGET_SHIFT_KEY } from '../widgets/widgetTaskHandler'; import type { WidgetData, WidgetFlight, WidgetShiftData } from '../widgets/widgetTaskHandler'; @@ -68,7 +68,6 @@ import { import { clamp, DEFAULT_NOTIFICATION_SETTINGS, - sameAirlineKeys, sanitizeNotificationSettings, type FlightNotificationSettings, } from '../utils/flightNotificationSettings'; @@ -559,17 +558,12 @@ export default function FlightScreen({ isFocused = true }: { isFocused?: boolean setSelectedAirlines(next); persistSelectedAirlines(next).catch(() => {}); }, [persistSelectedAirlines]); - const airportAirlinesRef = useRef([]); const selectedAirlinesRef = useRef([]); const notifSettingsRef = useRef(DEFAULT_NOTIFICATION_SETTINGS); const selectedAirlinesNotifSignatureRef = useRef(''); const fetchInFlightRef = useRef(false); const lastFlightRefreshAttemptAtRef = useRef(0); - useEffect(() => { - airportAirlinesRef.current = airportAirlines; - }, [airportAirlines]); - useEffect(() => { selectedAirlinesRef.current = selectedAirlines; }, [selectedAirlines]); @@ -673,19 +667,16 @@ export default function FlightScreen({ isFocused = true }: { isFocused?: boolean const nextAirportAirlines = getAirportAirlines(airportCode); setAirportAirlines(nextAirportAirlines); - const savedProfileAirlines = activeProfile?.airportCode === airportCode ? activeProfile.airlines : []; - const previousAirportAirlines = airportAirlinesRef.current; - const previousSelectedAirlines = selectedAirlinesRef.current; - const hadAllPreviouslySelected = - previousAirportAirlines.length > 0 && - previousAirportAirlines.every(key => previousSelectedAirlines.includes(key)); - - if (savedProfileAirlines.length === 0) { - if (previousSelectedAirlines.length > 0) { - applySelectedAirlines([]); - } - } else if (hadAllPreviouslySelected && !sameAirlineKeys(savedProfileAirlines, nextAirportAirlines)) { - applySelectedAirlines(nextAirportAirlines); + // Le compagnie appena rilevate nello schedule NON vengono mai selezionate + // in automatico: restano deselezionate nel filtro finché l'utente non le + // spunta, così in bacheca non compaiono voli che non gestisce. + const reconciledSelection = reconcileSelectedAirlines({ + savedProfileAirlines: activeProfile?.airportCode === airportCode ? activeProfile.airlines : [], + previousSelectedAirlines: selectedAirlinesRef.current, + nextAirportAirlines, + }); + if (reconciledSelection) { + applySelectedAirlines(reconciledSelection); } // Accumula voli: fonde i dati freschi con quelli in cache e conserva solo // i voli non più vecchi di 1 ora dall'orario migliore disponibile. diff --git a/src/utils/airlineAliases.ts b/src/utils/airlineAliases.ts new file mode 100644 index 00000000..dbd02e46 --- /dev/null +++ b/src/utils/airlineAliases.ts @@ -0,0 +1,69 @@ +/*---------------------------------------------------------------------------*\ +| Tabella unica degli alias compagnia (nomi, codici IATA/ICAO, brand del | +| gruppo). Era duplicata in flightScheduleAdapter, airportSettings e | +| airlineOps con piccole divergenze: qualunque nuovo alias va aggiunto SOLO | +| qui. L'ordine delle chiavi conta per la canonicalizzazione: le voci più | +| specifiche (es. "air arabia maroc") devono precedere quelle generiche | +| ("air arabia"), perché la prima regola che combacia vince. | +\*---------------------------------------------------------------------------*/ +export const AIRLINE_ALIASES: Record = { + ryanair: ['ryanair', 'fr', 'ryr'], + easyjet: ['easyjet', 'easy jet', 'easyjet europe', 'easyjet switzerland', 'easyjet uk', 'u2', 'ec', 'ds', 'eju', 'ezy', 'ezs'], + wizz: ['wizz', 'wizz air', 'wizz air malta', 'wizz air uk', 'wizz air abu dhabi', 'w6', 'w4', 'w9', 'wzz', 'wmt', 'wuk'], + volotea: ['volotea', 'v7'], + vueling: ['vueling', 'vy'], + transavia: ['transavia', 'transavia france', 'transavia holland', 'transavia airlines', 'hv', 'to', 'tra', 'tvf'], + 'aer lingus': ['aer lingus', 'ei'], + 'british airways': ['british airways', 'ba', 'baw'], + sas: ['sas', 'scandinavian', 'sk'], + scandinavian: ['sas', 'scandinavian', 'sk'], + flydubai: ['flydubai', 'fz', 'fdb'], + aeroitalia: ['aeroitalia', 'xz'], + 'air arabia maroc': ['air arabia maroc', '3o', 'mac'], + 'air arabia': ['air arabia', 'g9', 'abz'], + 'air dolomiti': ['air dolomiti', 'en', 'dla'], + buzz: ['buzz', 'rr', 'rys'], + dhl: ['dhl', 'qy', 'bcs'], + eurowings: ['eurowings', 'ew', 'ewg'], + 'ita airways': ['ita airways', 'ita', 'az', 'ity'], + lufthansa: ['lufthansa', 'lh', 'dlh'], +}; + +export function normalizeAirlineText(value: unknown): string { + if (typeof value !== 'string' && typeof value !== 'number') return ''; + return String(value).trim().toLowerCase().replace(/[^a-z0-9]+/g, ' ').replace(/\s+/g, ' ').trim(); +} + +export function compactAirlineText(value: unknown): string { + return normalizeAirlineText(value).replace(/\s+/g, ''); +} + +/* Gli alias corti (codici IATA/ICAO, max 3 caratteri) combaciano solo come + parola intera o valore esatto, per non far scattare "fr" dentro "france"; + quelli lunghi combaciano come sottostringa compatta ("easyjet" dentro + "easyjet europe"). */ +export function airlineAliasMatches(value: unknown, alias: string): boolean { + const normalizedValue = normalizeAirlineText(value); + const normalizedAlias = normalizeAirlineText(alias); + const compactAlias = compactAirlineText(alias); + if (!normalizedValue || !normalizedAlias || !compactAlias) return false; + + if (compactAlias.length <= 3) { + return normalizedValue.split(' ').includes(compactAlias) || compactAirlineText(value) === compactAlias; + } + + return compactAirlineText(value).includes(compactAlias); +} + +export function canonicalAirlineKey(value: unknown): string { + const normalized = normalizeAirlineText(value); + if (!normalized) return ''; + + for (const [key, aliases] of Object.entries(AIRLINE_ALIASES)) { + if (aliases.some(alias => airlineAliasMatches(normalized, alias))) { + return key; + } + } + + return normalized; +} diff --git a/src/utils/airlineOps.ts b/src/utils/airlineOps.ts index 27cf6017..b28aa388 100644 --- a/src/utils/airlineOps.ts +++ b/src/utils/airlineOps.ts @@ -1,3 +1,5 @@ +import { canonicalAirlineKey } from './airlineAliases'; + export type HexColor = `#${string}`; export type AirlineOps = { @@ -29,62 +31,6 @@ export const AIRLINE_OPS: Array<{ key: string; ops: AirlineOps }> = [ { key: 'flydubai', ops: { checkInOpen: 180, checkInClose: 60, gateOpen: 40, gateClose: 20 } }, ]; -const AIRLINE_ALIASES: Record = { - ryanair: ['ryanair', 'fr', 'ryr'], - easyjet: ['easyjet', 'easy jet', 'u2', 'ec', 'ds', 'eju', 'ezy', 'ezs'], - wizz: ['wizz', 'wizz air', 'w6', 'w4', 'w9', 'wzz', 'wmt', 'wuk'], - volotea: ['volotea', 'v7'], - vueling: ['vueling', 'vy'], - transavia: ['transavia', 'transavia france', 'transavia holland', 'hv', 'to', 'tra', 'tvf'], - 'aer lingus': ['aer lingus', 'ei'], - 'british airways': ['british airways', 'ba', 'baw'], - sas: ['sas', 'scandinavian', 'sk'], - flydubai: ['flydubai', 'fz', 'fdb'], - aeroitalia: ['aeroitalia', 'xz'], - 'air arabia maroc': ['air arabia maroc', '3o', 'mac'], - 'air arabia': ['air arabia', 'g9', 'abz'], - 'air dolomiti': ['air dolomiti', 'en', 'dla'], - buzz: ['buzz', 'rr', 'rys'], - dhl: ['dhl', 'qy', 'bcs'], - eurowings: ['eurowings', 'ew', 'ewg'], - 'ita airways': ['ita airways', 'ita', 'az', 'ity'], - lufthansa: ['lufthansa', 'lh', 'dlh'], -}; - -function normalizeAirlineText(value: unknown): string { - if (typeof value !== 'string' && typeof value !== 'number') return ''; - return String(value).trim().toLowerCase().replace(/[^a-z0-9]+/g, ' ').replace(/\s+/g, ' ').trim(); -} - -function compactAirlineText(value: unknown): string { - return normalizeAirlineText(value).replace(/\s+/g, ''); -} - -function airlineAliasMatches(value: string, alias: string): boolean { - const normalizedAlias = normalizeAirlineText(alias); - const compactAlias = compactAirlineText(alias); - if (!value || !normalizedAlias || !compactAlias) return false; - - if (compactAlias.length <= 3) { - return value.split(' ').includes(compactAlias) || compactAirlineText(value) === compactAlias; - } - - return compactAirlineText(value).includes(compactAlias); -} - -function canonicalAirlineKey(value: unknown): string { - const normalized = normalizeAirlineText(value); - if (!normalized) return ''; - - for (const [key, aliases] of Object.entries(AIRLINE_ALIASES)) { - if (aliases.some(alias => airlineAliasMatches(normalized, alias))) { - return key; - } - } - - return normalized; -} - export function getAirlineOps(name: string): AirlineOps { const key = canonicalAirlineKey(name); return AIRLINE_OPS.find(item => item.key === key)?.ops diff --git a/src/utils/airportSettings.ts b/src/utils/airportSettings.ts index beb5bba2..175c706c 100644 --- a/src/utils/airportSettings.ts +++ b/src/utils/airportSettings.ts @@ -1,5 +1,11 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { ALLOWED_AIRLINES, AIRLINE_DISPLAY_NAMES } from './airlineOps'; +import { + AIRLINE_ALIASES, + canonicalAirlineKey, + compactAirlineText, + normalizeAirlineText, +} from './airlineAliases'; export type AirportPreset = { code: string; @@ -53,35 +59,13 @@ const airportAirlinesCache: Record = Object.fromEntries( ); function normalizeAirlineKey(value: string | null | undefined): string { - return (value ?? '').trim().toLowerCase().replace(/[^a-z0-9]+/g, ' ').replace(/\s+/g, ' ').trim(); + return normalizeAirlineText(value); } function compactAirlineKey(value: string | null | undefined): string { - return normalizeAirlineKey(value).replace(/\s+/g, ''); + return compactAirlineText(value); } -const AIRLINE_CANONICAL_RULES: Array<{ canonical: string; needles: string[] }> = [ - { canonical: 'ryanair', needles: ['ryanair', 'fr', 'ryr'] }, - { canonical: 'easyjet', needles: ['easyjet', 'easy jet', 'easyjet europe', 'easyjet switzerland', 'easyjet uk', 'u2', 'ec', 'ds', 'eju', 'ezy', 'ezs'] }, - { canonical: 'wizz', needles: ['wizz', 'wizz air', 'wizz air malta', 'wizz air uk', 'wizz air abu dhabi', 'w6', 'w4', 'w9', 'wzz', 'wmt', 'wuk'] }, - { canonical: 'volotea', needles: ['volotea', 'v7'] }, - { canonical: 'vueling', needles: ['vueling', 'vy'] }, - { canonical: 'transavia', needles: ['transavia france', 'transavia holland', 'transavia airlines', 'transavia', 'hv', 'to', 'tra', 'tvf'] }, - { canonical: 'aer lingus', needles: ['aer lingus', 'ei'] }, - { canonical: 'british airways', needles: ['british airways', 'ba', 'baw'] }, - { canonical: 'sas', needles: ['sas', 'scandinavian', 'sk'] }, - { canonical: 'flydubai', needles: ['flydubai', 'fz', 'fdb'] }, - { canonical: 'aeroitalia', needles: ['aeroitalia', 'xz'] }, - { canonical: 'air arabia maroc', needles: ['air arabia maroc', '3o', 'mac'] }, - { canonical: 'air arabia', needles: ['air arabia', 'g9', 'abz'] }, - { canonical: 'air dolomiti', needles: ['air dolomiti', 'en', 'dla'] }, - { canonical: 'buzz', needles: ['buzz', 'rr', 'rys'] }, - { canonical: 'dhl', needles: ['dhl', 'qy', 'bcs'] }, - { canonical: 'eurowings', needles: ['eurowings', 'ew', 'ewg'] }, - { canonical: 'ita airways', needles: ['ita airways', 'ita', 'az', 'ity'] }, - { canonical: 'lufthansa', needles: ['lufthansa', 'lh', 'dlh'] }, -]; - function isGenericAirlinePlaceholder(value: string): boolean { return value === 'sconosciuta' || value === 'unknown' @@ -94,20 +78,6 @@ function isLikelyRawAirlineCode(value: string): boolean { return /^[a-z0-9]{1,3}$/.test(compactAirlineKey(value)); } -function airlineRuleMatches(value: string, needle: string): boolean { - const normalizedNeedle = normalizeAirlineKey(needle); - const compactNeedle = compactAirlineKey(needle); - if (!normalizedNeedle || !compactNeedle) { - return false; - } - - if (compactNeedle.length <= 3) { - return value.split(' ').includes(compactNeedle) || compactAirlineKey(value) === compactNeedle; - } - - return compactAirlineKey(value).includes(compactNeedle); -} - function canonicalizeAirlineKey(value: string | null | undefined): string { const normalized = normalizeAirlineKey(value); if (!normalized) { @@ -118,10 +88,9 @@ function canonicalizeAirlineKey(value: string | null | undefined): string { return ''; } - for (const rule of AIRLINE_CANONICAL_RULES) { - if (rule.needles.some(needle => airlineRuleMatches(normalized, needle))) { - return rule.canonical; - } + const canonical = canonicalAirlineKey(normalized); + if (AIRLINE_ALIASES[canonical]) { + return canonical; } if (isLikelyRawAirlineCode(normalized)) { @@ -257,6 +226,31 @@ export async function storeDetectedAirportAirlines(code: string | null | undefin return next; } +/*---------------------------------------------------------------------------*\ +| Riconcilia il filtro compagnie quando la lista dell'aeroporto cambia (i | +| provider aggiungono le compagnie rilevate nello schedule, charter inclusi). | +| Regole: | +| - profilo senza compagnie salvate → filtro vuoto (nessun filtro attivo); | +| - chiavi sparite dalla lista aeroporto → tolte dalla selezione; | +| - compagnie NUOVE rilevate → mai selezionate in automatico: comparirebbero | +| in bacheca voli che l'utente non gestisce. | +| Ritorna null quando la selezione corrente può restare invariata. | +\*---------------------------------------------------------------------------*/ +export function reconcileSelectedAirlines(options: { + savedProfileAirlines: string[]; + previousSelectedAirlines: string[]; + nextAirportAirlines: string[]; +}): string[] | null { + const { savedProfileAirlines, previousSelectedAirlines, nextAirportAirlines } = options; + + if (savedProfileAirlines.length === 0) { + return previousSelectedAirlines.length > 0 ? [] : null; + } + + const pruned = previousSelectedAirlines.filter(key => nextAirportAirlines.includes(key)); + return pruned.length === previousSelectedAirlines.length ? null : pruned; +} + export function getAirportAirlines(code: string | null | undefined): string[] { const normalized = isValidAirportCode(code) ? normalizeAirportCode(code) : DEFAULT_AIRPORT_CODE; return airportAirlinesCache[normalized] ?? AIRPORT_AIRLINES[normalized] ?? ALLOWED_AIRLINES; diff --git a/src/utils/flightScheduleAdapter.ts b/src/utils/flightScheduleAdapter.ts index eb68950d..a720a5a4 100644 --- a/src/utils/flightScheduleAdapter.ts +++ b/src/utils/flightScheduleAdapter.ts @@ -1,3 +1,5 @@ +import { AIRLINE_ALIASES, airlineAliasMatches, normalizeAirlineText } from './airlineAliases'; + export type FlightDirection = 'arrival' | 'departure'; export type FlightTimestampBucket = 'real' | 'estimated' | 'scheduled'; @@ -20,38 +22,6 @@ export function getFlightAirlineName(item: any): string { return String(item?.flight?.airline?.name ?? '').trim(); } -const AIRLINE_MATCH_ALIASES: Record = { - ryanair: ['ryanair', 'fr', 'ryr'], - easyjet: ['easyjet', 'easy jet', 'u2', 'ec', 'ds', 'eju', 'ezy', 'ezs'], - wizz: ['wizz', 'wizz air', 'w6', 'w4', 'w9', 'wzz', 'wmt', 'wuk'], - volotea: ['volotea', 'v7'], - vueling: ['vueling', 'vy'], - transavia: ['transavia', 'transavia france', 'transavia holland', 'hv', 'to', 'tra', 'tvf'], - 'aer lingus': ['aer lingus', 'ei'], - 'british airways': ['british airways', 'ba', 'baw'], - sas: ['sas', 'scandinavian', 'sk', 'sas'], - scandinavian: ['sas', 'scandinavian', 'sk', 'sas'], - flydubai: ['flydubai', 'fz', 'fdb'], - aeroitalia: ['aeroitalia', 'xz'], - 'air arabia maroc': ['air arabia maroc', '3o', 'mac'], - 'air arabia': ['air arabia', 'g9', 'abz'], - 'air dolomiti': ['air dolomiti', 'en', 'dla'], - buzz: ['buzz', 'rr', 'rys'], - dhl: ['dhl', 'qy', 'bcs'], - eurowings: ['eurowings', 'ew', 'ewg'], - 'ita airways': ['ita airways', 'az', 'ity'], - lufthansa: ['lufthansa', 'lh', 'dlh'], -}; - -function normalizeAirlineText(value: unknown): string { - if (typeof value !== 'string' && typeof value !== 'number') return ''; - return String(value).trim().toLowerCase().replace(/[^a-z0-9]+/g, ' ').replace(/\s+/g, ' ').trim(); -} - -function compactAirlineText(value: unknown): string { - return normalizeAirlineText(value).replace(/\s+/g, ''); -} - function getFlightNumberAirlinePrefix(item: any): string { const flightNumber = getFlightNumber(item).toUpperCase().replace(/[\s\-_]/g, ''); return flightNumber.match(/^([A-Z0-9]{2,3}?)(?=\d)/)?.[1] ?? ''; @@ -71,28 +41,14 @@ function getFlightAirlineIdentifiers(item: any): string[] { .map(value => String(value)); } -function airlineIdentifierMatchesAlias(identifier: string | number, alias: string): boolean { - const normalizedIdentifier = normalizeAirlineText(identifier); - const compactIdentifier = compactAirlineText(identifier); - const normalizedAlias = normalizeAirlineText(alias); - const compactAlias = compactAirlineText(alias); - if (!normalizedIdentifier || !normalizedAlias || !compactAlias) return false; - - if (compactAlias.length <= 3) { - return normalizedIdentifier.split(' ').includes(compactAlias) || compactIdentifier === compactAlias; - } - - return compactIdentifier.includes(compactAlias); -} - export function isFlightAirlineMatch(item: any, airlineKey: string): boolean { const normalizedKey = normalizeAirlineText(airlineKey); if (!normalizedKey) return false; - const aliases = AIRLINE_MATCH_ALIASES[normalizedKey] ?? [normalizedKey]; + const aliases = AIRLINE_ALIASES[normalizedKey] ?? [normalizedKey]; const identifiers = getFlightAirlineIdentifiers(item); return identifiers.some(identifier => - aliases.some(alias => airlineIdentifierMatchesAlias(identifier, alias)), + aliases.some(alias => airlineAliasMatches(identifier, alias)), ); } From 2460066ab41a677716ba12315db8d87861026820 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 12:15:46 +0000 Subject: [PATCH 02/10] fix(theme): raise light-theme contrast to WCAG AA Light-theme tokens failed WCAG checks when used as text: textMuted 2.39:1, success 2.54, warning 2.15, danger 3.76, info 3.68, inactive tab icons 2.01. Darken the semantic fills to their -600/-700 variants (>=4.5:1 on card and bg), raise textMuted alpha 0.45 -> 0.68 (~4.3:1, matching the dark theme) and tabIconInactive 0.38 -> 0.60 (>=3:1 for icons). Chip backgrounds (*Soft) keep the original bright tints. The dark theme already passed everywhere and is unchanged. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012vEkzMVahsn6d7vyL4xayt --- src/context/ThemeContext.tsx | 14 +- src/screens/ShiftScreen.tsx | 367 ----------------------------------- 2 files changed, 8 insertions(+), 373 deletions(-) delete mode 100644 src/screens/ShiftScreen.tsx diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx index 9dd8e25c..99c8c787 100644 --- a/src/context/ThemeContext.tsx +++ b/src/context/ThemeContext.tsx @@ -58,7 +58,7 @@ const LIGHT: ThemeColors = { cardSecondary: '#F2F2F7', text: '#1C1C1E', textSub: '#48484A', - textMuted: 'rgba(60,60,67,0.45)', + textMuted: 'rgba(60,60,67,0.68)', // alpha 0.45 → 0.68: a 2.39:1 non era leggibile; ora ~4.3:1, come nel tema scuro primary: '#F47B16', primaryDark: '#C2520A', primaryLight: '#FFEDD5', @@ -69,16 +69,18 @@ const LIGHT: ThemeColors = { appBar: 'rgba(242,242,247,0.85)', tabBar: 'rgba(255,255,255,0.90)', tabIconActive: '#F47B16', - tabIconInactive:'rgba(60,60,67,0.38)', + tabIconInactive:'rgba(60,60,67,0.60)', tabLabelActive: '#F47B16', pillActive: 'rgba(244,123,22,0.14)', - success: '#10B981', + // Fill semantici scuriti per reggere WCAG AA (≥4.5:1) usati come testo su + // card/bg. Gli sfondi chip *Soft restano sulle tinte brillanti originali. + success: '#047857', successSoft: 'rgba(16,185,129,0.13)', - warning: '#F59E0B', + warning: '#B45309', warningSoft: 'rgba(245,158,11,0.13)', - danger: '#EF4444', + danger: '#DC2626', dangerSoft: 'rgba(239,68,68,0.13)', - info: '#3B82F6', + info: '#2563EB', infoSoft: 'rgba(59,130,246,0.13)', neutral: '#64748B', statusBar: 'dark-content', diff --git a/src/screens/ShiftScreen.tsx b/src/screens/ShiftScreen.tsx deleted file mode 100644 index 64e5dc15..00000000 --- a/src/screens/ShiftScreen.tsx +++ /dev/null @@ -1,367 +0,0 @@ -import React, { useState, useRef } from 'react'; -import { View, Text, StyleSheet, ActivityIndicator, ScrollView, Alert, TouchableOpacity, Image, Linking } from 'react-native'; -import { MaterialIcons } from '@expo/vector-icons'; -import * as ImagePicker from 'expo-image-picker'; -import { WebView } from 'react-native-webview'; -import * as Calendar from 'expo-calendar'; -import { useLanguage } from '../context/LanguageContext'; -import { TYPE } from '../theme/typography'; - -const PRIMARY = '#F47B16'; -const DARK_ORANGE = '#C2520A'; -const BG = '#F3F4F6'; - -export default function ShiftScreen() { - const { t } = useLanguage(); - const [imageList, setImageList] = useState([]); - const [ocrText, setOcrText] = useState(''); - const [processing, setProcessing] = useState(false); - const webViewRef = useRef(null); - - const pickImage = async () => { - try { - let result = await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ['images'], - allowsMultipleSelection: true, - selectionLimit: 0, - orderedSelection: true, - quality: 1, - base64: true, - }); - - if (!result.canceled && result.assets && result.assets.length > 0) { - setImageList(result.assets.map(a => a.uri)); - setProcessing(true); - setOcrText(''); - - const base64List = result.assets - .map(a => a.base64 ? `data:${a.mimeType || 'image/jpeg'};base64,${a.base64}` : null) - .filter((item): item is string => !!item); - if (base64List.length === 0) { - setProcessing(false); - Alert.alert('Errore OCR', 'Nessuna immagine leggibile selezionata.'); - return; - } - const base64Json = JSON.stringify(base64List); - - const jsCode = ` - if (window.runTesseract) { - window.runTesseract(${JSON.stringify(base64Json)}); - } else { - window.ReactNativeWebView.postMessage(JSON.stringify({ success: false, error: "Motore OCR non pronto." })); - } - true; - `; - webViewRef.current?.injectJavaScript(jsCode); - } - } catch (e) { - Alert.alert("Errore OCR", "Impossibile elaborare l'immagine."); - setProcessing(false); - } - }; - - const handleWebViewMessage = (event: any) => { - const rawData = event.nativeEvent.data; - try { - const result = JSON.parse(rawData); - if (result.success) { - setOcrText(result.text); - } else { - Alert.alert("Errore", "Impossibile analizzare il documento: " + result.error); - } - } catch(e) { - console.error(e); - } finally { - setProcessing(false); - } - }; - - const parseAndSaveShifts = async () => { - const { status, canAskAgain } = await Calendar.requestCalendarPermissionsAsync(); - if (status !== 'granted') { - if (!canAskAgain) { - Alert.alert("Permesso negato", "Abilita l'accesso al calendario nelle impostazioni del dispositivo.", [ - { text: 'Annulla', style: 'cancel' }, - { text: 'Apri Impostazioni', onPress: () => Linking.openSettings() }, - ]); - } else { - Alert.alert("Permesso negato", "Devi autorizzare l'accesso al calendario del telefono."); - } - return; - } - - try { - const calendars = await Calendar.getCalendarsAsync(Calendar.EntityTypes.EVENT); - // Su iOS isPrimary è comodo, su Android cerchiamo un calendario che accetti scritture - let targetCalendar = calendars.find(c => c.allowsModifications && c.isPrimary); - if (!targetCalendar) { - targetCalendar = calendars.find(c => c.allowsModifications); - } - - if (!targetCalendar) { - Alert.alert('Errore', t('shiftNoCalendar')); - return; - } - // Normalizzazione Estrema OCR globale prima di estrarre - const norText = ocrText.replace(/[OoQ]/g, '0').replace(/[Il|]/g, '1'); - - // Estrai tutte le date in ordine compatto - const dateRegex = /\b(\d{2})[\/\-](\d{2})[\/\-](\d{4})\b/g; - const dates: any[] = []; - let matchDate; - while ((matchDate = dateRegex.exec(norText)) !== null) { - dates.push({ - day: parseInt(matchDate[1], 10), - month: parseInt(matchDate[2], 10) - 1, // JS months are 0-indexed - year: parseInt(matchDate[3], 10), - raw: matchDate[0] - }); - } - - // Nascondiamo gli anni a 4 cifre per evitare che "2026" possa essere letto come l'orario "20:26" dall'OCR - const safeTextForTimes = norText.replace(/\b20\d{2}\b/g, ' ANNO '); - - // Estrai tutti i turni (orari o Riposo) in ordine compatto - // Tolto il flag 'i' per evitare falsi positivi sulla lettera 'r' minuscola (es. o[r]ario, ma[r]tedì, ecc.) - const shiftRegex = /\b([01]?\d|2\d)[.,:]?(\d{2})\s*[-–—_~|]+\s*([01]?\d|2\d)[.,:]?(\d{2})\b|\b(R|RIP|RIP0S0|R1P0S0|R1POSO)\b/g; - const shifts: any[] = []; - let matchShift; - while ((matchShift = shiftRegex.exec(safeTextForTimes)) !== null) { - if (matchShift[5]) { - shifts.push({ isRest: true, raw: matchShift[0] }); - } else { - shifts.push({ - isRest: false, - startH: parseInt(matchShift[1], 10), startM: parseInt(matchShift[2], 10), - endH: parseInt(matchShift[3], 10), endM: parseInt(matchShift[4], 10), - raw: matchShift[0] - }); - } - } - - let savedCount = 0; - // ZIP degli array: Associa la prima data al primo turno, la seconda al secondo, ecc. - // E' perfetto per le estrazioni in colonna! - const iterCount = Math.min(dates.length, shifts.length); - - for (let i = 0; i < iterCount; i++) { - const d = dates[i]; - const s = shifts[i]; - - // --- PREVENZIONE DUPLICATI --- - // Controlliamo l'intera giornata per evitare sovrascritture se l'operazione viene ripetuta - const dayStart = new Date(d.year, d.month, d.day, 0, 0, 0); - const dayEnd = new Date(d.year, d.month, d.day, 23, 59, 59); - const existingEvents = await Calendar.getEventsAsync([targetCalendar.id], dayStart, dayEnd); - - const isDuplicate = existingEvents.some(e => { - if (s.isRest) { - return e.title.includes("Riposo"); - } else { - // Verifica se c'è già un turno di lavoro che inizia alla stessa ora - const eStart = new Date(e.startDate); - return e.title.includes("Lavoro") && eStart.getHours() === s.startH; - } - }); - - if (isDuplicate) { - continue; // Salta alla prossima iterazione senza aggiungere - } - // ------------------------------ - - if (s.isRest) { - const alldayStart = new Date(d.year, d.month, d.day, 12, 0, 0); - const alldayEnd = new Date(d.year, d.month, d.day, 14, 0, 0); - await Calendar.createEventAsync(targetCalendar.id, { - title: 'Riposo', - startDate: alldayStart, - endDate: alldayEnd, - allDay: true, - notes: "Dati estratti: " + d.raw + " -> " + s.raw, - timeZone: 'Europe/Rome', - }); - savedCount++; - } else { - const startDate = new Date(d.year, d.month, d.day); - startDate.setHours(s.startH, s.startM, 0, 0); - - let endDate = new Date(d.year, d.month, d.day); - endDate.setHours(s.endH, s.endM, 0, 0); - - if (endDate <= startDate) { - endDate.setDate(endDate.getDate() + 1); // Notturno - } - - await Calendar.createEventAsync(targetCalendar.id, { - title: 'Turno Lavoro', - startDate: startDate, - endDate: endDate, - notes: "Dati estratti: " + d.raw + " -> " + s.raw, - timeZone: 'Europe/Rome', - }); - savedCount++; - } - } - - if (savedCount > 0) { - Alert.alert( - t('shiftSyncOkTitle'), - `${savedCount} turni salvati nel calendario.` - ); - } else { - Alert.alert("Nessun orario trovato", `Errore estrazione. Date Trovate: ${dates.length}, Orari Trovati: ${shifts.length}. Assicurati di scansionare bene le colonne.`); - } - - } catch (e: any) { - console.error(e); - Alert.alert(t('shiftCalErrTitle'), 'Non è stato possibile salvare: ' + e.message); - } - }; - - const engineHtml = ` - - - - - - - - - - - `; - - return ( - - - - - - {/* Page Header */} - - {t('shiftTitle')} - {t('shiftSub')} - - - - - - {t('shiftSyncTitle')} - - - Seleziona gli screenshot del tuo tabellone orari. Il sistema li leggerà per cercare e salvare automaticamente i voli nel calendario del tuo telefono. - - - - - - - - {t('shiftScanBtn')} - - - - - {imageList.length > 0 && ( - - {imageList.map((uri, index) => ( - - ))} - - )} - - {processing && ( - - - {t('shiftExtracting')} - - )} - - {ocrText ? ( - - {t('shiftExtractedTitle')} - {ocrText} - - - - - {t('shiftSyncBtn')} - - - - ) : null} - - ); -} - -const styles = StyleSheet.create({ - container: { - flexGrow: 1, - backgroundColor: BG, - paddingBottom: 32, - }, - hiddenWebView: { height: 1, width: 1, opacity: 0, position: 'absolute', top: -100 }, - pageHeader: { - backgroundColor: '#fff', - paddingHorizontal: 16, paddingVertical: 14, - borderBottomWidth: 1, borderBottomColor: '#E5E7EB', - }, - pageTitle: { ...TYPE.title, color: DARK_ORANGE }, - pageSub: { fontSize: 13, color: '#6B7280', marginTop: 4 }, - infoCard: { - backgroundColor: '#fff', - margin: 16, marginBottom: 0, - padding: 16, borderRadius: 14, - borderLeftWidth: 4, borderLeftColor: PRIMARY, - shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 6, elevation: 2, - }, - infoTitleRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 8 }, - infoTitle: { ...TYPE.subhead, color: PRIMARY }, - infoDesc: { fontSize: 13, color: '#6B7280', lineHeight: 20 }, - buttonsContainer: { margin: 16, marginBottom: 0 }, - button: { - backgroundColor: DARK_ORANGE, - padding: 16, borderRadius: 14, - alignItems: 'center', - shadowColor: DARK_ORANGE, shadowOpacity: 0.3, shadowRadius: 8, elevation: 5, - }, - buttonInner: { flexDirection: 'row', alignItems: 'center', gap: 8 }, - buttonText: { ...TYPE.subhead, color: '#fff' }, - imagesPreview: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', margin: 16, gap: 10 }, - image: { width: '45%', height: 140, resizeMode: 'cover', borderRadius: 10, borderWidth: 1, borderColor: '#E5E7EB' }, - loadingContainer: { marginTop: 24, alignItems: 'center' }, - loadingText: { marginTop: 10, color: '#6B7280', fontWeight: '500' }, - resultContainer: { - margin: 16, padding: 16, - backgroundColor: '#fff', borderRadius: 14, - shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 8, elevation: 3, - }, - resultTitle: { ...TYPE.subhead, color: DARK_ORANGE, marginBottom: 10, borderBottomWidth: 1, borderBottomColor: '#E5E7EB', paddingBottom: 8 }, - resultText: { fontSize: 13, color: '#374151', lineHeight: 20, marginBottom: 16 }, - saveButton: { - backgroundColor: PRIMARY, - padding: 15, borderRadius: 12, alignItems: 'center', - shadowColor: PRIMARY, shadowOpacity: 0.3, shadowRadius: 6, elevation: 4, - }, - saveButtonText: { ...TYPE.subhead, color: '#fff' }, -}); - From 349144270c1d3d32b61a016cab2f2f31374caadf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 12:15:46 +0000 Subject: [PATCH 03/10] chore: remove dead ShiftScreen and literal 'bold' font weights ShiftScreen is not mounted anywhere (App.tsx routes tabs to HomeScreen for shifts) and was the only theme-blind screen left; drop it together with its 16 orphaned translation keys. Replace the remaining fontWeight: 'bold' literals with WEIGHT.semibold ('700', same rendered weight) per the typography scale rule. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012vEkzMVahsn6d7vyL4xayt --- src/i18n/translations.ts | 30 ------------------------------ src/screens/CalendarScreen.tsx | 4 ++-- src/screens/FlightScreen.tsx | 6 +++--- src/screens/TraveldocScreen.tsx | 3 ++- src/widgets/ShiftWidget.tsx | 23 ++++++++++++----------- 5 files changed, 19 insertions(+), 47 deletions(-) diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index e3750ea4..832b6b9d 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -137,21 +137,6 @@ const it = { traveldocSub: 'Verifica documenti di viaggio', traveldocLoading: 'Caricamento TravelDoc…', traveldocSlowConn: 'Caricamento lento. Verifica la connessione internet.', - // ShiftScreen - shiftTitle: 'Gestione Turni', - shiftSub: 'Scansiona i turni dal tabellone e sincronizzali nel calendario.', - shiftSyncTitle: 'Sincronizzazione Calendario', - shiftSyncDesc: 'Seleziona gli screenshot del tuo tabellone orari...', - shiftScanBtn: 'Scansiona Screenshot Turni', - shiftExtracting: 'Estrazione del testo in corso...', - shiftExtractedTitle: 'Testo Estratto:', - shiftSyncBtn: 'Sincronizza nel Calendario!', - shiftErrOcrTitle: 'Errore OCR', shiftErrOcrMsg: "Impossibile elaborare l'immagine.", - shiftPermTitle: 'Permesso negato', - shiftPermMsg: "Devi autorizzare l'accesso al calendario del telefono.", - shiftNoCalendar: 'Nessun calendario scrivibile trovato sul dispositivo.', - shiftSyncOkTitle: 'Turni Sincronizzati!', - shiftNoShifts: 'Nessun orario trovato', shiftCalErrTitle: 'Errore Calendario', // Calendar calTitle: 'Gestione Turni', calEditBtn: 'Modifica Turni', calModeCalendar: 'Calendario', calModeWeek: 'Settimana', calModeMonthHours: 'Ore mese', @@ -438,21 +423,6 @@ const en: typeof it = { traveldocSub: 'Travel document check', traveldocLoading: 'Loading TravelDoc…', traveldocSlowConn: 'Slow loading. Check your internet connection.', - // ShiftScreen - shiftTitle: 'Shift Manager', - shiftSub: 'Scan shifts from the schedule board and sync them to the calendar.', - shiftSyncTitle: 'Calendar Sync', - shiftSyncDesc: 'Select screenshots of your schedule board...', - shiftScanBtn: 'Scan Shift Screenshots', - shiftExtracting: 'Extracting text...', - shiftExtractedTitle: 'Extracted Text:', - shiftSyncBtn: 'Sync to Calendar!', - shiftErrOcrTitle: 'OCR Error', shiftErrOcrMsg: 'Could not process the image.', - shiftPermTitle: 'Permission denied', - shiftPermMsg: 'You need to grant access to the phone calendar.', - shiftNoCalendar: 'No writable calendar found on the device.', - shiftSyncOkTitle: 'Shifts Synced!', - shiftNoShifts: 'No schedules found', shiftCalErrTitle: 'Calendar Error', // Calendar calTitle: 'Shift Manager', calEditBtn: 'Edit Shifts', calModeCalendar: 'Calendar', calModeWeek: 'Week', calModeMonthHours: 'Month hours', diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index 36134d78..d058637a 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -32,7 +32,7 @@ import { type ParsedSchedule, type ParsedEmployee, } from '../utils/pdfShiftParser'; import { useLanguage } from '../context/LanguageContext'; -import { TYPE } from '../theme/typography'; +import { TYPE, WEIGHT } from '../theme/typography'; const STORAGE_KEY = '@shift_import_name'; @@ -1357,7 +1357,7 @@ function makeStyles(c: ThemeColors) { secondaryBtn: { flex: 1, paddingVertical: 12, borderRadius: 10, alignItems: 'center', borderWidth: 1 }, secondaryBtnText: { fontSize: 14, fontWeight: '600' }, primaryBtn: { flex: 2, paddingVertical: 12, borderRadius: 10, alignItems: 'center' }, - primaryBtnText: { color: '#fff', fontSize: 14, fontWeight: 'bold' }, + primaryBtnText: { color: '#fff', fontSize: 14, fontWeight: WEIGHT.semibold }, // Edit menu editMenuContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, paddingBottom: 40 }, editMenuOption: { flexDirection: 'row', alignItems: 'center', gap: 14, padding: 16, borderRadius: 14, marginBottom: 10 }, diff --git a/src/screens/FlightScreen.tsx b/src/screens/FlightScreen.tsx index 13a9a51b..47d89fcb 100644 --- a/src/screens/FlightScreen.tsx +++ b/src/screens/FlightScreen.tsx @@ -82,7 +82,7 @@ import { schedulePinnedNotifications, scheduleShiftNotifications, } from '../utils/flightNotificationScheduler'; -import { TYPE } from '../theme/typography'; +import { TYPE, WEIGHT } from '../theme/typography'; const WearDataSender = Platform.OS === 'android' ? NativeModules.WearDataSender : null; @@ -1550,10 +1550,10 @@ function makeStyles(c: ThemeColors, isOperations = false) { card: { backgroundColor: operationPanel, borderRadius: isOperations ? 18 : 16, marginBottom: 10, overflow: 'hidden', shadowColor: c.primary, shadowOpacity: isOperations || c.isDark ? 0 : 0.08, shadowRadius: 10, elevation: isOperations || c.isDark ? 0 : 3, borderWidth: 1, borderColor: operationBorder, borderLeftWidth: isOperations ? 4 : 1 }, cardShift: { borderWidth: 1.5, borderColor: c.warning }, shiftBanner: { backgroundColor: c.warning, paddingVertical: 5, paddingHorizontal: 12 }, - shiftBannerText: { color: '#fff', fontWeight: 'bold', fontSize: 11, letterSpacing: 0.5 }, + shiftBannerText: { color: '#fff', fontWeight: WEIGHT.semibold, fontSize: 11, letterSpacing: 0.5 }, cardPinned: { borderWidth: 2, borderColor: c.warning }, pinBanner: { backgroundColor: isOperations ? 'rgba(245,158,11,0.18)' : c.warning, paddingVertical: 5, paddingHorizontal: 12, borderBottomWidth: isOperations ? 1 : 0, borderBottomColor: 'rgba(245,158,11,0.28)' }, - pinBannerText: { color: isOperations ? '#FBBF24' : '#fff', fontWeight: 'bold', fontSize: 11, letterSpacing: 0.5 }, + pinBannerText: { color: isOperations ? '#FBBF24' : '#fff', fontWeight: WEIGHT.semibold, fontSize: 11, letterSpacing: 0.5 }, statusPill: { paddingHorizontal: 10, paddingVertical: isOperations ? 3 : 4, borderRadius: isOperations ? 10 : 20, marginTop: isOperations ? 6 : 8, alignSelf: 'flex-end', borderWidth: isOperations ? 1 : 0, borderColor: isOperations ? operationBorderSoft : 'transparent' }, statusText: { ...TYPE.micro, letterSpacing: isOperations ? 0.6 : 0 }, cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: isOperations ? 9 : 10, paddingHorizontal: 14, borderBottomWidth: isOperations ? 1 : 0, borderBottomColor: operationBorderSoft }, diff --git a/src/screens/TraveldocScreen.tsx b/src/screens/TraveldocScreen.tsx index f45b7f1d..f746647e 100644 --- a/src/screens/TraveldocScreen.tsx +++ b/src/screens/TraveldocScreen.tsx @@ -5,6 +5,7 @@ import { WebView } from 'react-native-webview'; import { useAppTheme } from '../context/ThemeContext'; import { useLanguage } from '../context/LanguageContext'; import TactilePressable from '../components/motion/TactilePressable'; +import { WEIGHT } from '../theme/typography'; const DARK_CSS_JS = ` (function() { @@ -98,7 +99,7 @@ export default function TraveldocScreen({ isFocused = true }: { isFocused?: bool const styles = StyleSheet.create({ header: { paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1 }, - title: { fontSize: 22, fontWeight: 'bold' }, + title: { fontSize: 22, fontWeight: WEIGHT.semibold }, sub: { fontSize: 12, marginTop: 2 }, loadingWrap: { position: 'absolute', top: 60, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', zIndex: 10, paddingHorizontal: 20 }, loadingText: { marginTop: 12, fontSize: 14, textAlign: 'center' }, diff --git a/src/widgets/ShiftWidget.tsx b/src/widgets/ShiftWidget.tsx index 2a288c6e..14e0d761 100644 --- a/src/widgets/ShiftWidget.tsx +++ b/src/widgets/ShiftWidget.tsx @@ -3,6 +3,7 @@ import { FlexWidget, TextWidget, ListWidget } from 'react-native-android-widget' import type { ThemeMode, ThemeSnapshotColors } from '../utils/themeMode'; import type { WidgetData, WidgetFlight } from './widgetTaskHandler'; import { getWidgetThemePalette, type WidgetThemePalette } from './widgetTheme'; +import { WEIGHT } from '../theme/typography'; const PILL_R = 10; @@ -53,7 +54,7 @@ function FlightRow({ > @@ -90,7 +91,7 @@ function FlightRow({ alignItems: 'center', }} > - + - + @@ -122,7 +123,7 @@ function FlightRow({ alignItems: 'center', }} > - + - + - + @@ -192,7 +193,7 @@ function Header({ label, theme }: { label?: string; theme: WidgetThemePalette }) /> @@ -258,13 +259,13 @@ export function ShiftWidget({ data, themeMode = 'light', themeSnapshot }: ShiftW /> From ca814cf6b5abbe5f4a14ad1af9af7fbbbf49ae26 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 12:19:41 +0000 Subject: [PATCH 04/10] refactor: extend the TYPE scale to remaining screens and shared components Conservative pass, same rules as the first migration: exact size+weight matches and clear roles only (screen/modal titles -> headline, 22/w titles -> title, 16/700 -> subhead, 13/600 -> callout, 12/600 -> caption, 10/800 -> micro). Touches Manuals, Phonebook, Password, Notepad, Traveldoc, ProfileSwitcherModal, UpdateModal, ShiftTimeline. Onboarding, ArionInbox, DrawerMenuPanel, FlightSourceDebugModal and FlightStates keep their intentional heavy-900 operations styling; off-scale sizes (14/17/28) were left inline on purpose. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012vEkzMVahsn6d7vyL4xayt --- src/components/ProfileSwitcherModal.tsx | 7 +++---- src/components/ShiftTimeline.tsx | 3 ++- src/components/UpdateModal.tsx | 4 ++-- src/screens/ManualsScreen.tsx | 13 +++++++------ src/screens/NotepadScreen.tsx | 5 +++-- src/screens/PasswordScreen.tsx | 9 +++++---- src/screens/PhonebookScreen.tsx | 11 ++++++----- src/screens/TraveldocScreen.tsx | 4 ++-- 8 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/components/ProfileSwitcherModal.tsx b/src/components/ProfileSwitcherModal.tsx index 1c61cabf..1d22175b 100644 --- a/src/components/ProfileSwitcherModal.tsx +++ b/src/components/ProfileSwitcherModal.tsx @@ -13,6 +13,7 @@ import { MaterialIcons } from '@expo/vector-icons'; import { useAirport, type AirportProfile } from '../context/AirportContext'; import { useLanguage } from '../context/LanguageContext'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; +import { TYPE } from '../theme/typography'; import { AIRPORT_PRESETS, formatAirportSettingLabel, @@ -388,8 +389,7 @@ function makeStyles(colors: ThemeColors) { borderBottomColor: colors.border, }, title: { - fontSize: 18, - fontWeight: '800', + ...TYPE.headline, color: colors.text, }, subtitle: { @@ -463,8 +463,7 @@ function makeStyles(colors: ThemeColors) { backgroundColor: colors.primary, }, activePillText: { - fontSize: 10, - fontWeight: '800', + ...TYPE.micro, color: '#fff', letterSpacing: 0.4, }, diff --git a/src/components/ShiftTimeline.tsx b/src/components/ShiftTimeline.tsx index 1cac4496..2df62147 100644 --- a/src/components/ShiftTimeline.tsx +++ b/src/components/ShiftTimeline.tsx @@ -6,6 +6,7 @@ import { import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; +import { TYPE } from '../theme/typography'; import { useAirport } from '../context/AirportContext'; import { getAirlineOps, getAirlineColor } from '../utils/airlineOps'; import { fetchAirportScheduleRaw } from '../utils/fr24api'; @@ -321,7 +322,7 @@ function makeStyles(c: ThemeColors) { handleRow: { alignItems: 'center', paddingTop: 10, paddingBottom: 6 }, handle: { width: 36, height: 4, borderRadius: 2 }, header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingBottom: 10 }, - title: { fontSize: 18, fontWeight: '800' }, + title: { ...TYPE.headline }, subtitle: { fontSize: 12, marginTop: 2 }, closeBtn: { padding: 8, borderRadius: 20 }, legend: { flexDirection: 'row', gap: 16, paddingHorizontal: 20, paddingBottom: 12 }, diff --git a/src/components/UpdateModal.tsx b/src/components/UpdateModal.tsx index 64abb970..8dba5019 100644 --- a/src/components/UpdateModal.tsx +++ b/src/components/UpdateModal.tsx @@ -4,6 +4,7 @@ import { } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme } from '../context/ThemeContext'; +import { TYPE } from '../theme/typography'; import { type UpdateInfo, APP_VERSION } from '../utils/updateChecker'; import { downloadUpdatePackage, @@ -286,8 +287,7 @@ const styles = StyleSheet.create({ }, statusText: { flex: 1, - fontSize: 13, - fontWeight: '600', + ...TYPE.callout, }, progressTrack: { height: 8, diff --git a/src/screens/ManualsScreen.tsx b/src/screens/ManualsScreen.tsx index 0aed1ec7..9dea6b60 100644 --- a/src/screens/ManualsScreen.tsx +++ b/src/screens/ManualsScreen.tsx @@ -6,6 +6,7 @@ import { } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; +import { TYPE } from '../theme/typography'; import { enableLegacyAndroidLayoutAnimation } from '../utils/layoutAnimation'; const STORAGE_KEY = 'manuals_data_v2'; @@ -429,7 +430,7 @@ function makeItemStyles(c: ThemeColors) { flexDirection: 'row', alignItems: 'center', gap: 10, padding: 13, }, - title: { fontSize: 13, fontWeight: '600', color: c.text, flex: 1 }, + title: { ...TYPE.callout, color: c.text, flex: 1 }, body: { paddingHorizontal: 14, paddingBottom: 14, paddingTop: 2, borderTopWidth: 1, borderTopColor: c.cardSecondary, @@ -565,8 +566,8 @@ const modalStyles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }, scrollContent: { flexGrow: 1, justifyContent: 'flex-end' }, sheet: { borderTopLeftRadius: 20, borderTopRightRadius: 20, padding: 20, paddingBottom: 36, maxHeight: '92%' }, - title: { fontSize: 17, fontWeight: '700', marginBottom: 16 }, - label: { fontSize: 12, fontWeight: '600', marginBottom: 4, marginTop: 12 }, + title: { ...TYPE.headline, marginBottom: 16 }, + label: { ...TYPE.caption, marginBottom: 4, marginTop: 12 }, input: { borderWidth: 1, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 9, fontSize: 14 }, inputMulti: { minHeight: 100, paddingTop: 9 }, colorRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, marginTop: 8 }, @@ -591,7 +592,7 @@ function makeStyles(c: ThemeColors) { backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border, }, - headerTitle: { fontSize: 17, fontWeight: '700', color: c.primaryDark }, + headerTitle: { ...TYPE.headline, color: c.primaryDark }, airlineBar: { backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border, @@ -607,7 +608,7 @@ function makeStyles(c: ThemeColors) { backgroundColor: c.card, }, airlineCode: { fontSize: 11, fontWeight: '800', color: c.textSub }, - airlineName: { fontSize: 12, fontWeight: '600', color: c.textSub }, + airlineName: { ...TYPE.caption, color: c.textSub }, content: { flex: 1 }, contentPad: { padding: 14, paddingBottom: 80 }, banner: { @@ -622,7 +623,7 @@ function makeStyles(c: ThemeColors) { borderWidth: 1, borderStyle: 'dashed', borderRadius: 8, marginBottom: 8, }, - addBtnText: { fontSize: 13, fontWeight: '600' }, + addBtnText: { ...TYPE.callout }, }); } diff --git a/src/screens/NotepadScreen.tsx b/src/screens/NotepadScreen.tsx index 9114a9e1..898203fa 100644 --- a/src/screens/NotepadScreen.tsx +++ b/src/screens/NotepadScreen.tsx @@ -6,6 +6,7 @@ import { import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; +import { TYPE } from '../theme/typography'; import { useLanguage } from '../context/LanguageContext'; const STORAGE_KEY = 'aerostaff_notepad_v1'; @@ -20,7 +21,7 @@ function makeStyles(c: ThemeColors) { borderBottomWidth: 1, borderBottomColor: c.border, }, titleRow: { flexDirection: 'row', alignItems: 'center', gap: 8 }, - title: { fontSize: 17, fontWeight: '700', color: c.primaryDark }, + title: { ...TYPE.headline, color: c.primaryDark }, actions: { flexDirection: 'row', alignItems: 'center', gap: 8 }, iconBtn: { padding: 8, borderRadius: 10 }, saveBtn: { @@ -31,7 +32,7 @@ function makeStyles(c: ThemeColors) { // Dims the entire save button (background + icon + label) when content is // already saved — intentional: the full-button fade signals an inactive state. saveBtnDim: { opacity: 0.55 }, - saveTxt: { color: '#fff', fontWeight: '600', fontSize: 13 }, + saveTxt: { ...TYPE.callout, color: '#fff' }, statusBar: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 16, paddingVertical: 6, diff --git a/src/screens/PasswordScreen.tsx b/src/screens/PasswordScreen.tsx index f706c3b1..1c2ec7c9 100644 --- a/src/screens/PasswordScreen.tsx +++ b/src/screens/PasswordScreen.tsx @@ -7,6 +7,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import * as SecureStore from 'expo-secure-store'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; +import { TYPE } from '../theme/typography'; import { useLanguage } from '../context/LanguageContext'; import { secureWipeAsyncStorageItem } from '../utils/secureWipe'; @@ -396,7 +397,7 @@ function makePinStyles(c: ThemeColors) { return StyleSheet.create({ overlay: { flex: 1, backgroundColor: c.bg, justifyContent: 'center', alignItems: 'center' }, box: { alignItems: 'center', padding: 32, width: '100%', maxWidth: 320 }, - title: { fontSize: 16, fontWeight: '700', color: c.text, marginBottom: 24 }, + title: { ...TYPE.subhead, color: c.text, marginBottom: 24 }, dots: { flexDirection: 'row', gap: 16, marginBottom: 32 }, dot: { width: 16, height: 16, borderRadius: 8, borderWidth: 2, borderColor: c.primary, backgroundColor: 'transparent' }, dotFilled: { backgroundColor: c.primary }, @@ -428,7 +429,7 @@ function makeStyles(c: ThemeColors) { root: { flex: 1, backgroundColor: c.bg }, toolbar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 12, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border }, titleRow: { flexDirection: 'row', alignItems: 'center', gap: 8 }, - title: { fontSize: 17, fontWeight: '700', color: c.primaryDark }, + title: { ...TYPE.headline, color: c.primaryDark }, toolbarActions:{ flexDirection: 'row', alignItems: 'center', gap: 8 }, iconBtn: { width: 36, height: 36, borderRadius: 10, backgroundColor: c.cardSecondary, justifyContent: 'center', alignItems: 'center' }, iconBtnActive:{ backgroundColor: c.primary }, @@ -440,8 +441,8 @@ function makeStyles(c: ThemeColors) { modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }, modalScrollContent: { flexGrow: 1, justifyContent: 'flex-end' }, modalBox: { backgroundColor: c.card, borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, paddingBottom: Platform.OS === 'ios' ? 40 : 24, maxHeight: '92%' }, - modalTitle: { fontSize: 18, fontWeight: '700', color: c.primaryDark, marginBottom: 20 }, - label: { fontSize: 12, fontWeight: '600', color: c.textSub, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 0.5 }, + modalTitle: { ...TYPE.headline, color: c.primaryDark, marginBottom: 20 }, + label: { ...TYPE.caption, color: c.textSub, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 0.5 }, input: { backgroundColor: c.bg, borderWidth: 1, borderColor: c.border, borderRadius: 10, paddingHorizontal: 14, paddingVertical: 10, fontSize: 15, color: c.text, marginBottom: 14 }, inputMulti: { height: 80, paddingTop: 10 }, pwInputRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 14 }, diff --git a/src/screens/PhonebookScreen.tsx b/src/screens/PhonebookScreen.tsx index b45efbb9..e82ae6c3 100644 --- a/src/screens/PhonebookScreen.tsx +++ b/src/screens/PhonebookScreen.tsx @@ -6,6 +6,7 @@ import { import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; +import { TYPE } from '../theme/typography'; import { useLanguage } from '../context/LanguageContext'; const STORAGE_KEY = 'aerostaff_phonebook_v1'; @@ -58,8 +59,8 @@ function makeModalStyles(c: ThemeColors) { backgroundColor: c.border, alignSelf: 'center', marginBottom: 18, }, - title: { fontSize: 18, fontWeight: '700', color: c.primaryDark, marginBottom: 16 }, - label: { fontSize: 12, fontWeight: '600', color: c.textSub, marginBottom: 6, marginTop: 12 }, + title: { ...TYPE.headline, color: c.primaryDark, marginBottom: 16 }, + label: { ...TYPE.caption, color: c.textSub, marginBottom: 6, marginTop: 12 }, input: { borderWidth: 1.5, borderColor: c.border, borderRadius: 12, paddingHorizontal: 14, paddingVertical: 11, @@ -71,7 +72,7 @@ function makeModalStyles(c: ThemeColors) { borderRadius: 20, borderWidth: 1.5, borderColor: c.border, marginRight: 8, backgroundColor: c.card, }, - catTxt: { fontSize: 12, fontWeight: '600', color: c.textSub }, + catTxt: { ...TYPE.caption, color: c.textSub }, actions: { flexDirection: 'row', gap: 10, marginTop: 20 }, cancelBtn: { flex: 1, borderWidth: 1.5, borderColor: c.border, @@ -300,7 +301,7 @@ function makeStyles(c: ThemeColors) { backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border, }, - headerTitle: { fontSize: 17, fontWeight: '700', color: c.primaryDark, flex: 1 }, + headerTitle: { ...TYPE.headline, color: c.primaryDark, flex: 1 }, addBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: c.primary, borderRadius: 10, @@ -322,7 +323,7 @@ function makeStyles(c: ThemeColors) { backgroundColor: c.card, }, filterChipActive: { backgroundColor: c.primary, borderColor: c.primary }, - filterTxt: { fontSize: 12, fontWeight: '600', color: c.textSub }, + filterTxt: { ...TYPE.caption, color: c.textSub }, filterTxtActive: { color: '#fff' }, list: { flex: 1 }, listPad: { padding: 16, paddingBottom: 96 }, diff --git a/src/screens/TraveldocScreen.tsx b/src/screens/TraveldocScreen.tsx index f746647e..4f4235fe 100644 --- a/src/screens/TraveldocScreen.tsx +++ b/src/screens/TraveldocScreen.tsx @@ -5,7 +5,7 @@ import { WebView } from 'react-native-webview'; import { useAppTheme } from '../context/ThemeContext'; import { useLanguage } from '../context/LanguageContext'; import TactilePressable from '../components/motion/TactilePressable'; -import { WEIGHT } from '../theme/typography'; +import { TYPE, WEIGHT } from '../theme/typography'; const DARK_CSS_JS = ` (function() { @@ -99,7 +99,7 @@ export default function TraveldocScreen({ isFocused = true }: { isFocused?: bool const styles = StyleSheet.create({ header: { paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1 }, - title: { fontSize: 22, fontWeight: WEIGHT.semibold }, + title: { ...TYPE.title }, sub: { fontSize: 12, marginTop: 2 }, loadingWrap: { position: 'absolute', top: 60, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', zIndex: 10, paddingHorizontal: 20 }, loadingText: { marginTop: 12, fontSize: 14, textAlign: 'center' }, From 6f7d0c0059dd247638ac3b0cf6701259b17f7acf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 12:24:53 +0000 Subject: [PATCH 05/10] feat(a11y): label all icon-only buttons for screen readers Icon-only touchables (close, edit, delete, call, refresh, steppers, password reveal, webview nav, app-bar back/menu) announced as unnamed buttons under TalkBack: 8 accessibilityLabels across 156 touchables. Add accessibilityRole="button" and localized labels (new a11y* keys in both languages) to all 38 icon-only sites across screens, shared modals and the app bar. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012vEkzMVahsn6d7vyL4xayt --- App.tsx | 4 ++-- src/components/DrawerMenuPanel.tsx | 6 ++++-- src/components/ProfileSwitcherModal.tsx | 3 ++- src/components/ShiftTimeline.tsx | 2 +- .../flights/FlightNotificationSettingsModal.tsx | 4 ++++ src/components/flights/FlightSourceDebugModal.tsx | 2 +- src/i18n/translations.ts | 14 ++++++++++++++ src/screens/ArionInboxScreen.tsx | 6 ++++++ src/screens/CalendarScreen.tsx | 4 ++-- src/screens/ManualsScreen.tsx | 13 +++++++++---- src/screens/NotepadScreen.tsx | 2 +- src/screens/PasswordScreen.tsx | 11 ++++++----- src/screens/PhonebookScreen.tsx | 8 ++++---- src/screens/SettingsScreen.tsx | 6 ++++++ 14 files changed, 62 insertions(+), 23 deletions(-) diff --git a/App.tsx b/App.tsx index 40165789..e6641365 100644 --- a/App.tsx +++ b/App.tsx @@ -315,11 +315,11 @@ function AppInner() { /> )} {overlay ? ( - + ) : ( - setDrawerOpen(true)} animatedStyle={styles.iconBtn} depth={2} pressedScale={0.94} haptic="selection"> + setDrawerOpen(true)} animatedStyle={styles.iconBtn} depth={2} pressedScale={0.94} haptic="selection" accessibilityRole="button" accessibilityLabel={t('a11yOpenMenu')}> )} diff --git a/src/components/DrawerMenuPanel.tsx b/src/components/DrawerMenuPanel.tsx index ba29dc85..86f29205 100644 --- a/src/components/DrawerMenuPanel.tsx +++ b/src/components/DrawerMenuPanel.tsx @@ -3,6 +3,7 @@ import { StyleSheet, Text, View } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { type ThemeColors } from '../context/ThemeContext'; +import { useLanguage } from '../context/LanguageContext'; import AeroStaffLogo from './AeroStaffLogo'; import FrostedSurface from './FrostedSurface'; import BoardReveal from './motion/BoardReveal'; @@ -113,6 +114,7 @@ export default function DrawerMenuPanel({ onSelect, surfaceVariant = 'app', }: DrawerMenuPanelProps) { + const { t } = useLanguage(); const surface = getDrawerSurface(colors, surfaceVariant); const styles = useMemo(() => makeStyles(colors, surface), [colors, surface]); @@ -136,7 +138,7 @@ export default function DrawerMenuPanel({ Operations - + @@ -148,7 +150,7 @@ export default function DrawerMenuPanel({ style={styles.headerGradient} > - + diff --git a/src/components/ProfileSwitcherModal.tsx b/src/components/ProfileSwitcherModal.tsx index 1d22175b..91819294 100644 --- a/src/components/ProfileSwitcherModal.tsx +++ b/src/components/ProfileSwitcherModal.tsx @@ -184,7 +184,7 @@ export default function ProfileSwitcherModal({ visible, onClose }: Props) { {t('profileTitle')} {t('profileSubtitle')} - + @@ -226,6 +226,7 @@ export default function ProfileSwitcherModal({ visible, onClose }: Props) { style={styles.profileAction} onPress={() => openEditor(profile)} activeOpacity={0.8} + accessibilityRole="button" accessibilityLabel={t('profileEdit')} > diff --git a/src/components/ShiftTimeline.tsx b/src/components/ShiftTimeline.tsx index 2df62147..daa157c0 100644 --- a/src/components/ShiftTimeline.tsx +++ b/src/components/ShiftTimeline.tsx @@ -153,7 +153,7 @@ export default function ShiftTimeline({ visible, onClose, shiftStart, shiftEnd, Voli nel Turno {fmtTime(startSec)} – {fmtTime(endSec)} - + diff --git a/src/components/flights/FlightNotificationSettingsModal.tsx b/src/components/flights/FlightNotificationSettingsModal.tsx index e99e1303..e74fadbe 100644 --- a/src/components/flights/FlightNotificationSettingsModal.tsx +++ b/src/components/flights/FlightNotificationSettingsModal.tsx @@ -143,6 +143,7 @@ export default function FlightNotificationSettingsModal({ onPress={() => onUpdateNotificationSettings({ arrivalLeadMinutes: clamp(notifSettings.arrivalLeadMinutes - 1, MIN_NOTIF_MINUTES, MAX_NOTIF_MINUTES), }).catch(() => {})} + accessibilityRole="button" accessibilityLabel={t('a11yDecrease')} > @@ -152,6 +153,7 @@ export default function FlightNotificationSettingsModal({ onPress={() => onUpdateNotificationSettings({ arrivalLeadMinutes: clamp(notifSettings.arrivalLeadMinutes + 1, MIN_NOTIF_MINUTES, MAX_NOTIF_MINUTES), }).catch(() => {})} + accessibilityRole="button" accessibilityLabel={t('a11yIncrease')} > @@ -166,6 +168,7 @@ export default function FlightNotificationSettingsModal({ onPress={() => onUpdateNotificationSettings({ departureLeadMinutes: clamp(notifSettings.departureLeadMinutes - 1, MIN_NOTIF_MINUTES, MAX_NOTIF_MINUTES), }).catch(() => {})} + accessibilityRole="button" accessibilityLabel={t('a11yDecrease')} > @@ -175,6 +178,7 @@ export default function FlightNotificationSettingsModal({ onPress={() => onUpdateNotificationSettings({ departureLeadMinutes: clamp(notifSettings.departureLeadMinutes + 1, MIN_NOTIF_MINUTES, MAX_NOTIF_MINUTES), }).catch(() => {})} + accessibilityRole="button" accessibilityLabel={t('a11yIncrease')} > diff --git a/src/components/flights/FlightSourceDebugModal.tsx b/src/components/flights/FlightSourceDebugModal.tsx index d3cca80a..a48d0c10 100644 --- a/src/components/flights/FlightSourceDebugModal.tsx +++ b/src/components/flights/FlightSourceDebugModal.tsx @@ -75,7 +75,7 @@ export default function FlightSourceDebugModal({ {t('flightSourceDebugTitle')} {t('flightSourceDebugSub')} - + diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index 832b6b9d..85e56690 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -7,6 +7,13 @@ const it = { overlayNotepad: 'Blocco Note', overlayPhonebook: 'Rubrica', overlayPasswords: 'Password', overlayManuals: 'Manuali DCS', overlayArionInbox: 'Arion Inbox', overlaySettings: 'Impostazioni', // Common + // Accessibilità (label per screen reader) + a11yClose: 'Chiudi', a11yBack: 'Indietro', a11yForward: 'Avanti', + a11yRefresh: 'Aggiorna', a11yOpenInBrowser: 'Apri nel browser', + a11yClearSearch: 'Cancella ricerca', a11yEdit: 'Modifica', + a11yShowPassword: 'Mostra password', a11yHidePassword: 'Nascondi password', + a11yBackspace: 'Cancella ultima cifra', a11yCall: 'Chiama', + a11yOpenMenu: 'Apri menu', a11yDecrease: 'Diminuisci', a11yIncrease: 'Aumenta', cancel: 'Annulla', save: 'Salva', delete: 'Elimina', remove: 'Rimuovi', error: 'Errore', confirm: 'Conferma', ok: 'OK', add: 'Aggiungi', yes: 'Sì', no: 'No', profileTitle: 'Profili aeroporto', @@ -293,6 +300,13 @@ const en: typeof it = { overlayNotepad: 'Notepad', overlayPhonebook: 'Phonebook', overlayPasswords: 'Password', overlayManuals: 'DCS Manuals', overlayArionInbox: 'Arion Inbox', overlaySettings: 'Settings', // Common + // Accessibility (screen reader labels) + a11yClose: 'Close', a11yBack: 'Back', a11yForward: 'Forward', + a11yRefresh: 'Refresh', a11yOpenInBrowser: 'Open in browser', + a11yClearSearch: 'Clear search', a11yEdit: 'Edit', + a11yShowPassword: 'Show password', a11yHidePassword: 'Hide password', + a11yBackspace: 'Delete last digit', a11yCall: 'Call', + a11yOpenMenu: 'Open menu', a11yDecrease: 'Decrease', a11yIncrease: 'Increase', cancel: 'Cancel', save: 'Save', delete: 'Delete', remove: 'Remove', error: 'Error', confirm: 'Confirm', ok: 'OK', add: 'Add', yes: 'Yes', no: 'No', profileTitle: 'Airport profiles', diff --git a/src/screens/ArionInboxScreen.tsx b/src/screens/ArionInboxScreen.tsx index 4673aae1..e4e061ba 100644 --- a/src/screens/ArionInboxScreen.tsx +++ b/src/screens/ArionInboxScreen.tsx @@ -3,6 +3,7 @@ import { ActivityIndicator, Linking, StyleSheet, Text, TouchableOpacity, View } import { MaterialIcons } from '@expo/vector-icons'; import { WebView, type WebViewNavigation } from 'react-native-webview'; import { useAppTheme } from '../context/ThemeContext'; +import { useLanguage } from '../context/LanguageContext'; const ARION_INBOX_URL = 'https://prd-arion-ap.firebaseapp.com/messages/inbox'; @@ -13,6 +14,7 @@ type WebLoadError = { export default function ArionInboxScreen() { const { colors, mode } = useAppTheme(); + const { t } = useLanguage(); const webViewRef = useRef(null); const [progress, setProgress] = useState(0); const [canGoBack, setCanGoBack] = useState(false); @@ -47,6 +49,7 @@ export default function ArionInboxScreen() { disabled={!canGoBack} onPress={() => webViewRef.current?.goBack()} activeOpacity={0.8} + accessibilityRole="button" accessibilityLabel={t('a11yBack')} > @@ -55,6 +58,7 @@ export default function ArionInboxScreen() { disabled={!canGoForward} onPress={() => webViewRef.current?.goForward()} activeOpacity={0.8} + accessibilityRole="button" accessibilityLabel={t('a11yForward')} > @@ -62,6 +66,7 @@ export default function ArionInboxScreen() { style={[styles.navButton, { backgroundColor: colors.cardSecondary }]} onPress={() => webViewRef.current?.reload()} activeOpacity={0.8} + accessibilityRole="button" accessibilityLabel={t('a11yRefresh')} > @@ -78,6 +83,7 @@ export default function ArionInboxScreen() { style={[styles.externalButton, { borderColor: colors.border, backgroundColor: colors.cardSecondary }]} onPress={openExternal} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yOpenInBrowser')} > diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index d058637a..799b232c 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -979,7 +979,7 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole {/* Header fisso */} {t('calAddShiftTitle')} - setManualModalOpen(false)}> + setManualModalOpen(false)} accessibilityRole="button" accessibilityLabel={t('a11yClose')}> @@ -1088,7 +1088,7 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole {t('calImportTitle')} {importStep !== 'saving' && ( - { setImportModalVisible(false); setImportStep('idle'); setImportFileCount(0); }}> + { setImportModalVisible(false); setImportStep('idle'); setImportFileCount(0); }} accessibilityRole="button" accessibilityLabel={t('a11yClose')}> )} diff --git a/src/screens/ManualsScreen.tsx b/src/screens/ManualsScreen.tsx index 9dea6b60..e5738e7b 100644 --- a/src/screens/ManualsScreen.tsx +++ b/src/screens/ManualsScreen.tsx @@ -6,6 +6,7 @@ import { } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; +import { useLanguage } from '../context/LanguageContext'; import { TYPE } from '../theme/typography'; import { enableLegacyAndroidLayoutAnimation } from '../utils/layoutAnimation'; @@ -330,6 +331,7 @@ function RichBodyText({ text, colors }: { text: string; colors: any }) { // ─── Commands Tab component ────────────────────────────────────────────────── function CommandsTab({ commands, colors }: { commands: DCSCommand[]; colors: any }) { + const { t } = useLanguage(); const [search, setSearch] = useState(''); const lower = search.toLowerCase(); const filtered = lower @@ -360,7 +362,7 @@ function CommandsTab({ commands, colors }: { commands: DCSCommand[]; colors: any autoCapitalize="none" /> {search.length > 0 && ( - setSearch('')}> + setSearch('')} accessibilityRole="button" accessibilityLabel={t('a11yClearSearch')}> )} @@ -451,6 +453,7 @@ function ManualItemRow({ editMode: boolean; onEdit: () => void; }) { + const { t } = useLanguage(); const { colors } = useAppTheme(); const itemStyles = useMemo(() => makeItemStyles(colors), [colors]); const [open, setOpen] = useState(false); @@ -470,7 +473,7 @@ function ManualItemRow({ /> {item.title} {editMode && ( - + )} @@ -512,6 +515,7 @@ function SectionBlock({ onAddItem: () => void; onEditItem: (itemIdx: number) => void; }) { + const { t } = useLanguage(); const { colors } = useAppTheme(); const sectionStyles = useMemo(() => makeSectionStyles(colors), [colors]); const [open, setOpen] = useState(true); @@ -527,7 +531,7 @@ function SectionBlock({ {section.title} {editMode && ( - + )} @@ -957,6 +961,7 @@ function ItemModal({ } export default function ManualsScreen() { + const { t } = useLanguage(); const { colors } = useAppTheme(); const s = useMemo(() => makeStyles(colors), [colors]); const [airlines, setAirlines] = useState(DEFAULT_AIRLINES); @@ -998,7 +1003,7 @@ export default function ManualsScreen() { Manuali DCS - setEditMode(v => !v)} style={{ marginLeft: 'auto' }}> + setEditMode(v => !v)} style={{ marginLeft: 'auto' }} accessibilityRole="button" accessibilityLabel={t('a11yEdit')}> {t('notepadTitle')} - + {keys.map((k, i) => ( k === '' ? : k === '⌫' ? ( - + ) : ( @@ -141,6 +141,7 @@ function PinOverlay({ onUnlock, onCancel, title }: { onUnlock: (pin: string) => // ─── Password Row ───────────────────────────────────────────────────────────── function PasswordRowComponent({ item, onEdit, onDelete }: { item: PasswordEntry; onEdit: () => void; onDelete: () => void }) { const { colors } = useAppTheme(); + const { t } = useLanguage(); const s = useMemo(() => makeRowStyles(colors), [colors]); const [revealed, setRevealed] = useState(false); @@ -151,17 +152,17 @@ function PasswordRowComponent({ item, onEdit, onDelete }: { item: PasswordEntry; {item.username ? {item.username} : null} {revealed ? item.password : '••••••••'} - setRevealed(r => !r)} style={s.eyeBtn}> + setRevealed(r => !r)} style={s.eyeBtn} accessibilityRole="button" accessibilityLabel={t(revealed ? 'a11yHidePassword' : 'a11yShowPassword')}> {item.notes ? {item.notes} : null} - + - + @@ -368,7 +369,7 @@ export default function PasswordScreen() { secureTextEntry={!showPw} autoCapitalize="none" /> - setShowPw(p => !p)} style={s.eyeModal}> + setShowPw(p => !p)} style={s.eyeModal} accessibilityRole="button" accessibilityLabel={t(showPw ? 'a11yHidePassword' : 'a11yShowPassword')}> diff --git a/src/screens/PhonebookScreen.tsx b/src/screens/PhonebookScreen.tsx index e82ae6c3..a98034fd 100644 --- a/src/screens/PhonebookScreen.tsx +++ b/src/screens/PhonebookScreen.tsx @@ -274,13 +274,13 @@ function ContactRowComponent({ contact, onEdit, onDelete }: ContactRowProps) { {contact.number} {!!contact.note && {contact.note}} - + - onEdit(contact)}> + onEdit(contact)} accessibilityRole="button" accessibilityLabel={t('a11yEdit')}> - + @@ -422,7 +422,7 @@ export default function PhonebookScreen() { autoCorrect={false} /> {!!search && ( - setSearch('')}> + setSearch('')} accessibilityRole="button" accessibilityLabel={t('a11yClearSearch')}> )} diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index aeb4e02f..3fbeb402 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -991,6 +991,7 @@ export default function SettingsScreen({ style={[styles.providerCloseBtn, { backgroundColor: colors.cardSecondary }]} onPress={closeProviderModal} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yClose')} > @@ -1373,6 +1374,7 @@ export default function SettingsScreen({ style={[styles.providerRefreshBtn, { backgroundColor: colors.cardSecondary, borderColor: colors.border }]} onPress={() => { refreshProviderDebug().catch(() => {}); }} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yRefresh')} > @@ -1468,6 +1470,7 @@ export default function SettingsScreen({ style={[styles.providerRefreshBtn, { backgroundColor: colors.cardSecondary, borderColor: colors.border }]} onPress={() => { refreshNotificationDebug().catch(() => {}); }} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yRefresh')} > @@ -1593,6 +1596,7 @@ export default function SettingsScreen({ style={[styles.providerCloseBtn, { backgroundColor: colors.cardSecondary }]} onPress={closeDebugModal} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yClose')} > @@ -1677,6 +1681,7 @@ export default function SettingsScreen({ style={[styles.providerRefreshBtn, { backgroundColor: colors.cardSecondary, borderColor: colors.border }]} onPress={() => { refreshProviderDebug().catch(() => {}); }} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yRefresh')} > @@ -1772,6 +1777,7 @@ export default function SettingsScreen({ style={[styles.providerRefreshBtn, { backgroundColor: colors.cardSecondary, borderColor: colors.border }]} onPress={() => { refreshNotificationDebug().catch(() => {}); }} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yRefresh')} > From 01339dbb499a98f75e0a977184fda3ad9571665d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 04:06:26 +0000 Subject: [PATCH 06/10] fix(theme): add primaryText token for readable brand-colored text Light-theme primary orange is 2.7:1 on card - fine for buttons and large graphics, unreadable as text. primaryText resolves to the dark orange (#C2520A, 4.7:1) in light mode and stays the standard teal in dark mode, so the Operations board look is unchanged. Migrated the 21 text usages (links, active labels, hour totals, onboarding actions); icons and surfaces keep primary. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012vEkzMVahsn6d7vyL4xayt --- src/components/ProfileSwitcherModal.tsx | 10 +++++----- src/components/UpdateModal.tsx | 2 +- src/components/flights/FlightFilterModal.tsx | 2 +- src/context/ThemeContext.tsx | 6 ++++++ src/screens/ArionInboxScreen.tsx | 2 +- src/screens/CalendarScreen.tsx | 10 +++++----- src/screens/OnboardingScreen.tsx | 12 ++++++------ src/screens/SettingsScreen.tsx | 4 ++-- 8 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/components/ProfileSwitcherModal.tsx b/src/components/ProfileSwitcherModal.tsx index 91819294..9ea510e8 100644 --- a/src/components/ProfileSwitcherModal.tsx +++ b/src/components/ProfileSwitcherModal.tsx @@ -276,8 +276,8 @@ export default function ProfileSwitcherModal({ visible, onClose }: Props) { onPress={() => setDraftAirportCode(airport.code)} activeOpacity={0.8} > - {airport.code} - {airport.city} + {airport.code} + {airport.city} ); })} @@ -309,7 +309,7 @@ export default function ProfileSwitcherModal({ visible, onClose }: Props) { activeOpacity={0.85} > - {label} + {label} - GitHub + GitHub - + {allSelected ? t('flightFilterDeselAll') : t('flightFilterSelAll')} diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx index 99c8c787..1a10b8a6 100644 --- a/src/context/ThemeContext.tsx +++ b/src/context/ThemeContext.tsx @@ -24,6 +24,10 @@ export type ThemeColors = { primary: string; primaryDark: string; primaryLight: string; + /* Arancio/teal per TESTO su card e bg: nel tema chiaro primary (2.7:1) non + è leggibile come testo, primaryText sì (>=4.5:1). Per icone e superfici + continuare a usare primary. */ + primaryText: string; // Glass tokens glass: string; glassBorder: string; @@ -62,6 +66,7 @@ const LIGHT: ThemeColors = { primary: '#F47B16', primaryDark: '#C2520A', primaryLight: '#FFEDD5', + primaryText: '#C2520A', glass: '#FFFFFF', glassBorder: 'transparent', glassStrong: '#FFFFFF', @@ -98,6 +103,7 @@ const DARK: ThemeColors = { primary: '#2DD4BF', primaryDark: '#99F6E4', primaryLight: 'rgba(45,212,191,0.18)', + primaryText: '#2DD4BF', glass: '#111A1F', glassBorder: 'rgba(45,212,191,0.24)', glassStrong: '#19262D', diff --git a/src/screens/ArionInboxScreen.tsx b/src/screens/ArionInboxScreen.tsx index e4e061ba..2dca7b03 100644 --- a/src/screens/ArionInboxScreen.tsx +++ b/src/screens/ArionInboxScreen.tsx @@ -115,7 +115,7 @@ export default function ArionInboxScreen() { Riprova - Browser + Browser diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index 799b232c..336e38a2 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -1140,7 +1140,7 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole {fmtDate(shift.date)} {shift.type === 'work' ? ( - + {shift.start} - {shift.end} @@ -1255,7 +1255,7 @@ function makeStyles(c: ThemeColors) { legendText: { ...TYPE.caption, color: c.textSub }, calendarSummary: { marginTop: 14, paddingTop: 14, borderTopWidth: 1, borderTopColor: c.border }, calendarSummaryLabel: { color: c.textSub, fontSize: 12, fontWeight: '700', letterSpacing: 0.8, textTransform: 'uppercase' }, - calendarSummaryValue: { color: c.primary, fontSize: 28, fontWeight: '800', marginTop: 6 }, + calendarSummaryValue: { color: c.primaryText, fontSize: 28, fontWeight: '800', marginTop: 6 }, calendarSummaryMeta: { ...TYPE.callout, color: c.textSub, marginTop: 4 }, weekCard: { backgroundColor: c.card, @@ -1274,7 +1274,7 @@ function makeStyles(c: ThemeColors) { weekTitle: { color: c.primaryDark, fontSize: 20, fontWeight: '900' }, weekRange: { color: c.textSub, fontSize: 12, fontWeight: '700', marginTop: 3, textTransform: 'uppercase' }, weekTotalPill: { backgroundColor: c.primaryLight, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 8, alignItems: 'flex-end' }, - weekTotalValue: { color: c.primary, fontSize: 18, fontWeight: '900' }, + weekTotalValue: { color: c.primaryText, fontSize: 18, fontWeight: '900' }, weekTotalLabel: { ...TYPE.micro, color: c.primaryDark, marginTop: 1 }, weekRow: { flexDirection: 'row', @@ -1329,10 +1329,10 @@ function makeStyles(c: ThemeColors) { shiftIconBox: { width: 44, height: 44, backgroundColor: c.primaryLight, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, shiftTypeName: { ...TYPE.headline, color: c.primaryDark }, timeRow: { flexDirection: 'row', alignItems: 'center' }, - timeText: { ...TYPE.title, color: c.primary }, + timeText: { ...TYPE.title, color: c.primaryText }, flightBadge: { marginTop: 14, backgroundColor: c.primaryLight, borderRadius: 10, paddingHorizontal: 14, paddingVertical: 8, alignSelf: 'flex-start' }, flightBadgeRow: { flexDirection: 'row', alignItems: 'center', gap: 6 }, - flightBadgeText: { color: c.primary, fontWeight: '700', fontSize: 13 }, + flightBadgeText: { color: c.primaryText, fontWeight: '700', fontSize: 13 }, restRow: { flexDirection: 'row', alignItems: 'center', marginTop: 10 }, restIconBox: { width: 48, height: 48, borderRadius: 14, backgroundColor: c.successSoft, alignItems: 'center', justifyContent: 'center', marginRight: 12 }, restText: { ...TYPE.headline, color: c.success }, diff --git a/src/screens/OnboardingScreen.tsx b/src/screens/OnboardingScreen.tsx index 523bf007..ce62780a 100644 --- a/src/screens/OnboardingScreen.tsx +++ b/src/screens/OnboardingScreen.tsx @@ -147,7 +147,7 @@ export default function OnboardingScreen({ - SETUP GUIDATO + SETUP GUIDATO Prepara AeroStaff Pro Configura il minimo utile: aeroporto, calendario, fonti voli, notifiche e widget. @@ -187,28 +187,28 @@ export default function OnboardingScreen({ {item.title} - {item.required && richiesto} + {item.required && richiesto} {item.detail} {item.id === 'profile' && ( - Apri + Apri )} {item.id === 'calendar' && item.status !== 'ready' && ( - Consenti + Consenti )} {item.id === 'flightData' && ( - API + API )} {item.id === 'notifications' && item.status !== 'ready' && ( - Attiva + Attiva )} diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 3fbeb402..0334f2a5 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -142,7 +142,7 @@ function ThemeCard({ option, selected, onSelect, activeLabel }: { - + {option.label} {selected && ( @@ -787,7 +787,7 @@ export default function SettingsScreen({ {checkingUpdate ? : } - + {checkingUpdate ? 'Controllo…' : 'Controlla'} From cc0dd7b6e50e81cecfc97be536c25e9b463ccc9a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 04:07:03 +0000 Subject: [PATCH 07/10] refactor: adopt SPACING/RADIUS tokens across screens and components Value-preserving codemod: padding/margin/gap literals already on the 4-based scale (4/8/12/16/20/24/32) and radii on the radius scale (8/12/16/20/999) swapped for SPACING.*/RADIUS.* tokens in 26 files. Off-scale values (10, 14, 18...) left inline on purpose - no rendered pixel changes in this commit. dev/ previews and widget primitives excluded. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012vEkzMVahsn6d7vyL4xayt --- App.tsx | 7 +- src/components/AppTabBar.tsx | 13 +- src/components/DrawerMenuPanel.tsx | 11 +- src/components/GlassCard.tsx | 3 +- src/components/ProfileSwitcherModal.tsx | 43 +++---- src/components/ShiftTimeline.tsx | 29 ++--- src/components/TimeCarouselPicker.tsx | 11 +- src/components/UpdateModal.tsx | 25 ++-- src/components/flights/AirlineLogo.tsx | 5 +- .../flights/FlightSourceDebugModal.tsx | 35 +++--- src/components/flights/FlightStates.tsx | 15 +-- .../flights/SwipeableFlightCard.tsx | 5 +- .../motion/CockpitFlightProgress.tsx | 11 +- src/components/motion/ValueChangeFlash.tsx | 5 +- src/screens/ArionInboxScreen.tsx | 19 +-- src/screens/CalendarScreen.tsx | 111 +++++++++--------- src/screens/DesignLabScreen.tsx | 5 +- src/screens/FlightScreen.tsx | 55 ++++----- src/screens/HomeScreen.tsx | 97 +++++++-------- src/screens/ManualsScreen.tsx | 55 ++++----- src/screens/NotepadScreen.tsx | 13 +- src/screens/OnboardingScreen.tsx | 15 +-- src/screens/PasswordScreen.tsx | 49 ++++---- src/screens/PhonebookScreen.tsx | 47 ++++---- src/screens/SettingsScreen.tsx | 103 ++++++++-------- src/screens/TraveldocScreen.tsx | 15 +-- 26 files changed, 414 insertions(+), 388 deletions(-) diff --git a/App.tsx b/App.tsx index e6641365..3e8e843b 100644 --- a/App.tsx +++ b/App.tsx @@ -44,6 +44,7 @@ import { useReducedMotionPreference, } from './src/utils/motion'; import { ONBOARDING_SETUP_STORAGE_KEY, shouldShowOnboarding } from './src/utils/appSetup'; +import { SPACING, RADIUS } from './src/theme/spacing'; installGlobalCrashHandler(); @@ -461,13 +462,13 @@ const styles = StyleSheet.create({ appBar: { flexDirection: 'row', alignItems: 'center', - paddingHorizontal: 12, + paddingHorizontal: SPACING.md, paddingBottom: 10, borderBottomWidth: 1, overflow: 'hidden', }, - iconBtn: { padding: 6, borderRadius: 8, marginRight: 6 }, - titleRow: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 8 }, + iconBtn: { padding: 6, borderRadius: RADIUS.sm, marginRight: 6 }, + titleRow: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, appBarTitle: { fontSize: 18, fontWeight: '700', letterSpacing: 0.3 }, avatar: { width: 34, height: 34, borderRadius: 17, diff --git a/src/components/AppTabBar.tsx b/src/components/AppTabBar.tsx index aefb3dd0..a3a824ee 100644 --- a/src/components/AppTabBar.tsx +++ b/src/components/AppTabBar.tsx @@ -3,6 +3,7 @@ import { Animated, StyleSheet, Text, View } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import FrostedSurface from './FrostedSurface'; import TactilePressable from './motion/TactilePressable'; +import { SPACING, RADIUS } from '../theme/spacing'; import { motionDurations, motionEasing, @@ -470,7 +471,7 @@ const styles = StyleSheet.create({ bottom: 0, width: 24, left: 8, - borderRadius: 999, + borderRadius: RADIUS.pill, backgroundColor: 'rgba(255,255,255,0.24)', transform: [{ skewX: '-18deg' }], }, @@ -480,7 +481,7 @@ const styles = StyleSheet.create({ right: 8, bottom: 5, height: 3, - borderRadius: 999, + borderRadius: RADIUS.pill, opacity: 0.72, }, tabPressable: { @@ -505,19 +506,19 @@ const styles = StyleSheet.create({ bottom: 4, width: 18, height: 3, - borderRadius: 999, + borderRadius: RADIUS.pill, }, opsDeck: { flex: 1, paddingHorizontal: 10, - paddingVertical: 8, + paddingVertical: SPACING.sm, gap: 7, }, opsHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingHorizontal: 4, + paddingHorizontal: SPACING.xs, }, opsKicker: { color: 'rgba(204,251,241,0.58)', @@ -584,7 +585,7 @@ const styles = StyleSheet.create({ alignSelf: 'flex-start', borderWidth: 1, borderColor: 'rgba(204,251,241,0.14)', - borderRadius: 999, + borderRadius: RADIUS.pill, paddingHorizontal: 5, paddingVertical: 1, }, diff --git a/src/components/DrawerMenuPanel.tsx b/src/components/DrawerMenuPanel.tsx index 86f29205..9ad1f50d 100644 --- a/src/components/DrawerMenuPanel.tsx +++ b/src/components/DrawerMenuPanel.tsx @@ -8,6 +8,7 @@ import AeroStaffLogo from './AeroStaffLogo'; import FrostedSurface from './FrostedSurface'; import BoardReveal from './motion/BoardReveal'; import TactilePressable from './motion/TactilePressable'; +import { SPACING, RADIUS } from '../theme/spacing'; export type DrawerItem = { id: string; @@ -227,7 +228,7 @@ function makeStyles(c: ThemeColors, surface: DrawerSurfaceConfig) { opsBrandRow: { flexDirection: 'row', alignItems: 'center', - gap: 12, + gap: SPACING.md, flex: 1, }, opsLogoBox: { @@ -257,7 +258,7 @@ function makeStyles(c: ThemeColors, surface: DrawerSurfaceConfig) { opsClose: { width: 34, height: 34, - borderRadius: 12, + borderRadius: RADIUS.md, borderWidth: 1, borderColor: 'rgba(45,212,191,0.24)', backgroundColor: 'rgba(2,8,12,0.36)', @@ -268,9 +269,9 @@ function makeStyles(c: ThemeColors, surface: DrawerSurfaceConfig) { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingHorizontal: 20, + paddingHorizontal: SPACING.xl, paddingTop: surface.isOperations ? 16 : 20, - paddingBottom: 8, + paddingBottom: SPACING.sm, }, sectionLabel: { fontSize: 10, @@ -315,7 +316,7 @@ function makeStyles(c: ThemeColors, surface: DrawerSurfaceConfig) { itemCopy: { flex: 1 }, itemLabel: { fontSize: 14, fontWeight: '600', color: c.text }, itemSub: { fontSize: 11, color: c.isDark ? 'rgba(229,233,240,0.70)' : c.textMuted, marginTop: 1 }, - divider: { height: 1, backgroundColor: c.border, marginHorizontal: 18, marginTop: 16 }, + divider: { height: 1, backgroundColor: c.border, marginHorizontal: 18, marginTop: SPACING.lg }, version: { fontSize: 11, color: c.isDark ? 'rgba(229,233,240,0.66)' : c.textMuted, diff --git a/src/components/GlassCard.tsx b/src/components/GlassCard.tsx index 295c2cf1..e06bd7ff 100644 --- a/src/components/GlassCard.tsx +++ b/src/components/GlassCard.tsx @@ -3,6 +3,7 @@ import { View, StyleSheet, Platform, ViewStyle } from 'react-native'; import { BlurView } from 'expo-blur'; import { LinearGradient } from 'expo-linear-gradient'; import { useAppTheme } from '../context/ThemeContext'; +import { SPACING } from '../theme/spacing'; type Variant = 'default' | 'strong' | 'subtle'; @@ -155,6 +156,6 @@ const styles = StyleSheet.create({ borderWidth: 0.75, }, content: { - padding: 16, + padding: SPACING.lg, }, }); diff --git a/src/components/ProfileSwitcherModal.tsx b/src/components/ProfileSwitcherModal.tsx index 9ea510e8..a86c1dc9 100644 --- a/src/components/ProfileSwitcherModal.tsx +++ b/src/components/ProfileSwitcherModal.tsx @@ -22,6 +22,7 @@ import { normalizeAirportCode, } from '../utils/airportSettings'; import { AIRLINE_COLORS, AIRLINE_DISPLAY_NAMES } from '../utils/airlineOps'; +import { SPACING, RADIUS } from '../theme/spacing'; type Props = { visible: boolean; @@ -382,7 +383,7 @@ function makeStyles(colors: ThemeColors) { header: { flexDirection: 'row', alignItems: 'flex-start', - gap: 12, + gap: SPACING.md, paddingHorizontal: 18, paddingTop: 18, paddingBottom: 14, @@ -394,7 +395,7 @@ function makeStyles(colors: ThemeColors) { color: colors.text, }, subtitle: { - marginTop: 4, + marginTop: SPACING.xs, fontSize: 13, lineHeight: 18, color: colors.textSub, @@ -432,7 +433,7 @@ function makeStyles(colors: ThemeColors) { profileBadge: { width: 48, height: 48, - borderRadius: 16, + borderRadius: RADIUS.lg, alignItems: 'center', justifyContent: 'center', }, @@ -443,8 +444,8 @@ function makeStyles(colors: ThemeColors) { profileTitleRow: { flexDirection: 'row', alignItems: 'center', - gap: 8, - marginBottom: 4, + gap: SPACING.sm, + marginBottom: SPACING.xs, }, profileName: { fontSize: 15, @@ -458,9 +459,9 @@ function makeStyles(colors: ThemeColors) { marginTop: 2, }, activePill: { - paddingHorizontal: 8, + paddingHorizontal: SPACING.sm, paddingVertical: 3, - borderRadius: 999, + borderRadius: RADIUS.pill, backgroundColor: colors.primary, }, activePillText: { @@ -471,7 +472,7 @@ function makeStyles(colors: ThemeColors) { profileAction: { width: 36, height: 36, - borderRadius: 12, + borderRadius: RADIUS.md, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.card, @@ -480,7 +481,7 @@ function makeStyles(colors: ThemeColors) { flexDirection: 'row', alignItems: 'center', gap: 10, - padding: 16, + padding: SPACING.lg, borderTopWidth: 1, borderTopColor: colors.border, backgroundColor: colors.card, @@ -488,7 +489,7 @@ function makeStyles(colors: ThemeColors) { secondaryBtn: { flexDirection: 'row', alignItems: 'center', - gap: 8, + gap: SPACING.sm, paddingHorizontal: 14, paddingVertical: 11, borderRadius: 14, @@ -502,8 +503,8 @@ function makeStyles(colors: ThemeColors) { primaryBtn: { flexDirection: 'row', alignItems: 'center', - gap: 8, - paddingHorizontal: 16, + gap: SPACING.sm, + paddingHorizontal: SPACING.lg, paddingVertical: 11, borderRadius: 14, backgroundColor: colors.primary, @@ -516,7 +517,7 @@ function makeStyles(colors: ThemeColors) { deleteBtn: { flexDirection: 'row', alignItems: 'center', - gap: 8, + gap: SPACING.sm, paddingHorizontal: 10, paddingVertical: 10, }, @@ -530,14 +531,14 @@ function makeStyles(colors: ThemeColors) { fontWeight: '800', color: colors.textSub, letterSpacing: 0.5, - marginBottom: 8, + marginBottom: SPACING.sm, }, input: { borderWidth: 1, borderColor: colors.border, borderRadius: 14, paddingHorizontal: 14, - paddingVertical: 12, + paddingVertical: SPACING.md, fontSize: 15, color: colors.text, backgroundColor: colors.cardSecondary, @@ -545,11 +546,11 @@ function makeStyles(colors: ThemeColors) { }, quickPicks: { gap: 10, - paddingBottom: 8, - paddingRight: 8, + paddingBottom: SPACING.sm, + paddingRight: SPACING.sm, }, quickPickChip: { - paddingHorizontal: 12, + paddingHorizontal: SPACING.md, paddingVertical: 10, borderRadius: 14, borderWidth: 1, @@ -575,8 +576,8 @@ function makeStyles(colors: ThemeColors) { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - marginTop: 4, - marginBottom: 8, + marginTop: SPACING.xs, + marginBottom: SPACING.sm, }, airlineHeaderActions: { flexDirection: 'row', @@ -596,7 +597,7 @@ function makeStyles(colors: ThemeColors) { alignItems: 'center', gap: 10, paddingHorizontal: 14, - paddingVertical: 12, + paddingVertical: SPACING.md, borderRadius: 14, borderWidth: 1, borderColor: colors.border, diff --git a/src/components/ShiftTimeline.tsx b/src/components/ShiftTimeline.tsx index daa157c0..2cf6db57 100644 --- a/src/components/ShiftTimeline.tsx +++ b/src/components/ShiftTimeline.tsx @@ -13,6 +13,7 @@ import { fetchAirportScheduleRaw } from '../utils/fr24api'; import { filterFlightsByAirlines, getFlightAirportLabel } from '../utils/flightScheduleAdapter'; import { enableLegacyAndroidLayoutAnimation } from '../utils/layoutAnimation'; import { useLanguage } from '../context/LanguageContext'; +import { SPACING, RADIUS } from '../theme/spacing'; enableLegacyAndroidLayoutAnimation(); @@ -161,7 +162,7 @@ export default function ShiftTimeline({ visible, onClose, shiftStart, shiftEnd, )} {/* Legenda */} - + Check-in @@ -179,14 +180,14 @@ export default function ShiftTimeline({ visible, onClose, shiftStart, shiftEnd, ) : error ? ( - Errore nel caricamento + Errore nel caricamento Riprova ) : flights.length === 0 ? ( - + Nessuna partenza nel turno ) : ( @@ -321,20 +322,20 @@ function makeStyles(c: ThemeColors) { }, handleRow: { alignItems: 'center', paddingTop: 10, paddingBottom: 6 }, handle: { width: 36, height: 4, borderRadius: 2 }, - header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingBottom: 10 }, + header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: SPACING.xl, paddingBottom: 10 }, title: { ...TYPE.headline }, subtitle: { fontSize: 12, marginTop: 2 }, - closeBtn: { padding: 8, borderRadius: 20 }, - legend: { flexDirection: 'row', gap: 16, paddingHorizontal: 20, paddingBottom: 12 }, + closeBtn: { padding: SPACING.sm, borderRadius: RADIUS.xl }, + legend: { flexDirection: 'row', gap: SPACING.lg, paddingHorizontal: SPACING.xl, paddingBottom: SPACING.md }, legendItem: { flexDirection: 'row', alignItems: 'center', gap: 6 }, legendDot: { width: 10, height: 10, borderRadius: 5 }, legendText: { fontSize: 11, fontWeight: '600' }, center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - retryBtn: { paddingHorizontal: 20, paddingVertical: 10, borderRadius: 10 }, + retryBtn: { paddingHorizontal: SPACING.xl, paddingVertical: 10, borderRadius: 10 }, scrollArea: { flex: 1 }, // Righello orizzontale in alto - rulerWrap: { flexDirection: 'row', paddingHorizontal: 12, marginBottom: 4, height: 32 }, + rulerWrap: { flexDirection: 'row', paddingHorizontal: SPACING.md, marginBottom: SPACING.xs, height: 32 }, rulerLabelSpace: { width: 80 }, ruler: { flex: 1, position: 'relative' }, rulerTick: { position: 'absolute', top: 0, alignItems: 'center', transform: [{ translateX: -1 }] }, @@ -345,8 +346,8 @@ function makeStyles(c: ThemeColors) { nowTick: { width: 2, height: 10, backgroundColor: '#EF4444', borderRadius: 1 }, // Righe voli - flightRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 8, borderBottomWidth: 1 }, - flightLabelWrap: { width: 80, flexDirection: 'row', alignItems: 'center', gap: 4 }, + flightRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: SPACING.md, paddingVertical: SPACING.sm, borderBottomWidth: 1 }, + flightLabelWrap: { width: 80, flexDirection: 'row', alignItems: 'center', gap: SPACING.xs }, airlineDot: { width: 6, height: 6, borderRadius: 3, flexShrink: 0 }, flightLabel: { fontSize: 11, fontWeight: '700', flexShrink: 1 }, flightDest: { fontSize: 10, fontWeight: '600' }, @@ -355,16 +356,16 @@ function makeStyles(c: ThemeColors) { ganttArea: { flex: 1, height: 36, position: 'relative', justifyContent: 'center' }, ganttGridLine: { position: 'absolute', top: 0, bottom: 0, width: 1, opacity: 0.25 }, ganttNowLine: { position: 'absolute', top: 0, bottom: 0, width: 2, backgroundColor: '#EF4444', opacity: 0.5, zIndex: 5 }, - ganttBar: { position: 'absolute', height: 14, borderRadius: 3, justifyContent: 'center', paddingHorizontal: 4 }, + ganttBar: { position: 'absolute', height: 14, borderRadius: 3, justifyContent: 'center', paddingHorizontal: SPACING.xs }, ganttBarCI: { backgroundColor: CI_COLOR, top: 2 }, ganttBarGate: { backgroundColor: GATE_COLOR, bottom: 2 }, ganttBarText: { fontSize: 8, fontWeight: '800', color: '#fff' }, depMarker: { position: 'absolute', top: 0, bottom: 0, borderLeftWidth: 2, borderStyle: 'dashed' }, // Card espansa - expandedCard: { borderRadius: 10, padding: 12, marginHorizontal: 12, marginBottom: 4, borderWidth: 1 }, - expandedTitle: { fontSize: 14, fontWeight: '700', marginBottom: 8 }, - expandedRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 }, + expandedCard: { borderRadius: 10, padding: SPACING.md, marginHorizontal: SPACING.md, marginBottom: SPACING.xs, borderWidth: 1 }, + expandedTitle: { fontSize: 14, fontWeight: '700', marginBottom: SPACING.sm }, + expandedRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: SPACING.xs }, expandedLabel: { fontSize: 11, fontWeight: '600' }, expandedValue: { fontSize: 11, fontWeight: '700' }, }); diff --git a/src/components/TimeCarouselPicker.tsx b/src/components/TimeCarouselPicker.tsx index 6b411968..d6dbb9e3 100644 --- a/src/components/TimeCarouselPicker.tsx +++ b/src/components/TimeCarouselPicker.tsx @@ -9,6 +9,7 @@ import { TouchableOpacity, } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; +import { SPACING, RADIUS } from '../theme/spacing'; const ITEM_H = 42; const VISIBLE = 5; @@ -190,10 +191,10 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - borderRadius: 12, + borderRadius: RADIUS.md, borderWidth: 1, overflow: 'hidden', - paddingHorizontal: 8, + paddingHorizontal: SPACING.sm, marginVertical: 2, }, selectionRect: { @@ -201,7 +202,7 @@ const styles = StyleSheet.create({ left: 3, right: 3, height: ITEM_H, - borderRadius: 8, + borderRadius: RADIUS.sm, borderWidth: 1.5, zIndex: 1, }, @@ -218,7 +219,7 @@ const styles = StyleSheet.create({ colon: { fontSize: 24, fontWeight: '700', - marginHorizontal: 4, + marginHorizontal: SPACING.xs, marginBottom: 1, }, fade: { @@ -233,4 +234,4 @@ const styles = StyleSheet.create({ fadeBottom: { bottom: 0 }, }); -export default TimeCarouselPicker; +export default TimeCarouselPicker; diff --git a/src/components/UpdateModal.tsx b/src/components/UpdateModal.tsx index 1a40237e..57b7cd5c 100644 --- a/src/components/UpdateModal.tsx +++ b/src/components/UpdateModal.tsx @@ -6,6 +6,7 @@ import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme } from '../context/ThemeContext'; import { TYPE } from '../theme/typography'; import { type UpdateInfo, APP_VERSION } from '../utils/updateChecker'; +import { SPACING, RADIUS } from '../theme/spacing'; import { downloadUpdatePackage, getDownloadedUpdateUri, @@ -152,7 +153,7 @@ export default function UpdateModal({ info, onDismiss }: Props) { {/* Header */} - + Aggiornamento disponibile v{APP_VERSION} → v{latestVersionLabel} @@ -240,19 +241,19 @@ const styles = StyleSheet.create({ backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center', - padding: 24, + padding: SPACING.xxl, }, sheet: { width: '100%', maxWidth: 420, - borderRadius: 20, + borderRadius: RADIUS.xl, overflow: 'hidden', maxHeight: '80%', }, header: { flexDirection: 'row', alignItems: 'center', - padding: 20, + padding: SPACING.xl, }, headerTitle: { color: '#fff', @@ -268,22 +269,22 @@ const styles = StyleSheet.create({ maxHeight: 320, }, notesContent: { - padding: 20, + padding: SPACING.xl, }, notesText: { fontSize: 13, lineHeight: 20, }, statusBox: { - paddingHorizontal: 20, + paddingHorizontal: SPACING.xl, paddingTop: 14, - paddingBottom: 12, + paddingBottom: SPACING.md, borderBottomWidth: 1, }, statusRow: { flexDirection: 'row', alignItems: 'center', - gap: 8, + gap: SPACING.sm, }, statusText: { flex: 1, @@ -291,19 +292,19 @@ const styles = StyleSheet.create({ }, progressTrack: { height: 8, - borderRadius: 999, + borderRadius: RADIUS.pill, overflow: 'hidden', marginTop: 10, }, progressFill: { height: '100%', - borderRadius: 999, + borderRadius: RADIUS.pill, }, footer: { flexDirection: 'row', alignItems: 'center', - gap: 8, - padding: 16, + gap: SPACING.sm, + padding: SPACING.lg, borderTopWidth: 1, }, btnLater: { diff --git a/src/components/flights/AirlineLogo.tsx b/src/components/flights/AirlineLogo.tsx index e911c0a0..ed475f8b 100644 --- a/src/components/flights/AirlineLogo.tsx +++ b/src/components/flights/AirlineLogo.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { Image, Text, View } from 'react-native'; import { getAirlineMonogram } from '../../utils/airlineBranding'; +import { RADIUS } from '../../theme/spacing'; export function LogoPill({ iataCode, airlineName, color }: { iataCode: string; airlineName: string; color: string }) { const [err, setErr] = useState(false); @@ -8,13 +9,13 @@ export function LogoPill({ iataCode, airlineName, color }: { iataCode: string; a const initials = airlineName.split(' ').slice(0, 2).map(w => w[0]).join('').toUpperCase(); if (iataCode && !err) { return ( - + setErr(true)} /> ); } return ( - + {initials} ); diff --git a/src/components/flights/FlightSourceDebugModal.tsx b/src/components/flights/FlightSourceDebugModal.tsx index a48d0c10..e426b001 100644 --- a/src/components/flights/FlightSourceDebugModal.tsx +++ b/src/components/flights/FlightSourceDebugModal.tsx @@ -5,6 +5,7 @@ import type { ThemeColors } from '../../context/ThemeContext'; import type { TranslationKey } from '../../i18n/translations'; import type { FlightScheduleProviderStatus } from '../../utils/fr24api'; import { formatFlightSourceLabel } from '../../utils/flightSourceLabel'; +import { SPACING, RADIUS } from '../../theme/spacing'; import { formatFlightCacheAge, formatProviderDiagnostic, @@ -172,13 +173,13 @@ const styles = StyleSheet.create({ header: { flexDirection: 'row', alignItems: 'center', - gap: 12, - padding: 16, + gap: SPACING.md, + padding: SPACING.lg, }, headerIcon: { width: 44, height: 44, - borderRadius: 16, + borderRadius: RADIUS.lg, alignItems: 'center', justifyContent: 'center', }, @@ -191,17 +192,17 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - content: { paddingHorizontal: 16, paddingBottom: 18, gap: 12 }, + content: { paddingHorizontal: SPACING.lg, paddingBottom: 18, gap: SPACING.md }, sourcePill: { flexDirection: 'row', alignItems: 'center', - gap: 8, + gap: SPACING.sm, alignSelf: 'flex-start', maxWidth: '100%', - borderRadius: 999, + borderRadius: RADIUS.pill, borderWidth: 1, - paddingHorizontal: 12, - paddingVertical: 8, + paddingHorizontal: SPACING.md, + paddingVertical: SPACING.sm, }, sourceText: { flexShrink: 1, fontSize: 12, fontWeight: '900' }, refreshRow: { @@ -210,20 +211,20 @@ const styles = StyleSheet.create({ gap: 10, borderRadius: 14, borderWidth: 1, - paddingHorizontal: 12, + paddingHorizontal: SPACING.md, paddingVertical: 10, }, - metaGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 }, - metaBox: { minWidth: '47%', flexGrow: 1, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 10 }, + metaGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.sm }, + metaBox: { minWidth: '47%', flexGrow: 1, borderRadius: 14, paddingHorizontal: SPACING.md, paddingVertical: 10 }, metaLabel: { fontSize: 10, fontWeight: '900', letterSpacing: 0.7, textTransform: 'uppercase' }, metaValue: { fontSize: 14, fontWeight: '900', marginTop: 3 }, - filterBox: { borderWidth: 1, borderRadius: 16, padding: 12 }, - filterText: { fontSize: 14, fontWeight: '800', marginTop: 4 }, + filterBox: { borderWidth: 1, borderRadius: RADIUS.lg, padding: SPACING.md }, + filterText: { fontSize: 14, fontWeight: '800', marginTop: SPACING.xs }, reasonText: { fontSize: 12, lineHeight: 17, marginTop: 6 }, - sectionTitle: { fontSize: 11, fontWeight: '900', letterSpacing: 1.1, marginTop: 4 }, - providerList: { gap: 8 }, - providerRow: { flexDirection: 'row', gap: 10, borderWidth: 1, borderRadius: 14, padding: 12 }, - statusDot: { width: 10, height: 10, borderRadius: 5, marginTop: 4 }, + sectionTitle: { fontSize: 11, fontWeight: '900', letterSpacing: 1.1, marginTop: SPACING.xs }, + providerList: { gap: SPACING.sm }, + providerRow: { flexDirection: 'row', gap: 10, borderWidth: 1, borderRadius: 14, padding: SPACING.md }, + statusDot: { width: 10, height: 10, borderRadius: 5, marginTop: SPACING.xs }, providerName: { fontSize: 13, fontWeight: '900' }, providerSub: { fontSize: 11, lineHeight: 16, marginTop: 2 }, }); diff --git a/src/components/flights/FlightStates.tsx b/src/components/flights/FlightStates.tsx index 521fc57b..9c89cb9d 100644 --- a/src/components/flights/FlightStates.tsx +++ b/src/components/flights/FlightStates.tsx @@ -3,6 +3,7 @@ import { ActivityIndicator, Text, View } from 'react-native'; import type { ThemeColors } from '../../context/ThemeContext'; import type { TranslationKey } from '../../i18n/translations'; import type { FlightScheduleProviderStatus } from '../../utils/fr24api'; +import { SPACING, RADIUS } from '../../theme/spacing'; import { formatProviderDiagnostic, getTomorrowEmptyReason, @@ -50,8 +51,8 @@ export function EmptyFlightState({ return ( {title} {body ? ( - {body} + {body} ) : null} {activeDay === 'tomorrow' ? ( @@ -70,7 +71,7 @@ export function EmptyFlightState({ ) : null} {providerLines.length > 0 ? ( - + {t('flightProviderDebugTitle')} @@ -93,13 +94,13 @@ export function FlightLoadingState({ t: (key: TranslationKey) => string; }) { return ( - + setEditMenuOpen(false)} /> - Modifica Turni + Modifica Turni { setEditMenuOpen(false); startImport(); }}> @@ -977,7 +978,7 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole {/* Header fisso */} - + {t('calAddShiftTitle')} setManualModalOpen(false)} accessibilityRole="button" accessibilityLabel={t('a11yClose')}> @@ -989,7 +990,7 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole keyboardShouldPersistTaps="handled" nestedScrollEnabled showsVerticalScrollIndicator={false} - contentContainerStyle={{ paddingHorizontal: 24, paddingBottom: 40 }} + contentContainerStyle={{ paddingHorizontal: SPACING.xxl, paddingBottom: 40 }} > {/* Data */} {t('calDataLabel')} @@ -998,13 +999,13 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole value={manualDate.split('-').reverse().join('/')} editable={false} /> - + Seleziona un giorno dal calendario per cambiare la data {/* Tipo */} {t('calTypeLabel')} - + {(['Lavoro', 'Riposo'] as const).map(shiftType => ( - {t('calEndTime')} + {t('calEndTime')} )} - + {t('calSaveShift')} @@ -1152,7 +1153,7 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole ))} - + setImportStep('pickName')} @@ -1193,18 +1194,18 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole function makeStyles(c: ThemeColors) { return StyleSheet.create({ - pageHeader: { backgroundColor: c.card, paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: c.border }, + pageHeader: { backgroundColor: c.card, paddingHorizontal: SPACING.lg, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: c.border }, pageTitle: { ...TYPE.title, color: c.primaryDark }, pageSub: { fontSize: 11, color: c.textSub, letterSpacing: 1.5, marginTop: 3 }, - importBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 14, paddingVertical: 8, borderRadius: 10 }, + importBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 14, paddingVertical: SPACING.sm, borderRadius: 10 }, importBtnText: { color: '#fff', fontSize: 14, fontWeight: '600' }, viewModeRow: { flexDirection: 'row', - gap: 8, - marginHorizontal: 16, + gap: SPACING.sm, + marginHorizontal: SPACING.lg, marginTop: 14, backgroundColor: c.card, - borderRadius: 16, + borderRadius: RADIUS.lg, padding: 6, borderWidth: c.isDark ? 1 : 0, borderColor: c.glassBorder, @@ -1215,17 +1216,17 @@ function makeStyles(c: ThemeColors) { alignItems: 'center', justifyContent: 'center', gap: 6, - borderRadius: 12, + borderRadius: RADIUS.md, paddingVertical: 10, }, viewModeText: { fontSize: 13, fontWeight: '800' }, calendarCard: { backgroundColor: c.card, - borderRadius: 20, - marginHorizontal: 16, - marginTop: 16, - paddingHorizontal: 12, - paddingTop: 8, + borderRadius: RADIUS.xl, + marginHorizontal: SPACING.lg, + marginTop: SPACING.lg, + paddingHorizontal: SPACING.md, + paddingTop: SPACING.sm, paddingBottom: 14, shadowColor: c.primary, shadowOpacity: c.isDark ? 0 : 0.08, @@ -1234,8 +1235,8 @@ function makeStyles(c: ThemeColors) { borderWidth: c.isDark ? 1 : 0, borderColor: c.glassBorder, }, - monthCalendar: { borderRadius: 16 }, - monthCalendarHeader: { paddingBottom: 8, marginBottom: 6 }, + monthCalendar: { borderRadius: RADIUS.lg }, + monthCalendarHeader: { paddingBottom: SPACING.sm, marginBottom: 6 }, dayCellWrap: { alignItems: 'center', justifyContent: 'center', paddingVertical: 2 }, dayCellInner: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', borderWidth: 1.5, borderColor: 'transparent' }, dayCellInnerSelected: { backgroundColor: c.primary }, @@ -1247,8 +1248,8 @@ function makeStyles(c: ThemeColors) { dayCellTextSelected: { color: '#fff' }, dayDotsRow: { minHeight: 10, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginTop: 3 }, dayDot: { width: 5, height: 5, borderRadius: 2.5, marginHorizontal: 1.5 }, - calendarLegend: { flexDirection: 'row', flexWrap: 'wrap', gap: 16, paddingHorizontal: 6, paddingTop: 8 }, - legendItem: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + calendarLegend: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.lg, paddingHorizontal: 6, paddingTop: SPACING.sm }, + legendItem: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, legendDot: { width: 8, height: 8, borderRadius: 4 }, legendTodayRing: { width: 12, height: 12, borderRadius: 6, borderWidth: 1.5, borderColor: c.primary, alignItems: 'center', justifyContent: 'center' }, legendTodayCenter: { width: 4, height: 4, borderRadius: 2, backgroundColor: c.primary }, @@ -1256,13 +1257,13 @@ function makeStyles(c: ThemeColors) { calendarSummary: { marginTop: 14, paddingTop: 14, borderTopWidth: 1, borderTopColor: c.border }, calendarSummaryLabel: { color: c.textSub, fontSize: 12, fontWeight: '700', letterSpacing: 0.8, textTransform: 'uppercase' }, calendarSummaryValue: { color: c.primaryText, fontSize: 28, fontWeight: '800', marginTop: 6 }, - calendarSummaryMeta: { ...TYPE.callout, color: c.textSub, marginTop: 4 }, + calendarSummaryMeta: { ...TYPE.callout, color: c.textSub, marginTop: SPACING.xs }, weekCard: { backgroundColor: c.card, - borderRadius: 20, - marginHorizontal: 16, - marginTop: 16, - padding: 16, + borderRadius: RADIUS.xl, + marginHorizontal: SPACING.lg, + marginTop: SPACING.lg, + padding: SPACING.lg, shadowColor: c.primary, shadowOpacity: c.isDark ? 0 : 0.08, shadowRadius: 10, @@ -1270,19 +1271,19 @@ function makeStyles(c: ThemeColors) { borderWidth: c.isDark ? 1 : 0, borderColor: c.glassBorder, }, - weekHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 12, marginBottom: 14 }, + weekHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: SPACING.md, marginBottom: 14 }, weekTitle: { color: c.primaryDark, fontSize: 20, fontWeight: '900' }, weekRange: { color: c.textSub, fontSize: 12, fontWeight: '700', marginTop: 3, textTransform: 'uppercase' }, - weekTotalPill: { backgroundColor: c.primaryLight, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 8, alignItems: 'flex-end' }, + weekTotalPill: { backgroundColor: c.primaryLight, borderRadius: 14, paddingHorizontal: SPACING.md, paddingVertical: SPACING.sm, alignItems: 'flex-end' }, weekTotalValue: { color: c.primaryText, fontSize: 18, fontWeight: '900' }, weekTotalLabel: { ...TYPE.micro, color: c.primaryDark, marginTop: 1 }, weekRow: { flexDirection: 'row', alignItems: 'center', - gap: 12, + gap: SPACING.md, borderWidth: 1, borderColor: c.border, - borderRadius: 16, + borderRadius: RADIUS.lg, padding: 10, marginTop: 9, backgroundColor: c.bg, @@ -1292,7 +1293,7 @@ function makeStyles(c: ThemeColors) { borderRadius: 14, backgroundColor: c.card, alignItems: 'center', - paddingVertical: 8, + paddingVertical: SPACING.sm, borderWidth: 1, borderColor: c.border, }, @@ -1304,17 +1305,17 @@ function makeStyles(c: ThemeColors) { weekShiftTitle: { fontSize: 16, fontWeight: '900' }, weekShiftMeta: { color: c.text, fontSize: 15, fontWeight: '800', marginTop: 5 }, weekFlightMeta: { color: c.textSub, fontSize: 12, fontWeight: '700', marginTop: 3 }, - weekRestRow: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + weekRestRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, weekRestText: { color: c.success, fontSize: 16, fontWeight: '900' }, weekEmptyText: { color: c.textSub, fontSize: 14, fontWeight: '700' }, mainCard: { backgroundColor: c.card, borderRadius: 14, - marginHorizontal: 16, marginTop: 16, - padding: 20, + marginHorizontal: SPACING.lg, marginTop: SPACING.lg, + padding: SPACING.xl, shadowColor: c.primary, shadowOpacity: c.isDark ? 0 : 0.08, shadowRadius: 10, elevation: c.isDark ? 0 : 4, borderWidth: c.isDark ? 1 : 0, borderColor: c.glassBorder, minHeight: 160, }, - selectedDayHeader: { marginBottom: 12, paddingRight: 90 }, + selectedDayHeader: { marginBottom: SPACING.md, paddingRight: 90 }, selectedDayLabel: { color: c.textSub, fontSize: 12, fontWeight: '700', letterSpacing: 0.8, textTransform: 'uppercase' }, weatherBadge: { position: 'absolute', top: 14, right: 14, @@ -1325,50 +1326,50 @@ function makeStyles(c: ThemeColors) { weatherIcon: { marginRight: 2 }, weatherPlace: { fontSize: 10, color: c.textSub, fontWeight: '600' }, weatherText: { ...TYPE.caption, color: c.text }, - shiftTypeRow: { flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 14, marginTop: 6 }, - shiftIconBox: { width: 44, height: 44, backgroundColor: c.primaryLight, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, + shiftTypeRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.md, marginBottom: 14, marginTop: 6 }, + shiftIconBox: { width: 44, height: 44, backgroundColor: c.primaryLight, borderRadius: RADIUS.md, justifyContent: 'center', alignItems: 'center' }, shiftTypeName: { ...TYPE.headline, color: c.primaryDark }, timeRow: { flexDirection: 'row', alignItems: 'center' }, timeText: { ...TYPE.title, color: c.primaryText }, - flightBadge: { marginTop: 14, backgroundColor: c.primaryLight, borderRadius: 10, paddingHorizontal: 14, paddingVertical: 8, alignSelf: 'flex-start' }, + flightBadge: { marginTop: 14, backgroundColor: c.primaryLight, borderRadius: 10, paddingHorizontal: 14, paddingVertical: SPACING.sm, alignSelf: 'flex-start' }, flightBadgeRow: { flexDirection: 'row', alignItems: 'center', gap: 6 }, flightBadgeText: { color: c.primaryText, fontWeight: '700', fontSize: 13 }, restRow: { flexDirection: 'row', alignItems: 'center', marginTop: 10 }, - restIconBox: { width: 48, height: 48, borderRadius: 14, backgroundColor: c.successSoft, alignItems: 'center', justifyContent: 'center', marginRight: 12 }, + restIconBox: { width: 48, height: 48, borderRadius: 14, backgroundColor: c.successSoft, alignItems: 'center', justifyContent: 'center', marginRight: SPACING.md }, restText: { ...TYPE.headline, color: c.success }, - emptyText: { ...TYPE.body, textAlign: 'center', color: c.textSub, marginTop: 20 }, + emptyText: { ...TYPE.body, textAlign: 'center', color: c.textSub, marginTop: SPACING.xl }, // Modal modalOverlay: { flex: 1, justifyContent: 'flex-end' }, modalBg: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)' }, modalScrollContent: { flex: 1, justifyContent: 'flex-end' }, - modalContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, paddingBottom: 100, maxHeight: '92%' }, + modalContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: SPACING.xxl, paddingBottom: 100, maxHeight: '92%' }, manualModalContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, paddingBottom: 0, maxHeight: '92%' }, - modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: SPACING.md }, modalTitle: { ...TYPE.headline }, - centerBox: { alignItems: 'center', paddingVertical: 40, gap: 12 }, + centerBox: { alignItems: 'center', paddingVertical: 40, gap: SPACING.md }, stepText: { fontSize: 16, fontWeight: '600' }, - stepLabel: { ...TYPE.body, marginBottom: 12 }, - nameRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 14, paddingHorizontal: 12, borderBottomWidth: 1, borderRadius: 8, marginBottom: 4 }, + stepLabel: { ...TYPE.body, marginBottom: SPACING.md }, + nameRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 14, paddingHorizontal: SPACING.md, borderBottomWidth: 1, borderRadius: RADIUS.sm, marginBottom: SPACING.xs }, nameText: { fontSize: 15, fontWeight: '500' }, - previewRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 10, paddingHorizontal: 4, borderBottomWidth: 1 }, + previewRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 10, paddingHorizontal: SPACING.xs, borderBottomWidth: 1 }, previewDate: { fontSize: 14, fontWeight: '600' }, - previewPill: { paddingHorizontal: 12, paddingVertical: 5, borderRadius: 8 }, + previewPill: { paddingHorizontal: SPACING.md, paddingVertical: 5, borderRadius: RADIUS.sm }, previewPillText: { fontSize: 13, fontWeight: '700' }, - secondaryBtn: { flex: 1, paddingVertical: 12, borderRadius: 10, alignItems: 'center', borderWidth: 1 }, + secondaryBtn: { flex: 1, paddingVertical: SPACING.md, borderRadius: 10, alignItems: 'center', borderWidth: 1 }, secondaryBtnText: { fontSize: 14, fontWeight: '600' }, - primaryBtn: { flex: 2, paddingVertical: 12, borderRadius: 10, alignItems: 'center' }, + primaryBtn: { flex: 2, paddingVertical: SPACING.md, borderRadius: 10, alignItems: 'center' }, primaryBtnText: { color: '#fff', fontSize: 14, fontWeight: WEIGHT.semibold }, // Edit menu - editMenuContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, paddingBottom: 40 }, - editMenuOption: { flexDirection: 'row', alignItems: 'center', gap: 14, padding: 16, borderRadius: 14, marginBottom: 10 }, + editMenuContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: SPACING.xxl, paddingBottom: 40 }, + editMenuOption: { flexDirection: 'row', alignItems: 'center', gap: 14, padding: SPACING.lg, borderRadius: 14, marginBottom: 10 }, editMenuLabel: { fontSize: 15, fontWeight: '600' }, editMenuSub: { fontSize: 12, marginTop: 2 }, // Manual entry manualLabel: { fontSize: 11, fontWeight: '700', letterSpacing: 1, marginBottom: 6 }, - manualInput: { borderWidth: 1, borderRadius: 10, paddingHorizontal: 14, paddingVertical: 12, fontSize: 16, marginBottom: 4 }, + manualInput: { borderWidth: 1, borderRadius: 10, paddingHorizontal: 14, paddingVertical: SPACING.md, fontSize: 16, marginBottom: SPACING.xs }, manualTimeRow: { flexDirection: 'row', gap: 10, marginBottom: 14 }, manualTimeInput: { flex: 1, textAlign: 'center' }, - manualTypeBtn: { flex: 1, paddingVertical: 12, borderRadius: 10, borderWidth: 1.5, alignItems: 'center' }, + manualTypeBtn: { flex: 1, paddingVertical: SPACING.md, borderRadius: 10, borderWidth: 1.5, alignItems: 'center' }, manualTypeInner: { flexDirection: 'row', alignItems: 'center', gap: 6 }, }); } diff --git a/src/screens/DesignLabScreen.tsx b/src/screens/DesignLabScreen.tsx index e4d06c0a..563a32e0 100644 --- a/src/screens/DesignLabScreen.tsx +++ b/src/screens/DesignLabScreen.tsx @@ -5,6 +5,7 @@ import { useAppTheme } from '../context/ThemeContext'; import CinematicMotionBoard from '../dev/CinematicMotionBoard'; import DesignDirectionPreview from '../dev/DesignDirectionPreview'; import { DESIGN_DIRECTIONS } from '../dev/designDirections'; +import { SPACING } from '../theme/spacing'; export default function DesignLabScreen() { const { colors } = useAppTheme(); @@ -106,14 +107,14 @@ function makeStyles(isDark: boolean) { paddingVertical: 13, }, selectorText: { fontSize: 14, fontWeight: '900' }, - motionSection: { gap: 5, marginTop: 4 }, + motionSection: { gap: 5, marginTop: SPACING.xs }, motionKicker: { fontSize: 10, fontWeight: '900', letterSpacing: 1.7 }, motionTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, motionCopy: { fontSize: 13, lineHeight: 19 }, notesCard: { borderWidth: 1, borderRadius: 24, - padding: 16, + padding: SPACING.lg, gap: 10, shadowColor: '#000', shadowOpacity: isDark ? 0.24 : 0.08, diff --git a/src/screens/FlightScreen.tsx b/src/screens/FlightScreen.tsx index 47d89fcb..7cbb2289 100644 --- a/src/screens/FlightScreen.tsx +++ b/src/screens/FlightScreen.tsx @@ -83,6 +83,7 @@ import { scheduleShiftNotifications, } from '../utils/flightNotificationScheduler'; import { TYPE, WEIGHT } from '../theme/typography'; +import { SPACING, RADIUS } from '../theme/spacing'; const WearDataSender = Platform.OS === 'android' ? NativeModules.WearDataSender : null; @@ -1412,7 +1413,7 @@ export default function FlightScreen({ isFocused = true }: { isFocused?: boolean keyExtractor={(item, i) => item.flight?.identification?.id || String(i)} renderItem={renderFlight} contentContainerStyle={{ - paddingHorizontal: 16, + paddingHorizontal: SPACING.lg, paddingTop: isOperations ? 8 : 18, paddingBottom: isOperations ? 176 : 120, }} @@ -1529,18 +1530,18 @@ function makeStyles(c: ThemeColors, isOperations = false) { const operationBorderSoft = isOperations ? 'rgba(45,212,191,0.18)' : c.border; return StyleSheet.create({ - pageHeader: { backgroundColor: isOperations ? 'rgba(2,8,12,0.90)' : c.card, paddingHorizontal: 16, paddingVertical: isOperations ? 12 : 14, borderBottomWidth: 1, borderBottomColor: operationBorderSoft, flexDirection: 'row', alignItems: 'center' }, + pageHeader: { backgroundColor: isOperations ? 'rgba(2,8,12,0.90)' : c.card, paddingHorizontal: SPACING.lg, paddingVertical: isOperations ? 12 : 14, borderBottomWidth: 1, borderBottomColor: operationBorderSoft, flexDirection: 'row', alignItems: 'center' }, notifBtn: { width: 42, height: 42, borderRadius: isOperations ? 14 : 21, backgroundColor: operationPanelStrong, justifyContent: 'center', alignItems: 'center', borderWidth: isOperations ? 1 : 0, borderColor: operationBorder }, notifBtnActive: { backgroundColor: c.primary, shadowColor: c.primary, shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.35, shadowRadius: 6, elevation: 5 }, - notifBadge: { position: 'absolute', top: -2, right: -2, width: 16, height: 16, borderRadius: 8, backgroundColor: c.danger, justifyContent: 'center', alignItems: 'center', borderWidth: 1.5, borderColor: c.card }, + notifBadge: { position: 'absolute', top: -2, right: -2, width: 16, height: 16, borderRadius: RADIUS.sm, backgroundColor: c.danger, justifyContent: 'center', alignItems: 'center', borderWidth: 1.5, borderColor: c.card }, notifBadgeTxt: { fontSize: 9, fontWeight: '800', color: '#fff' }, pageTitle: { ...(isOperations ? TYPE.titleLg : TYPE.title), color: isOperations ? c.text : c.primaryDark, letterSpacing: isOperations ? -0.5 : 0 }, pageSub: { fontSize: 13, color: c.textSub, marginTop: 2, letterSpacing: isOperations ? 0.7 : 0 }, - controlsRow: { flexDirection: 'row', gap: 8, padding: isOperations ? 9 : 12, backgroundColor: isOperations ? 'rgba(2,8,12,0.76)' : c.card, borderBottomWidth: 1, borderBottomColor: operationBorderSoft }, - sourceRow: { flexDirection: 'row', alignItems: 'flex-start', flexWrap: 'wrap', gap: 8, marginTop: isOperations ? 8 : 10, marginBottom: isOperations ? 2 : 8, marginHorizontal: 16 }, - sourceBadge: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', maxWidth: '100%', flexShrink: 1, paddingHorizontal: 10, paddingVertical: isOperations ? 6 : 7, borderRadius: 999, backgroundColor: isOperations ? 'rgba(45,212,191,0.12)' : c.primaryLight, borderWidth: 1, borderColor: operationBorder }, + controlsRow: { flexDirection: 'row', gap: SPACING.sm, padding: isOperations ? 9 : 12, backgroundColor: isOperations ? 'rgba(2,8,12,0.76)' : c.card, borderBottomWidth: 1, borderBottomColor: operationBorderSoft }, + sourceRow: { flexDirection: 'row', alignItems: 'flex-start', flexWrap: 'wrap', gap: SPACING.sm, marginTop: isOperations ? 8 : 10, marginBottom: isOperations ? 2 : 8, marginHorizontal: SPACING.lg }, + sourceBadge: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', maxWidth: '100%', flexShrink: 1, paddingHorizontal: 10, paddingVertical: isOperations ? 6 : 7, borderRadius: RADIUS.pill, backgroundColor: isOperations ? 'rgba(45,212,191,0.12)' : c.primaryLight, borderWidth: 1, borderColor: operationBorder }, sourceBadgeText: { flexShrink: 1, flexWrap: 'wrap', fontSize: 11, lineHeight: 15, fontWeight: '800', color: c.primaryDark }, - refreshBadge: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', paddingHorizontal: 10, paddingVertical: isOperations ? 6 : 7, borderRadius: 999, backgroundColor: isOperations ? 'rgba(15,23,42,0.82)' : c.cardSecondary, borderWidth: 1, borderColor: operationBorderSoft }, + refreshBadge: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', paddingHorizontal: 10, paddingVertical: isOperations ? 6 : 7, borderRadius: RADIUS.pill, backgroundColor: isOperations ? 'rgba(15,23,42,0.82)' : c.cardSecondary, borderWidth: 1, borderColor: operationBorderSoft }, refreshBadgeText: { fontSize: 11, fontWeight: '800', color: c.textSub }, segment: { flex: 1, flexDirection: 'row', backgroundColor: isOperations ? 'rgba(2,8,12,0.76)' : c.bg, borderRadius: isOperations ? 14 : 8, padding: 3, borderWidth: isOperations ? 1 : 0, borderColor: operationBorderSoft }, segBtn: { flex: 1, paddingVertical: isOperations ? 6 : 7, alignItems: 'center', borderRadius: isOperations ? 11 : 6 }, @@ -1549,10 +1550,10 @@ function makeStyles(c: ThemeColors, isOperations = false) { segBtnTextActive: { color: isOperations ? c.primaryDark : c.primary, fontWeight: '800' }, card: { backgroundColor: operationPanel, borderRadius: isOperations ? 18 : 16, marginBottom: 10, overflow: 'hidden', shadowColor: c.primary, shadowOpacity: isOperations || c.isDark ? 0 : 0.08, shadowRadius: 10, elevation: isOperations || c.isDark ? 0 : 3, borderWidth: 1, borderColor: operationBorder, borderLeftWidth: isOperations ? 4 : 1 }, cardShift: { borderWidth: 1.5, borderColor: c.warning }, - shiftBanner: { backgroundColor: c.warning, paddingVertical: 5, paddingHorizontal: 12 }, + shiftBanner: { backgroundColor: c.warning, paddingVertical: 5, paddingHorizontal: SPACING.md }, shiftBannerText: { color: '#fff', fontWeight: WEIGHT.semibold, fontSize: 11, letterSpacing: 0.5 }, cardPinned: { borderWidth: 2, borderColor: c.warning }, - pinBanner: { backgroundColor: isOperations ? 'rgba(245,158,11,0.18)' : c.warning, paddingVertical: 5, paddingHorizontal: 12, borderBottomWidth: isOperations ? 1 : 0, borderBottomColor: 'rgba(245,158,11,0.28)' }, + pinBanner: { backgroundColor: isOperations ? 'rgba(245,158,11,0.18)' : c.warning, paddingVertical: 5, paddingHorizontal: SPACING.md, borderBottomWidth: isOperations ? 1 : 0, borderBottomColor: 'rgba(245,158,11,0.28)' }, pinBannerText: { color: isOperations ? '#FBBF24' : '#fff', fontWeight: WEIGHT.semibold, fontSize: 11, letterSpacing: 0.5 }, statusPill: { paddingHorizontal: 10, paddingVertical: isOperations ? 3 : 4, borderRadius: isOperations ? 10 : 20, marginTop: isOperations ? 6 : 8, alignSelf: 'flex-end', borderWidth: isOperations ? 1 : 0, borderColor: isOperations ? operationBorderSoft : 'transparent' }, statusText: { ...TYPE.micro, letterSpacing: isOperations ? 0.6 : 0 }, @@ -1562,7 +1563,7 @@ function makeStyles(c: ThemeColors, isOperations = false) { headerText: { flex: 1, minWidth: 0 }, headerFlightNum: { color: isOperations ? c.primaryDark : '#fff', fontWeight: '900', fontSize: isOperations ? 16 : 15, lineHeight: 18, letterSpacing: isOperations ? 0.6 : 0 }, headerAirlineName: { color: isOperations ? c.textSub : 'rgba(255,255,255,0.8)', fontSize: 10, letterSpacing: isOperations ? 0.5 : 0 }, - headerMetaFlash: { alignItems: 'flex-end', borderRadius: 12, marginRight: -8, paddingHorizontal: 8, paddingVertical: 4, maxWidth: isOperations ? 150 : 142, flexShrink: 0 }, + headerMetaFlash: { alignItems: 'flex-end', borderRadius: RADIUS.md, marginRight: -8, paddingHorizontal: SPACING.sm, paddingVertical: SPACING.xs, maxWidth: isOperations ? 150 : 142, flexShrink: 0 }, headerTime: { color: isOperations ? c.text : '#fff', fontWeight: '900', fontSize: isOperations ? 19 : 18, lineHeight: 20, textAlign: 'right', fontVariant: ['tabular-nums'] }, headerDest: { color: isOperations ? c.textSub : 'rgba(255,255,255,0.8)', fontSize: 10, textAlign: 'right' }, headerAirportCode: { color: isOperations ? c.textSub : 'rgba(255,255,255,0.86)', fontSize: isOperations ? 11 : 10, lineHeight: 13, fontWeight: '900', letterSpacing: isOperations ? 1.1 : 0.8, textAlign: 'right' }, @@ -1570,49 +1571,49 @@ function makeStyles(c: ThemeColors, isOperations = false) { cardBody: { flexDirection: 'column', paddingVertical: isOperations ? 9 : 10, paddingHorizontal: 14, backgroundColor: operationPanel }, bodyInfo: { fontSize: 11, color: c.textSub }, bodyTime: { fontWeight: '700', color: c.text }, - opsRow: { flexDirection: 'row', gap: 8 }, - opsBadge: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: isOperations ? 'rgba(45,212,191,0.10)' : c.primaryLight, borderRadius: isOperations ? 12 : 10, paddingHorizontal: 10, paddingVertical: isOperations ? 6 : 8, borderWidth: isOperations ? 1 : 0, borderColor: operationBorderSoft }, + opsRow: { flexDirection: 'row', gap: SPACING.sm }, + opsBadge: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, backgroundColor: isOperations ? 'rgba(45,212,191,0.10)' : c.primaryLight, borderRadius: isOperations ? 12 : 10, paddingHorizontal: 10, paddingVertical: isOperations ? 6 : 8, borderWidth: isOperations ? 1 : 0, borderColor: operationBorderSoft }, opsIcon: { fontSize: 16 }, opsLabel: { fontSize: 10, fontWeight: '600', color: c.textSub, letterSpacing: 0.5 }, opsTime: { fontSize: 13, fontWeight: '800', color: c.primaryDark }, pinBtn: { width: 34, height: 34, borderRadius: 17, backgroundColor: 'rgba(255,255,255,0.15)', justifyContent: 'center', alignItems: 'center' }, pinBtnActive: { backgroundColor: 'rgba(245,158,11,0.25)' }, - filterBtn: { width: 42, height: 42, borderRadius: isOperations ? 14 : 21, backgroundColor: operationPanelStrong, justifyContent: 'center', alignItems: 'center', marginRight: 8, borderWidth: isOperations ? 1 : 0, borderColor: operationBorder }, + filterBtn: { width: 42, height: 42, borderRadius: isOperations ? 14 : 21, backgroundColor: operationPanelStrong, justifyContent: 'center', alignItems: 'center', marginRight: SPACING.sm, borderWidth: isOperations ? 1 : 0, borderColor: operationBorder }, filterBtnActive: { backgroundColor: c.primary, shadowColor: c.primary, shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.35, shadowRadius: 6, elevation: 5 }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.55)', justifyContent: 'flex-end' }, - alertOverlay: { flex: 1, backgroundColor: 'rgba(2,6,23,0.55)', justifyContent: 'center', alignItems: 'center', padding: 24 }, + alertOverlay: { flex: 1, backgroundColor: 'rgba(2,6,23,0.55)', justifyContent: 'center', alignItems: 'center', padding: SPACING.xxl }, alertCard: { width: '100%', maxWidth: 440, - borderRadius: 20, + borderRadius: RADIUS.xl, padding: 18, backgroundColor: c.card, borderWidth: 1, borderColor: c.glassBorder, }, - alertHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, gap: 10 }, + alertHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: SPACING.md, gap: 10 }, alertIconWrap: { width: 34, height: 34, borderRadius: 17, alignItems: 'center', justifyContent: 'center' }, alertSuccess: { backgroundColor: c.success }, alertWarning: { backgroundColor: c.warning }, alertInfo: { backgroundColor: c.primary }, alertTitle: { flex: 1, fontSize: 28, fontWeight: '900', color: c.text }, - alertMessage: { fontSize: 17, lineHeight: 24, color: c.textSub, marginBottom: 16 }, - alertBtn: { alignSelf: 'flex-end', paddingHorizontal: 18, paddingVertical: 10, borderRadius: 12, backgroundColor: c.primary }, + alertMessage: { fontSize: 17, lineHeight: 24, color: c.textSub, marginBottom: SPACING.lg }, + alertBtn: { alignSelf: 'flex-end', paddingHorizontal: 18, paddingVertical: 10, borderRadius: RADIUS.md, backgroundColor: c.primary }, alertBtnTxt: { color: '#fff', fontSize: 15, fontWeight: '800' }, - filterSheet: { backgroundColor: isOperations ? '#071414' : c.card, borderTopLeftRadius: isOperations ? 28 : 24, borderTopRightRadius: isOperations ? 28 : 24, padding: 20, paddingBottom: 36, borderTopWidth: isOperations ? 1 : 0, borderColor: operationBorder }, - filterSheetHandle: { width: 36, height: 4, borderRadius: 2, backgroundColor: isOperations ? 'rgba(45,212,191,0.34)' : c.border, alignSelf: 'center', marginBottom: 16 }, - filterSheetTitle: { fontSize: 16, fontWeight: '800', color: isOperations ? c.primaryDark : c.text, marginBottom: 16, textAlign: 'center', letterSpacing: isOperations ? 0.8 : 0 }, - notifSheetSub: { fontSize: 13, color: c.textSub, textAlign: 'center', marginTop: -8, marginBottom: 16 }, - notifRow: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingVertical: 10 }, + filterSheet: { backgroundColor: isOperations ? '#071414' : c.card, borderTopLeftRadius: isOperations ? 28 : 24, borderTopRightRadius: isOperations ? 28 : 24, padding: SPACING.xl, paddingBottom: 36, borderTopWidth: isOperations ? 1 : 0, borderColor: operationBorder }, + filterSheetHandle: { width: 36, height: 4, borderRadius: 2, backgroundColor: isOperations ? 'rgba(45,212,191,0.34)' : c.border, alignSelf: 'center', marginBottom: SPACING.lg }, + filterSheetTitle: { fontSize: 16, fontWeight: '800', color: isOperations ? c.primaryDark : c.text, marginBottom: SPACING.lg, textAlign: 'center', letterSpacing: isOperations ? 0.8 : 0 }, + notifSheetSub: { fontSize: 13, color: c.textSub, textAlign: 'center', marginTop: -8, marginBottom: SPACING.lg }, + notifRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.md, paddingVertical: 10 }, notifRowTextWrap: { flex: 1 }, notifRowTitle: { fontSize: 14, fontWeight: '700', color: c.text }, notifRowSub: { fontSize: 12, color: c.textSub, marginTop: 2 }, notifDivider: { height: 1, backgroundColor: c.border, marginVertical: 10 }, notifMinutesRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 10 }, - notifStepper: { flexDirection: 'row', alignItems: 'center', backgroundColor: c.bg, borderRadius: 10, padding: 4 }, - notifStepperBtn: { width: 32, height: 32, borderRadius: 8, alignItems: 'center', justifyContent: 'center', backgroundColor: c.card }, + notifStepper: { flexDirection: 'row', alignItems: 'center', backgroundColor: c.bg, borderRadius: 10, padding: SPACING.xs }, + notifStepperBtn: { width: 32, height: 32, borderRadius: RADIUS.sm, alignItems: 'center', justifyContent: 'center', backgroundColor: c.card }, notifStepperValue: { minWidth: 54, textAlign: 'center', fontSize: 14, fontWeight: '800', color: c.primaryDark }, - filterOption: { flexDirection: 'row', alignItems: 'center', gap: 12, padding: 14, borderRadius: 14, marginBottom: 8, borderWidth: 1.5 }, + filterOption: { flexDirection: 'row', alignItems: 'center', gap: SPACING.md, padding: 14, borderRadius: 14, marginBottom: SPACING.sm, borderWidth: 1.5 }, filterOptionActive: { borderWidth: 1.5, ...filterOptionActiveShadow, @@ -1629,7 +1630,7 @@ function makeStyles(c: ThemeColors, isOperations = false) { }, filterBrandDot: { width: 10, height: 10, borderRadius: 5 }, smFooter: { flexDirection: 'row', flexWrap: 'wrap', gap: 6, paddingHorizontal: 14, paddingBottom: isOperations ? 8 : 10, backgroundColor: operationPanel, borderTopWidth: isOperations ? 1 : 0, borderTopColor: operationBorderSoft }, - smPill: { flexDirection: 'row', alignItems: 'center', gap: 4, backgroundColor: isOperations ? 'rgba(45,212,191,0.10)' : c.primaryLight, borderRadius: isOperations ? 10 : 8, paddingHorizontal: 8, paddingVertical: isOperations ? 3 : 4, borderWidth: isOperations ? 1 : 0, borderColor: operationBorderSoft }, + smPill: { flexDirection: 'row', alignItems: 'center', gap: SPACING.xs, backgroundColor: isOperations ? 'rgba(45,212,191,0.10)' : c.primaryLight, borderRadius: isOperations ? 10 : 8, paddingHorizontal: SPACING.sm, paddingVertical: isOperations ? 3 : 4, borderWidth: isOperations ? 1 : 0, borderColor: operationBorderSoft }, smPillText: { fontSize: 11, fontWeight: '700', color: c.primaryDark }, }); } diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 64acbb85..5e49ec13 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -41,6 +41,7 @@ import { ShiftWidget } from '../widgets/ShiftWidget'; import { parseOcrShiftText } from '../utils/ocrShiftParser'; import { useLanguage } from '../context/LanguageContext'; import { TYPE } from '../theme/typography'; +import { SPACING, RADIUS } from '../theme/spacing'; const GOLD = '#F59E0B'; @@ -124,7 +125,7 @@ function PinnedFlightCardComponent({ item, colors, isOperations = false }: { ite return ( - + {flightNumber} @@ -156,17 +157,17 @@ function PinnedFlightCardComponent({ item, colors, isOperations = false }: { ite {/* Body */} - + {tab === 'departures' ? ( - - + + CHECK-IN {fmt(ops.checkInOpen)} – {fmt(ops.checkInClose)} - + GATE @@ -181,10 +182,10 @@ function PinnedFlightCardComponent({ item, colors, isOperations = false }: { ite )} {/* Status row */} - + {statusText} - + {t('homePinned')} @@ -232,31 +233,31 @@ function EasyJetOverlapMonitor({ overlappingFlights, tickerMs, colors, t, locale return ( {/* Header */} - - + + easyJet Overlap Active - + Aggiornato al secondo - + Rilevata fascia oraria con più voli easyJet in arrivo sovrapposti. Monitoraggio in tempo reale attivo. {/* Flight rows */} - + {overlappingFlights.map((item, idx) => { const flightNumber = item.flight?.identification?.number?.default || 'N/A'; const origin = getFlightAirportLabel(item.flight?.airport?.origin, 'N/A'); @@ -274,10 +275,10 @@ function EasyJetOverlapMonitor({ overlappingFlights, tickerMs, colors, t, locale - + {flightNumber} @@ -881,16 +882,16 @@ function makeStyles(c: ThemeColors, isOperations = false) { const operationShadow = isOperations ? 0 : undefined; return StyleSheet.create({ hiddenWV: { height: 1, width: 1, opacity: 0, position: 'absolute', top: -100 }, - topRow: { flexDirection: 'row', gap: 12, padding: 16, paddingBottom: 8 }, - weatherCard: { flex: 1, backgroundColor: operationPanel, borderRadius: isOperations ? 20 : 18, padding: 16, alignItems: 'center', shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: operationShadow ?? 0.12, shadowRadius: 12, elevation: isOperations ? 0 : 4, borderWidth: 1, borderColor: operationBorder }, - weatherIcon: { marginBottom: 4 }, + topRow: { flexDirection: 'row', gap: SPACING.md, padding: SPACING.lg, paddingBottom: SPACING.sm }, + weatherCard: { flex: 1, backgroundColor: operationPanel, borderRadius: isOperations ? 20 : 18, padding: SPACING.lg, alignItems: 'center', shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: operationShadow ?? 0.12, shadowRadius: 12, elevation: isOperations ? 0 : 4, borderWidth: 1, borderColor: operationBorder }, + weatherIcon: { marginBottom: SPACING.xs }, weatherTemp: { fontSize: isOperations ? 30 : 28, fontWeight: '800', color: c.primaryDark }, weatherDesc: { fontSize: 11, color: c.textSub, textAlign: 'center', marginTop: 2, letterSpacing: isOperations ? 0.4 : 0 }, dateCard: { width: isOperations ? 96 : 90, backgroundColor: isOperations ? 'rgba(45,212,191,0.12)' : c.primaryDark, borderRadius: isOperations ? 20 : 18, padding: 14, alignItems: 'center', justifyContent: 'center', shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.30, shadowRadius: 12, elevation: isOperations ? 0 : 6, borderWidth: isOperations ? 1 : 0, borderColor: operationBorder }, dateToday: { ...TYPE.micro, color: isOperations ? 'rgba(153,246,228,0.72)' : 'rgba(255,255,255,0.6)', letterSpacing: 1.7 }, dateNum: { ...TYPE.display, color: isOperations ? c.primaryDark : '#fff' }, dateMonth: { fontSize: 12, color: isOperations ? c.textSub : 'rgba(255,255,255,0.7)', marginTop: 2 }, - operationalCard: { marginHorizontal: 16, marginTop: 8, backgroundColor: operationPanel, borderRadius: isOperations ? 24 : 20, padding: 16, borderWidth: 1, borderColor: operationBorder, gap: 13, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.08, shadowRadius: 12, elevation: isOperations ? 0 : 3 }, + operationalCard: { marginHorizontal: SPACING.lg, marginTop: SPACING.sm, backgroundColor: operationPanel, borderRadius: isOperations ? 24 : 20, padding: SPACING.lg, borderWidth: 1, borderColor: operationBorder, gap: 13, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.08, shadowRadius: 12, elevation: isOperations ? 0 : 3 }, operationalHeader: { flexDirection: 'row', alignItems: 'center', gap: 14 }, operationalTitleBlock: { flex: 1, gap: 3 }, operationalKicker: { fontSize: 10, fontWeight: '900', letterSpacing: 1.8, color: isOperations ? 'rgba(153,246,228,0.70)' : c.textMuted }, @@ -900,49 +901,49 @@ function makeStyles(c: ThemeColors, isOperations = false) { operationalBeaconActive: { backgroundColor: c.success }, operationalBeaconNext: { backgroundColor: c.primary }, operationalBeaconRest: { backgroundColor: c.info }, - summaryBadgeRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 }, - summaryBadge: { flexDirection: 'row', alignItems: 'center', gap: 5, alignSelf: 'flex-start', borderRadius: 999, borderWidth: 1, borderColor: operationBorder, backgroundColor: isOperations ? 'rgba(45,212,191,0.12)' : c.primaryLight, paddingHorizontal: 9, paddingVertical: 5 }, + summaryBadgeRow: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.sm }, + summaryBadge: { flexDirection: 'row', alignItems: 'center', gap: 5, alignSelf: 'flex-start', borderRadius: RADIUS.pill, borderWidth: 1, borderColor: operationBorder, backgroundColor: isOperations ? 'rgba(45,212,191,0.12)' : c.primaryLight, paddingHorizontal: 9, paddingVertical: 5 }, summaryBadgeText: { fontSize: 11, fontWeight: '900', color: c.primaryDark }, - healthGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 }, - healthChip: { width: '48%', minWidth: 134, flexGrow: 1, flexDirection: 'row', alignItems: 'center', gap: 8, borderRadius: 14, borderWidth: 1, paddingHorizontal: 10, paddingVertical: 9 }, + healthGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.sm }, + healthChip: { width: '48%', minWidth: 134, flexGrow: 1, flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, borderRadius: 14, borderWidth: 1, paddingHorizontal: 10, paddingVertical: 9 }, healthText: { flex: 1, minWidth: 0 }, healthLabel: { ...TYPE.glyph, fontWeight: '900', letterSpacing: 0.8, textTransform: 'uppercase' }, healthValue: { fontSize: 12, fontWeight: '900', marginTop: 1 }, - sectionTitle: { fontSize: 12, fontWeight: '800', color: isOperations ? 'rgba(153,246,228,0.66)' : c.textSub, letterSpacing: isOperations ? 1.6 : 0.5, marginHorizontal: 16, marginTop: 16, marginBottom: 8, textTransform: 'uppercase' }, - shiftCard: { backgroundColor: operationPanel, borderRadius: isOperations ? 22 : 18, marginHorizontal: 16, padding: isOperations ? 18 : 16, flexDirection: 'row', gap: 14, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.10, shadowRadius: 12, elevation: isOperations ? 0 : 4, minHeight: isOperations ? 104 : 90, borderWidth: 1, borderColor: operationBorder }, - shiftStrip: { width: isOperations ? 5 : 4, borderRadius: 999, backgroundColor: c.primary, marginRight: 2 }, - shiftBadgeRow: { flexDirection: 'row', marginBottom: 8 }, - inProgressBadge: { backgroundColor: isOperations ? 'rgba(45,212,191,0.14)' : '#D1FAE5', paddingHorizontal: 10, paddingVertical: 3, borderRadius: 20, borderWidth: isOperations ? 1 : 0, borderColor: isOperations ? operationBorder : 'transparent' }, + sectionTitle: { fontSize: 12, fontWeight: '800', color: isOperations ? 'rgba(153,246,228,0.66)' : c.textSub, letterSpacing: isOperations ? 1.6 : 0.5, marginHorizontal: SPACING.lg, marginTop: SPACING.lg, marginBottom: SPACING.sm, textTransform: 'uppercase' }, + shiftCard: { backgroundColor: operationPanel, borderRadius: isOperations ? 22 : 18, marginHorizontal: SPACING.lg, padding: isOperations ? 18 : 16, flexDirection: 'row', gap: 14, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.10, shadowRadius: 12, elevation: isOperations ? 0 : 4, minHeight: isOperations ? 104 : 90, borderWidth: 1, borderColor: operationBorder }, + shiftStrip: { width: isOperations ? 5 : 4, borderRadius: RADIUS.pill, backgroundColor: c.primary, marginRight: 2 }, + shiftBadgeRow: { flexDirection: 'row', marginBottom: SPACING.sm }, + inProgressBadge: { backgroundColor: isOperations ? 'rgba(45,212,191,0.14)' : '#D1FAE5', paddingHorizontal: 10, paddingVertical: 3, borderRadius: RADIUS.xl, borderWidth: isOperations ? 1 : 0, borderColor: isOperations ? operationBorder : 'transparent' }, inProgressText: { ...TYPE.micro, color: isOperations ? c.primaryDark : c.success, letterSpacing: isOperations ? 1 : 0 }, - shiftTitle: { ...TYPE.headline, color: isOperations ? c.text : c.primaryDark, marginBottom: 4 }, - shiftTime: { fontSize: isOperations ? 28 : 22, fontWeight: '900', color: isOperations ? c.primaryDark : c.primary, marginBottom: 4, fontVariant: ['tabular-nums'] }, - timelineCard: { backgroundColor: operationPanel, borderRadius: isOperations ? 22 : 18, marginHorizontal: 16, marginTop: 12, padding: 16, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.08, shadowRadius: 10, elevation: isOperations ? 0 : 3, borderWidth: 1, borderColor: operationBorder }, + shiftTitle: { ...TYPE.headline, color: isOperations ? c.text : c.primaryDark, marginBottom: SPACING.xs }, + shiftTime: { fontSize: isOperations ? 28 : 22, fontWeight: '900', color: isOperations ? c.primaryDark : c.primary, marginBottom: SPACING.xs, fontVariant: ['tabular-nums'] }, + timelineCard: { backgroundColor: operationPanel, borderRadius: isOperations ? 22 : 18, marginHorizontal: SPACING.lg, marginTop: SPACING.md, padding: SPACING.lg, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.08, shadowRadius: 10, elevation: isOperations ? 0 : 3, borderWidth: 1, borderColor: operationBorder }, restRow: { flexDirection: 'row', alignItems: 'center' }, - restIconWrap: { width: 40, height: 40, borderRadius: 12, backgroundColor: c.success + '22', alignItems: 'center', justifyContent: 'center', marginRight: 12 }, + restIconWrap: { width: 40, height: 40, borderRadius: RADIUS.md, backgroundColor: c.success + '22', alignItems: 'center', justifyContent: 'center', marginRight: SPACING.md }, restText: { fontSize: 18, fontWeight: '700', color: c.success }, emptyShift: { ...TYPE.body, color: c.textSub, textAlign: 'center', flex: 1 }, - uploadToggle: { flexDirection: 'row', alignItems: 'center', gap: 10, marginHorizontal: 16, marginTop: 16, backgroundColor: c.card, borderRadius: 18, paddingHorizontal: 16, paddingVertical: 14, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: 0.08, shadowRadius: 8, elevation: 3, borderWidth: 1, borderColor: c.glassBorder }, + uploadToggle: { flexDirection: 'row', alignItems: 'center', gap: 10, marginHorizontal: SPACING.lg, marginTop: SPACING.lg, backgroundColor: c.card, borderRadius: 18, paddingHorizontal: SPACING.lg, paddingVertical: 14, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: 0.08, shadowRadius: 8, elevation: 3, borderWidth: 1, borderColor: c.glassBorder }, uploadToggleText: { flex: 1, fontSize: 15, fontWeight: '600', color: c.primaryDark }, - uploadSection: { marginHorizontal: 16, backgroundColor: c.card, borderRadius: 18, padding: 16, marginTop: 2, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: 0.06, shadowRadius: 6, elevation: 2, borderWidth: 1, borderColor: c.glassBorder }, + uploadSection: { marginHorizontal: SPACING.lg, backgroundColor: c.card, borderRadius: 18, padding: SPACING.lg, marginTop: 2, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: 0.06, shadowRadius: 6, elevation: 2, borderWidth: 1, borderColor: c.glassBorder }, uploadDesc: { fontSize: 13, color: c.textSub, lineHeight: 19, marginBottom: 14 }, - scanBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: c.primaryDark, borderRadius: 12, paddingVertical: 13, paddingHorizontal: 20 }, + scanBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: c.primaryDark, borderRadius: RADIUS.md, paddingVertical: 13, paddingHorizontal: SPACING.xl }, scanBtnText: { color: '#fff', fontWeight: '700', fontSize: 15 }, - imagesRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 12 }, + imagesRow: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.sm, marginTop: SPACING.md }, thumb: { width: '47%', height: 120, borderRadius: 10, resizeMode: 'cover' }, - ocrResult: { backgroundColor: c.cardSecondary, borderRadius: 12, padding: 12, marginTop: 12 }, + ocrResult: { backgroundColor: c.cardSecondary, borderRadius: RADIUS.md, padding: SPACING.md, marginTop: SPACING.md }, ocrTitle: { fontSize: 12, fontWeight: '700', color: c.textSub, marginBottom: 6 }, ocrText: { fontSize: 12, color: c.text, lineHeight: 18 }, - syncBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: c.primary, borderRadius: 12, paddingVertical: 13, marginTop: 12 }, + syncBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: c.primary, borderRadius: RADIUS.md, paddingVertical: 13, marginTop: SPACING.md }, syncBtnText: { color: '#fff', fontWeight: '700', fontSize: 15 }, - modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.55)', justifyContent: 'center', alignItems: 'center', padding: 20 }, - modalContent: { backgroundColor: c.isDark ? c.bg : c.card, width: '100%', borderRadius: 20, padding: 20, shadowColor: '#000', shadowOpacity: 0.15, shadowRadius: 14, elevation: 8, borderWidth: 1, borderColor: c.glassBorder }, + modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.55)', justifyContent: 'center', alignItems: 'center', padding: SPACING.xl }, + modalContent: { backgroundColor: c.isDark ? c.bg : c.card, width: '100%', borderRadius: RADIUS.xl, padding: SPACING.xl, shadowColor: '#000', shadowOpacity: 0.15, shadowRadius: 14, elevation: 8, borderWidth: 1, borderColor: c.glassBorder }, modalTitle: { fontSize: 17, fontWeight: '700', color: c.primaryDark, marginBottom: 14 }, - modalLabel: { fontSize: 12, fontWeight: '700', color: c.textSub, marginBottom: 8 }, - modalInput: { borderWidth: 1, borderColor: c.border, borderRadius: 10, padding: 12, marginBottom: 10, fontSize: 14, color: c.text }, + modalLabel: { fontSize: 12, fontWeight: '700', color: c.textSub, marginBottom: SPACING.sm }, + modalInput: { borderWidth: 1, borderColor: c.border, borderRadius: 10, padding: SPACING.md, marginBottom: 10, fontSize: 14, color: c.text }, modalBtn: { flex: 1, padding: 14, borderRadius: 10, alignItems: 'center' }, - typeBtn: { flex: 1, padding: 12, borderRadius: 10, backgroundColor: c.bg, alignItems: 'center' }, - inputLabel: { fontSize: 11, color: c.textSub, fontWeight: '700', marginBottom: 4, letterSpacing: 0.5 }, - modeBtn: { flex: 1, backgroundColor: c.primary, borderRadius: 14, paddingVertical: 20, alignItems: 'center', justifyContent: 'center', gap: 8, shadowColor: c.primary, shadowOpacity: 0.25, shadowRadius: 8, elevation: 4 }, + typeBtn: { flex: 1, padding: SPACING.md, borderRadius: 10, backgroundColor: c.bg, alignItems: 'center' }, + inputLabel: { fontSize: 11, color: c.textSub, fontWeight: '700', marginBottom: SPACING.xs, letterSpacing: 0.5 }, + modeBtn: { flex: 1, backgroundColor: c.primary, borderRadius: 14, paddingVertical: SPACING.xl, alignItems: 'center', justifyContent: 'center', gap: SPACING.sm, shadowColor: c.primary, shadowOpacity: 0.25, shadowRadius: 8, elevation: 4 }, modeBtnText: { color: '#fff', fontWeight: '700', fontSize: 13 }, }); } diff --git a/src/screens/ManualsScreen.tsx b/src/screens/ManualsScreen.tsx index e5738e7b..1c0f7c92 100644 --- a/src/screens/ManualsScreen.tsx +++ b/src/screens/ManualsScreen.tsx @@ -9,6 +9,7 @@ import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; import { useLanguage } from '../context/LanguageContext'; import { TYPE } from '../theme/typography'; import { enableLegacyAndroidLayoutAnimation } from '../utils/layoutAnimation'; +import { SPACING, RADIUS } from '../theme/spacing'; const STORAGE_KEY = 'manuals_data_v2'; @@ -343,14 +344,14 @@ function CommandsTab({ commands, colors }: { commands: DCSCommand[]; colors: any return ( {categories.map(cat => ( - + {filtered.filter(c => c.category === cat).map((c, i) => ( - + {c.desc} @@ -491,13 +492,13 @@ function ManualItemRow({ function makeSectionStyles(c: ThemeColors) { return StyleSheet.create({ wrapper: { - marginBottom: 12, + marginBottom: SPACING.md, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingVertical: 10, paddingHorizontal: 4, + paddingVertical: 10, paddingHorizontal: SPACING.xs, borderBottomWidth: 1, borderBottomColor: c.border, - marginBottom: 8, + marginBottom: SPACING.sm, }, title: { fontSize: 12, fontWeight: '700', color: c.textSub, letterSpacing: 0.8 }, body: { paddingLeft: 0 }, @@ -529,7 +530,7 @@ function SectionBlock({ {section.title} - + {editMode && ( @@ -553,7 +554,7 @@ function SectionBlock({ ))} {editMode && ( @@ -569,16 +570,16 @@ function SectionBlock({ const modalStyles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }, scrollContent: { flexGrow: 1, justifyContent: 'flex-end' }, - sheet: { borderTopLeftRadius: 20, borderTopRightRadius: 20, padding: 20, paddingBottom: 36, maxHeight: '92%' }, - title: { ...TYPE.headline, marginBottom: 16 }, - label: { ...TYPE.caption, marginBottom: 4, marginTop: 12 }, - input: { borderWidth: 1, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 9, fontSize: 14 }, + sheet: { borderTopLeftRadius: RADIUS.xl, borderTopRightRadius: RADIUS.xl, padding: SPACING.xl, paddingBottom: 36, maxHeight: '92%' }, + title: { ...TYPE.headline, marginBottom: SPACING.lg }, + label: { ...TYPE.caption, marginBottom: SPACING.xs, marginTop: SPACING.md }, + input: { borderWidth: 1, borderRadius: RADIUS.sm, paddingHorizontal: SPACING.md, paddingVertical: 9, fontSize: 14 }, inputMulti: { minHeight: 100, paddingTop: 9 }, - colorRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, marginTop: 8 }, + colorRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, marginTop: SPACING.sm }, colorDot: { width: 28, height: 28, borderRadius: 14 }, colorDotSelected: { borderWidth: 3, borderColor: '#000', transform: [{ scale: 1.2 }] }, - btnRow: { flexDirection: 'row', justifyContent: 'flex-end', gap: 8, marginTop: 20 }, - btn: { paddingHorizontal: 18, paddingVertical: 9, borderRadius: 8 }, + btnRow: { flexDirection: 'row', justifyContent: 'flex-end', gap: SPACING.sm, marginTop: SPACING.xl }, + btn: { paddingHorizontal: 18, paddingVertical: 9, borderRadius: RADIUS.sm }, btnCancel: { backgroundColor: 'transparent', borderWidth: 1, borderColor: '#ccc' }, btnSave: {}, btnDanger: { marginRight: 'auto', backgroundColor: '#FEE2E2' }, @@ -592,7 +593,7 @@ function makeStyles(c: ThemeColors) { root: { flex: 1, backgroundColor: c.bg }, header: { flexDirection: 'row', alignItems: 'center', gap: 10, - paddingHorizontal: 16, paddingVertical: 13, + paddingHorizontal: SPACING.lg, paddingVertical: 13, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border, }, @@ -603,12 +604,12 @@ function makeStyles(c: ThemeColors) { maxHeight: 62, }, airlineBarContent: { - paddingHorizontal: 12, paddingVertical: 10, gap: 8, + paddingHorizontal: SPACING.md, paddingVertical: 10, gap: SPACING.sm, }, airlineChip: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 14, paddingVertical: 7, - borderRadius: 20, borderWidth: 1.5, borderColor: c.border, + borderRadius: RADIUS.xl, borderWidth: 1.5, borderColor: c.border, backgroundColor: c.card, }, airlineCode: { fontSize: 11, fontWeight: '800', color: c.textSub }, @@ -620,12 +621,12 @@ function makeStyles(c: ThemeColors) { }, bannerCode: { fontSize: 28, fontWeight: '900', letterSpacing: 1 }, bannerName: { fontSize: 15, fontWeight: '600', marginTop: 2 }, - bannerSub: { fontSize: 12, marginTop: 4 }, + bannerSub: { fontSize: 12, marginTop: SPACING.xs }, addBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, - paddingVertical: 10, paddingHorizontal: 12, - borderWidth: 1, borderStyle: 'dashed', borderRadius: 8, - marginBottom: 8, + paddingVertical: 10, paddingHorizontal: SPACING.md, + borderWidth: 1, borderStyle: 'dashed', borderRadius: RADIUS.sm, + marginBottom: SPACING.sm, }, addBtnText: { ...TYPE.callout }, }); @@ -1077,7 +1078,7 @@ export default function ManualsScreen() { key={tab} onPress={() => setActiveTab(tab)} style={{ - paddingHorizontal: 20, paddingVertical: 8, borderRadius: 8, + paddingHorizontal: SPACING.xl, paddingVertical: SPACING.sm, borderRadius: RADIUS.sm, backgroundColor: activeTab === tab ? colors.primary : 'transparent', borderWidth: activeTab === tab ? 0 : 1, borderColor: colors.border, diff --git a/src/screens/NotepadScreen.tsx b/src/screens/NotepadScreen.tsx index 1f45cd11..377081e9 100644 --- a/src/screens/NotepadScreen.tsx +++ b/src/screens/NotepadScreen.tsx @@ -8,6 +8,7 @@ import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; import { TYPE } from '../theme/typography'; import { useLanguage } from '../context/LanguageContext'; +import { SPACING } from '../theme/spacing'; const STORAGE_KEY = 'aerostaff_notepad_v1'; @@ -16,18 +17,18 @@ function makeStyles(c: ThemeColors) { root: { flex: 1, backgroundColor: c.bg }, toolbar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingHorizontal: 16, paddingVertical: 12, + paddingHorizontal: SPACING.lg, paddingVertical: SPACING.md, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border, }, - titleRow: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + titleRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, title: { ...TYPE.headline, color: c.primaryDark }, - actions: { flexDirection: 'row', alignItems: 'center', gap: 8 }, - iconBtn: { padding: 8, borderRadius: 10 }, + actions: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, + iconBtn: { padding: SPACING.sm, borderRadius: 10 }, saveBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: c.primary, borderRadius: 10, - paddingHorizontal: 14, paddingVertical: 8, + paddingHorizontal: 14, paddingVertical: SPACING.sm, }, // Dims the entire save button (background + icon + label) when content is // already saved — intentional: the full-button fade signals an inactive state. @@ -35,7 +36,7 @@ function makeStyles(c: ThemeColors) { saveTxt: { ...TYPE.callout, color: '#fff' }, statusBar: { flexDirection: 'row', alignItems: 'center', gap: 6, - paddingHorizontal: 16, paddingVertical: 6, + paddingHorizontal: SPACING.lg, paddingVertical: 6, backgroundColor: c.bg, borderBottomWidth: 1, borderBottomColor: c.border, }, diff --git a/src/screens/OnboardingScreen.tsx b/src/screens/OnboardingScreen.tsx index ce62780a..b6df72e3 100644 --- a/src/screens/OnboardingScreen.tsx +++ b/src/screens/OnboardingScreen.tsx @@ -9,6 +9,7 @@ import { useAirport } from '../context/AirportContext'; import { formatAirportSettingLabel } from '../utils/airportSettings'; import { getFlightProviderSettingsState, type FlightProviderSettingsState } from '../utils/flightProviderSettings'; import { getNotificationDebugSnapshot, NOTIF_ENABLED_KEY, type NotificationDebugSnapshot } from '../utils/notificationDiagnostics'; +import { SPACING, RADIUS } from '../theme/spacing'; import { buildSetupChecklist, ONBOARDING_SETUP_STORAGE_KEY, @@ -246,13 +247,13 @@ export default function OnboardingScreen({ const styles = StyleSheet.create({ root: { flex: 1 }, - content: { padding: 16, paddingBottom: 116, gap: 14 }, - heroText: { flex: 1, gap: 4 }, + content: { padding: SPACING.lg, paddingBottom: 116, gap: 14 }, + heroText: { flex: 1, gap: SPACING.xs }, heroIcon: { width: 56, height: 56, borderRadius: 18, alignItems: 'center', justifyContent: 'center' }, kicker: { fontSize: 10, fontWeight: '900', letterSpacing: 1.7 }, title: { fontSize: 28, fontWeight: '900', letterSpacing: -0.7 }, copy: { fontSize: 13, lineHeight: 19 }, - progressCard: { borderWidth: 1, borderRadius: 20, padding: 16, flexDirection: 'row', alignItems: 'center', gap: 12 }, + progressCard: { borderWidth: 1, borderRadius: RADIUS.xl, padding: SPACING.lg, flexDirection: 'row', alignItems: 'center', gap: SPACING.md }, progressTitle: { fontSize: 18, fontWeight: '900' }, progressSub: { fontSize: 12, lineHeight: 17, marginTop: 2 }, steps: { gap: 10 }, @@ -263,15 +264,15 @@ const styles = StyleSheet.create({ stepTitle: { fontSize: 15, fontWeight: '900' }, required: { fontSize: 9, fontWeight: '900', letterSpacing: 0.8, textTransform: 'uppercase' }, stepDetail: { fontSize: 12, lineHeight: 17 }, - stepAction: { borderWidth: 1, borderRadius: 12, paddingHorizontal: 10, paddingVertical: 8 }, + stepAction: { borderWidth: 1, borderRadius: RADIUS.md, paddingHorizontal: 10, paddingVertical: SPACING.sm }, stepActionText: { fontSize: 12, fontWeight: '900' }, airportCard: { borderWidth: 1, borderRadius: 18, padding: 14 }, airportLabel: { fontSize: 10, fontWeight: '900', letterSpacing: 1.2, textTransform: 'uppercase' }, airportValue: { fontSize: 15, fontWeight: '900', marginTop: 3 }, footerActions: { flexDirection: 'row', gap: 10 }, - secondaryBtn: { flex: 1, borderWidth: 1, borderRadius: 16, paddingVertical: 14, alignItems: 'center' }, + secondaryBtn: { flex: 1, borderWidth: 1, borderRadius: RADIUS.lg, paddingVertical: 14, alignItems: 'center' }, secondaryText: { fontSize: 14, fontWeight: '900' }, - primaryBtn: { flex: 1.3, borderRadius: 16, paddingVertical: 14, alignItems: 'center' }, + primaryBtn: { flex: 1.3, borderRadius: RADIUS.lg, paddingVertical: 14, alignItems: 'center' }, primaryText: { color: '#fff', fontSize: 14, fontWeight: '900' }, }); @@ -280,7 +281,7 @@ function makeStyles(isOperations: boolean) { hero: { borderWidth: 1, borderRadius: isOperations ? 26 : 22, - padding: 16, + padding: SPACING.lg, flexDirection: 'row', gap: 14, alignItems: 'center', diff --git a/src/screens/PasswordScreen.tsx b/src/screens/PasswordScreen.tsx index 175c1c92..30ca468a 100644 --- a/src/screens/PasswordScreen.tsx +++ b/src/screens/PasswordScreen.tsx @@ -10,6 +10,7 @@ import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; import { TYPE } from '../theme/typography'; import { useLanguage } from '../context/LanguageContext'; import { secureWipeAsyncStorageItem } from '../utils/secureWipe'; +import { SPACING, RADIUS } from '../theme/spacing'; const PASSWORDS_KEY = 'aerostaff_passwords_v1'; const PIN_KEY = 'aerostaff_pin_v1'; @@ -107,7 +108,7 @@ function PinOverlay({ onUnlock, onCancel, title }: { onUnlock: (pin: string) => return ( - + {title} {[0,1,2,3].map(i => ( @@ -129,7 +130,7 @@ function PinOverlay({ onUnlock, onCancel, title }: { onUnlock: (pin: string) => ))} {onCancel && ( - + Annulla )} @@ -330,7 +331,7 @@ export default function PasswordScreen() { onDelete={() => deleteEntry(item.id)} /> )} - contentContainerStyle={{ padding: 16, paddingBottom: 96 }} + contentContainerStyle={{ padding: SPACING.lg, paddingBottom: 96 }} ListEmptyComponent={ @@ -374,7 +375,7 @@ export default function PasswordScreen() { - {t('passwordNotesLabel')} + {t('passwordNotesLabel')} setModal(m => ({ ...m, notes: v }))} placeholder="es. scade ogni 90 giorni…" placeholderTextColor={colors.textMuted} multiline numberOfLines={3} textAlignVertical="top" /> @@ -397,12 +398,12 @@ export default function PasswordScreen() { function makePinStyles(c: ThemeColors) { return StyleSheet.create({ overlay: { flex: 1, backgroundColor: c.bg, justifyContent: 'center', alignItems: 'center' }, - box: { alignItems: 'center', padding: 32, width: '100%', maxWidth: 320 }, - title: { ...TYPE.subhead, color: c.text, marginBottom: 24 }, - dots: { flexDirection: 'row', gap: 16, marginBottom: 32 }, - dot: { width: 16, height: 16, borderRadius: 8, borderWidth: 2, borderColor: c.primary, backgroundColor: 'transparent' }, + box: { alignItems: 'center', padding: SPACING.xxxl, width: '100%', maxWidth: 320 }, + title: { ...TYPE.subhead, color: c.text, marginBottom: SPACING.xxl }, + dots: { flexDirection: 'row', gap: SPACING.lg, marginBottom: SPACING.xxxl }, + dot: { width: 16, height: 16, borderRadius: RADIUS.sm, borderWidth: 2, borderColor: c.primary, backgroundColor: 'transparent' }, dotFilled: { backgroundColor: c.primary }, - grid: { flexDirection: 'row', flexWrap: 'wrap', width: 240, justifyContent: 'center', gap: 12 }, + grid: { flexDirection: 'row', flexWrap: 'wrap', width: 240, justifyContent: 'center', gap: SPACING.md }, key: { width: 64, height: 64, borderRadius: 32, backgroundColor: c.card, borderWidth: 1, borderColor: c.border, justifyContent: 'center', alignItems: 'center' }, keyEmpty:{ width: 64, height: 64 }, keyText: { fontSize: 22, fontWeight: '600', color: c.text }, @@ -411,15 +412,15 @@ function makePinStyles(c: ThemeColors) { function makeRowStyles(c: ThemeColors) { return StyleSheet.create({ - card: { backgroundColor: c.card, borderRadius: 16, padding: 14, marginBottom: 10, flexDirection: 'row', alignItems: 'flex-start', borderWidth: 1, borderColor: c.glassBorder, shadowColor: c.primary, shadowOpacity: c.isDark ? 0 : 0.08, shadowRadius: 8, elevation: c.isDark ? 0 : 3 }, + card: { backgroundColor: c.card, borderRadius: RADIUS.lg, padding: 14, marginBottom: 10, flexDirection: 'row', alignItems: 'flex-start', borderWidth: 1, borderColor: c.glassBorder, shadowColor: c.primary, shadowOpacity: c.isDark ? 0 : 0.08, shadowRadius: 8, elevation: c.isDark ? 0 : 3 }, cardLeft:{ flex: 1 }, name: { fontSize: 15, fontWeight: '700', color: c.primaryDark, marginBottom: 2 }, - username:{ fontSize: 12, color: c.textSub, marginBottom: 4 }, + username:{ fontSize: 12, color: c.textSub, marginBottom: SPACING.xs }, pwRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 2 }, pw: { fontSize: 13, color: c.text, letterSpacing: 1 }, eyeBtn: { padding: 2 }, - notes: { fontSize: 11, color: c.textMuted, fontStyle: 'italic', marginTop: 4 }, - actions: { flexDirection: 'column', gap: 6, marginLeft: 8 }, + notes: { fontSize: 11, color: c.textMuted, fontStyle: 'italic', marginTop: SPACING.xs }, + actions: { flexDirection: 'column', gap: 6, marginLeft: SPACING.sm }, editBtn: { width: 32, height: 32, borderRadius: 9, backgroundColor: c.primaryLight, justifyContent: 'center', alignItems: 'center' }, delBtn: { width: 32, height: 32, borderRadius: 9, backgroundColor: '#FEF2F2', justifyContent: 'center', alignItems: 'center' }, }); @@ -428,30 +429,30 @@ function makeRowStyles(c: ThemeColors) { function makeStyles(c: ThemeColors) { return StyleSheet.create({ root: { flex: 1, backgroundColor: c.bg }, - toolbar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 12, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border }, - titleRow: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + toolbar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: SPACING.lg, paddingVertical: SPACING.md, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border }, + titleRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, title: { ...TYPE.headline, color: c.primaryDark }, - toolbarActions:{ flexDirection: 'row', alignItems: 'center', gap: 8 }, + toolbarActions:{ flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, iconBtn: { width: 36, height: 36, borderRadius: 10, backgroundColor: c.cardSecondary, justifyContent: 'center', alignItems: 'center' }, iconBtnActive:{ backgroundColor: c.primary }, - addBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: c.primary, borderRadius: 10, paddingHorizontal: 12, paddingVertical: 8 }, + addBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: c.primary, borderRadius: 10, paddingHorizontal: SPACING.md, paddingVertical: SPACING.sm }, addBtnTxt: { color: '#fff', fontWeight: '600', fontSize: 13 }, - empty: { alignItems: 'center', marginTop: 80, gap: 8 }, + empty: { alignItems: 'center', marginTop: 80, gap: SPACING.sm }, emptyTxt: { fontSize: 16, fontWeight: '600', color: c.textSub }, emptySubTxt: { fontSize: 13, color: c.textMuted }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }, modalScrollContent: { flexGrow: 1, justifyContent: 'flex-end' }, - modalBox: { backgroundColor: c.card, borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, paddingBottom: Platform.OS === 'ios' ? 40 : 24, maxHeight: '92%' }, - modalTitle: { ...TYPE.headline, color: c.primaryDark, marginBottom: 20 }, + modalBox: { backgroundColor: c.card, borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: SPACING.xxl, paddingBottom: Platform.OS === 'ios' ? 40 : 24, maxHeight: '92%' }, + modalTitle: { ...TYPE.headline, color: c.primaryDark, marginBottom: SPACING.xl }, label: { ...TYPE.caption, color: c.textSub, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 0.5 }, input: { backgroundColor: c.bg, borderWidth: 1, borderColor: c.border, borderRadius: 10, paddingHorizontal: 14, paddingVertical: 10, fontSize: 15, color: c.text, marginBottom: 14 }, inputMulti: { height: 80, paddingTop: 10 }, - pwInputRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 14 }, + pwInputRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, marginBottom: 14 }, eyeModal: { padding: 10 }, - modalBtns: { flexDirection: 'row', gap: 10, marginTop: 8 }, - cancelBtn: { flex: 1, paddingVertical: 14, borderRadius: 12, backgroundColor: c.bg, alignItems: 'center', borderWidth: 1, borderColor: c.border }, + modalBtns: { flexDirection: 'row', gap: 10, marginTop: SPACING.sm }, + cancelBtn: { flex: 1, paddingVertical: 14, borderRadius: RADIUS.md, backgroundColor: c.bg, alignItems: 'center', borderWidth: 1, borderColor: c.border }, cancelTxt: { fontSize: 15, fontWeight: '600', color: c.textSub }, - saveBtn: { flex: 1, paddingVertical: 14, borderRadius: 12, backgroundColor: c.primary, alignItems: 'center' }, + saveBtn: { flex: 1, paddingVertical: 14, borderRadius: RADIUS.md, backgroundColor: c.primary, alignItems: 'center' }, saveTxt: { fontSize: 15, fontWeight: '700', color: '#fff' }, }); } diff --git a/src/screens/PhonebookScreen.tsx b/src/screens/PhonebookScreen.tsx index a98034fd..694fdb2e 100644 --- a/src/screens/PhonebookScreen.tsx +++ b/src/screens/PhonebookScreen.tsx @@ -8,6 +8,7 @@ import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; import { TYPE } from '../theme/typography'; import { useLanguage } from '../context/LanguageContext'; +import { SPACING, RADIUS } from '../theme/spacing'; const STORAGE_KEY = 'aerostaff_phonebook_v1'; @@ -52,37 +53,37 @@ function makeModalStyles(c: ThemeColors) { sheet: { backgroundColor: c.card, borderTopLeftRadius: 24, borderTopRightRadius: 24, - padding: 20, paddingBottom: 36, maxHeight: '92%', + padding: SPACING.xl, paddingBottom: 36, maxHeight: '92%', }, handle: { width: 40, height: 4, borderRadius: 2, backgroundColor: c.border, alignSelf: 'center', marginBottom: 18, }, - title: { ...TYPE.headline, color: c.primaryDark, marginBottom: 16 }, - label: { ...TYPE.caption, color: c.textSub, marginBottom: 6, marginTop: 12 }, + title: { ...TYPE.headline, color: c.primaryDark, marginBottom: SPACING.lg }, + label: { ...TYPE.caption, color: c.textSub, marginBottom: 6, marginTop: SPACING.md }, input: { - borderWidth: 1.5, borderColor: c.border, borderRadius: 12, + borderWidth: 1.5, borderColor: c.border, borderRadius: RADIUS.md, paddingHorizontal: 14, paddingVertical: 11, fontSize: 15, color: c.text, backgroundColor: c.cardSecondary, }, - catRow: { marginBottom: 4 }, + catRow: { marginBottom: SPACING.xs }, catChip: { paddingHorizontal: 14, paddingVertical: 7, - borderRadius: 20, borderWidth: 1.5, borderColor: c.border, - marginRight: 8, backgroundColor: c.card, + borderRadius: RADIUS.xl, borderWidth: 1.5, borderColor: c.border, + marginRight: SPACING.sm, backgroundColor: c.card, }, catTxt: { ...TYPE.caption, color: c.textSub }, - actions: { flexDirection: 'row', gap: 10, marginTop: 20 }, + actions: { flexDirection: 'row', gap: 10, marginTop: SPACING.xl }, cancelBtn: { flex: 1, borderWidth: 1.5, borderColor: c.border, - borderRadius: 12, paddingVertical: 13, alignItems: 'center', + borderRadius: RADIUS.md, paddingVertical: 13, alignItems: 'center', }, cancelTxt: { fontSize: 14, fontWeight: '600', color: c.textSub }, saveBtn: { flex: 2, backgroundColor: c.primary, - borderRadius: 12, paddingVertical: 13, - flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, + borderRadius: RADIUS.md, paddingVertical: 13, + flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: SPACING.sm, }, saveTxt: { fontSize: 14, fontWeight: '700', color: '#fff' }, }); @@ -217,19 +218,19 @@ function makeRowStyles(c: ThemeColors) { shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: c.isDark ? 0 : 0.05, shadowRadius: 4, elevation: c.isDark ? 0 : 2, borderWidth: c.isDark ? 1 : 0, borderColor: c.border, }, - dot: { width: 4, borderRadius: 2, alignSelf: 'stretch', marginRight: 12 }, + dot: { width: 4, borderRadius: 2, alignSelf: 'stretch', marginRight: SPACING.md }, info: { flex: 1 }, - topRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 3 }, + topRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, marginBottom: 3 }, name: { fontSize: 14, fontWeight: '700', color: c.text, flex: 1 }, badge: { - paddingHorizontal: 8, paddingVertical: 2, borderRadius: 10, + paddingHorizontal: SPACING.sm, paddingVertical: 2, borderRadius: 10, }, badgeTxt: { fontSize: 10, fontWeight: '700' }, number: { fontSize: 13, color: c.textSub, fontWeight: '500' }, note: { fontSize: 11, color: c.textMuted, marginTop: 2 }, callBtn: { width: 36, height: 36, borderRadius: 18, - justifyContent: 'center', alignItems: 'center', marginLeft: 8, + justifyContent: 'center', alignItems: 'center', marginLeft: SPACING.sm, }, editBtn: { padding: 6, marginLeft: 2 }, }); @@ -297,7 +298,7 @@ function makeStyles(c: ThemeColors) { root: { flex: 1, backgroundColor: c.bg }, header: { flexDirection: 'row', alignItems: 'center', gap: 10, - paddingHorizontal: 16, paddingVertical: 12, + paddingHorizontal: SPACING.lg, paddingVertical: SPACING.md, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border, }, @@ -305,31 +306,31 @@ function makeStyles(c: ThemeColors) { addBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: c.primary, borderRadius: 10, - paddingHorizontal: 12, paddingVertical: 8, + paddingHorizontal: SPACING.md, paddingVertical: SPACING.sm, }, addTxt: { color: '#fff', fontWeight: '700', fontSize: 13 }, searchRow: { flexDirection: 'row', alignItems: 'center', gap: 10, - margin: 12, paddingHorizontal: 14, paddingVertical: 10, + margin: SPACING.md, paddingHorizontal: 14, paddingVertical: 10, backgroundColor: c.card, borderRadius: 14, borderWidth: 1.5, borderColor: c.border, }, searchInput: { flex: 1, fontSize: 14, color: c.text }, filterBar: { maxHeight: 50, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border }, - filterContent: { paddingHorizontal: 12, paddingVertical: 9, gap: 8 }, + filterContent: { paddingHorizontal: SPACING.md, paddingVertical: 9, gap: SPACING.sm }, filterChip: { paddingHorizontal: 14, paddingVertical: 6, - borderRadius: 20, borderWidth: 1.5, borderColor: c.border, + borderRadius: RADIUS.xl, borderWidth: 1.5, borderColor: c.border, backgroundColor: c.card, }, filterChipActive: { backgroundColor: c.primary, borderColor: c.primary }, filterTxt: { ...TYPE.caption, color: c.textSub }, filterTxtActive: { color: '#fff' }, list: { flex: 1 }, - listPad: { padding: 16, paddingBottom: 96 }, + listPad: { padding: SPACING.lg, paddingBottom: 96 }, groupHeader: { - flexDirection: 'row', alignItems: 'center', gap: 8, - marginBottom: 8, marginTop: 6, + flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, + marginBottom: SPACING.sm, marginTop: 6, }, groupDot: { width: 8, height: 8, borderRadius: 4 }, groupLabel: { fontSize: 11, fontWeight: '700', color: c.textSub, letterSpacing: 0.8, flex: 1 }, diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 0334f2a5..9577ece4 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -46,6 +46,7 @@ import { type FlightProviderDiagnosticsSnapshot, } from '../utils/fr24api'; import { formatProviderDiagnostic } from '../utils/flightDiagnostics'; +import { SPACING, RADIUS } from '../theme/spacing'; import { getStaffMonitorDebugColumns, getStaffMonitorDebugFlights, @@ -679,9 +680,9 @@ export default function SettingsScreen({ {t('sectionTheme')} {isLoading ? ( - + - + {t('themeLoading')} @@ -1425,17 +1426,17 @@ export default function SettingsScreen({ )} - + {t('flightDebugStaffMonitorParser')} {staffMonitorDebug || t('flightDebugNoStaffMonitorDebug')} - + {t('flightDebugStaffMonitorRawArrivals')} - + {t('flightDebugStaffMonitorRawArrivalsSub')} - + {t('notificationDebugDuplicateList')} @@ -1543,7 +1544,7 @@ export default function SettingsScreen({ : t('notificationDebugNoDuplicates')} - + {t('notificationDebugLastEvents')} @@ -1732,17 +1733,17 @@ export default function SettingsScreen({ )} - + {t('flightDebugStaffMonitorParser')} {staffMonitorDebug || t('flightDebugNoStaffMonitorDebug')} - + {t('flightDebugStaffMonitorRawArrivals')} - + {t('flightDebugStaffMonitorRawArrivalsSub')} - + Caricamento lento o errore di rete. Date: Fri, 3 Jul 2026 11:46:23 +0000 Subject: [PATCH 08/10] fix(widget): self-heal stale shift snapshot from the system calendar The widget's shift snapshot (WIDGET_SHIFT_KEY) is written by the app and only covers today and tomorrow. If the app was not opened for more than a day, every periodic widget update resolved to no_shift/error until the next app launch - the widget appeared dead. The widget task now detects a stale snapshot (date != today), re-reads today's and tomorrow's Lavoro/Riposo events straight from the system calendar (permission check only, never a prompt) and rewrites the snapshot, falling back to the old behavior on any error. Covered by a regression test with mocked calendar/storage; the test loader now transpiles JSX so widget modules can be tested. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012vEkzMVahsn6d7vyL4xayt --- scripts/test-misc-utils.cjs | 77 ++++++++++++++++++++++++++++++ src/widgets/widgetTaskHandler.tsx | 78 ++++++++++++++++++++++++++++--- 2 files changed, 149 insertions(+), 6 deletions(-) diff --git a/scripts/test-misc-utils.cjs b/scripts/test-misc-utils.cjs index 2312085d..b9341c46 100644 --- a/scripts/test-misc-utils.cjs +++ b/scripts/test-misc-utils.cjs @@ -20,6 +20,7 @@ function loadTsModule(relativePath, mocks = {}) { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true, + jsx: ts.JsxEmit.React, }, }).outputText; const module = { exports: {} }; @@ -654,8 +655,84 @@ async function testRuntimeDiagnostics() { } } + +async function testWidgetShiftSelfHeal() { + // Regressione: snapshot turni di ieri + turno di oggi presente nel + // calendario di sistema -> il widget deve auto-rigenerare lo snapshot + // invece di mostrare "nessun turno" finche' l'app non viene aperta. + const toIso = date => { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; + }; + const today = new Date(); today.setHours(0, 0, 0, 0); + const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); + // Turno 00:00-23:59 così il test non dipende dall'ora in cui gira. + const shiftStart = new Date(today); shiftStart.setHours(0, 0, 0, 0); + const shiftEnd = new Date(today); shiftEnd.setHours(23, 59, 0, 0); + + const store = new Map(); + store.set('widget_shift_v1', JSON.stringify({ + date: toIso(yesterday), + shiftToday: null, + isRestDay: false, + nextShift: null, + })); + + const asyncStorageMock = { + getItem: async key => (store.has(key) ? store.get(key) : null), + setItem: async (key, value) => { store.set(key, value); }, + multiSet: async pairs => { for (const [k, v] of pairs) store.set(k, v); }, + }; + const calendarMock = { + EntityTypes: { EVENT: 'event' }, + getCalendarPermissionsAsync: async () => ({ status: 'granted' }), + getCalendarsAsync: async () => [{ id: 'cal1', allowsModifications: true, isPrimary: true }], + getEventsAsync: async () => [ + { id: 'e1', title: 'Lavoro', startDate: shiftStart.toISOString(), endDate: shiftEnd.toISOString() }, + ], + }; + + const handler = loadTsModule('src/widgets/widgetTaskHandler.tsx', { + '@react-native-async-storage/async-storage': asyncStorageMock, + 'expo-calendar': calendarMock, + 'react-native-android-widget': {}, + './ShiftWidget': { ShiftWidget: () => null }, + './widgetTheme': { getStoredWidgetThemeProps: async () => ({ themeMode: 'light', themeSnapshot: undefined }) }, + '../utils/liveArrivalEta': { applyLiveDepartureStatus: (deps) => deps, fetchAdsbAircraft: async () => [] }, + '../utils/flightProviders/staffMonitorProvider': { staffMonitorProvider: { supports: () => false, fetch: async () => ({ allDepartures: [] }) } }, + react: require('react'), + }); + + const data = await handler.getWidgetData(); + assert( + data.state === 'work' || data.state === 'work_empty', + `stale shift snapshot should self-heal from the calendar, got state=${data.state}`, + ); + + const rewritten = JSON.parse(store.get('widget_shift_v1')); + assert(rewritten.date === toIso(today), 'the shift snapshot should be rewritten with today\'s date'); + assert(rewritten.shiftToday && typeof rewritten.shiftToday.start === 'number', 'the rewritten snapshot should contain today\'s shift window'); + + // Permesso calendario negato -> nessun crash e fallback allo snapshot esistente. + const handlerDenied = loadTsModule('src/widgets/widgetTaskHandler.tsx', { + '@react-native-async-storage/async-storage': asyncStorageMock, + 'expo-calendar': { ...calendarMock, getCalendarPermissionsAsync: async () => ({ status: 'denied' }) }, + 'react-native-android-widget': {}, + './ShiftWidget': { ShiftWidget: () => null }, + './widgetTheme': { getStoredWidgetThemeProps: async () => ({ themeMode: 'light', themeSnapshot: undefined }) }, + '../utils/liveArrivalEta': { applyLiveDepartureStatus: (deps) => deps, fetchAdsbAircraft: async () => [] }, + '../utils/flightProviders/staffMonitorProvider': { staffMonitorProvider: { supports: () => false, fetch: async () => ({ allDepartures: [] }) } }, + react: require('react'), + }); + const denied = await handlerDenied.getWidgetData(); + assert(typeof denied.state === 'string', 'denied calendar permission should still return a widget state'); +} + async function main() { await testDateFormat(); + await testWidgetShiftSelfHeal(); await testThemeMode(); await testSecureWipe(); await testFlightProviderSettings(); diff --git a/src/widgets/widgetTaskHandler.tsx b/src/widgets/widgetTaskHandler.tsx index 129dda9c..7a138172 100644 --- a/src/widgets/widgetTaskHandler.tsx +++ b/src/widgets/widgetTaskHandler.tsx @@ -1,5 +1,6 @@ import React from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import * as Calendar from 'expo-calendar'; import type { WidgetTaskHandlerProps } from 'react-native-android-widget'; import type { HexColor } from '../utils/airlineOps'; import { getAirlineOps, getAirlineColor } from '../utils/airlineOps'; @@ -104,13 +105,79 @@ function resolveWidgetShift(shiftData: WidgetShiftData): ResolvedWidgetShift | ' return null; } +/*---------------------------------------------------------------------------*\ +| Lo snapshot turni (WIDGET_SHIFT_KEY) è scritto dall'app e copre solo oggi e | +| domani: se l'app non viene aperta per più di un giorno, il widget restava | +| su "nessun turno" fino alla successiva apertura. Qui il task del widget | +| rilegge da solo i turni dal calendario di sistema (stessa logica delle | +| schermate) e riscrive lo snapshot. Solo verifica del permesso, mai prompt: | +| in un contesto headless non c'è UI. In caso di errore si torna al vecchio | +| comportamento basato sullo snapshot esistente. | +\*---------------------------------------------------------------------------*/ +async function refreshShiftSnapshotFromCalendar(): Promise { + try { + const { status } = await Calendar.getCalendarPermissionsAsync(); + if (status !== 'granted') return null; + + const cals = await Calendar.getCalendarsAsync(Calendar.EntityTypes.EVENT); + const cal = cals.find(c => c.allowsModifications && c.isPrimary) || cals.find(c => c.allowsModifications); + if (!cal) return null; + + const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); + const todayEnd = new Date(todayStart); todayEnd.setHours(23, 59, 59, 999); + const tomorrowStart = addDays(todayStart, 1); + const tomorrowEnd = new Date(tomorrowStart); tomorrowEnd.setHours(23, 59, 59, 999); + + let shiftToday: { start: number; end: number } | null = null; + let shiftTomorrow: { start: number; end: number } | null = null; + let isRestDay = false; + const events = await Calendar.getEventsAsync([cal.id], todayStart, tomorrowEnd); + for (const e of events) { + if (e.title.includes('Riposo')) { + const evtDay = new Date(e.startDate); + if (evtDay >= todayStart && evtDay <= todayEnd) isRestDay = true; + continue; + } + if (!e.title.includes('Lavoro')) continue; + const start = new Date(e.startDate).getTime() / 1000; + const end = new Date(e.endDate).getTime() / 1000; + const evtDay = new Date(e.startDate); + if (evtDay >= todayStart && evtDay <= todayEnd) { + shiftToday = { start, end }; + isRestDay = false; + } else if (evtDay >= tomorrowStart && evtDay <= tomorrowEnd) { + shiftTomorrow = { start, end }; + } + } + + const snapshot: WidgetShiftData = { + date: toLocalIso(todayStart), + shiftToday, + isRestDay, + nextShift: shiftTomorrow ? { date: toLocalIso(tomorrowStart), ...shiftTomorrow } : null, + }; + await AsyncStorage.setItem(WIDGET_SHIFT_KEY, JSON.stringify(snapshot)); + return snapshot; + } catch { + return null; + } +} + +async function readShiftSnapshot(): Promise { + const shiftRaw = await AsyncStorage.getItem(WIDGET_SHIFT_KEY); + let shiftData: WidgetShiftData | null = shiftRaw ? JSON.parse(shiftRaw) : null; + if (!shiftData || shiftData.date !== toLocalIso()) { + shiftData = (await refreshShiftSnapshotFromCalendar()) ?? shiftData; + } + return shiftData; +} + // ─── Read cached data written by the main app ────────────────────────────────── export async function getWidgetData(): Promise { try { - const shiftRaw = await AsyncStorage.getItem(WIDGET_SHIFT_KEY); + const shiftData = await readShiftSnapshot(); - if (shiftRaw) { - const shiftData: WidgetShiftData = JSON.parse(shiftRaw); + if (shiftData) { const resolved = resolveWidgetShift(shiftData); if (resolved === 'rest') return { state: 'rest' }; if (resolved) { @@ -153,10 +220,9 @@ async function renderThemedWidget(props: WidgetTaskHandlerProps, data: WidgetDat // ─── Fetch fresh widget data from the live provider + cached shift key ──────── export async function fetchFreshWidgetData(): Promise { try { - const shiftRaw = await AsyncStorage.getItem(WIDGET_SHIFT_KEY); - if (!shiftRaw) return getWidgetData(); + const shiftData = await readShiftSnapshot(); + if (!shiftData) return getWidgetData(); - const shiftData: WidgetShiftData = JSON.parse(shiftRaw); const resolved = resolveWidgetShift(shiftData); if (resolved === 'rest') return { state: 'rest' }; if (!resolved) return { state: 'no_shift' }; From 0acf9d6b72d9f38f875c9508bf4364efd1cd3249 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 11:46:23 +0000 Subject: [PATCH 09/10] feat(calendar): navigate weeks in the week view The week view was locked to the week of the selected day. Add prev/next week arrows around the range label; navigation moves the selected day by 7 days and re-anchors the visible month so events for the new range load automatically. Localized a11y labels included. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012vEkzMVahsn6d7vyL4xayt --- src/i18n/translations.ts | 2 ++ src/screens/CalendarScreen.tsx | 41 +++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index 85e56690..6238088b 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -147,6 +147,7 @@ const it = { // Calendar calTitle: 'Gestione Turni', calEditBtn: 'Modifica Turni', calModeCalendar: 'Calendario', calModeWeek: 'Settimana', calModeMonthHours: 'Ore mese', + calPrevWeek: 'Settimana precedente', calNextWeek: 'Settimana successiva', calWeekShiftsCount: 'Turni lavoro: {count}', calMonthTotalHours: 'Totale ore del mese', calMonthShiftsCount: 'Turni lavoro: {count}', calToday: 'Oggi', calWeatherLocal: 'Meteo locale', calShiftWork: 'Turno Lavoro', @@ -440,6 +441,7 @@ const en: typeof it = { // Calendar calTitle: 'Shift Manager', calEditBtn: 'Edit Shifts', calModeCalendar: 'Calendar', calModeWeek: 'Week', calModeMonthHours: 'Month hours', + calPrevWeek: 'Previous week', calNextWeek: 'Next week', calWeekShiftsCount: 'Work shifts: {count}', calMonthTotalHours: 'Total month hours', calMonthShiftsCount: 'Work shifts: {count}', calToday: 'Today', calWeatherLocal: 'Local weather', calShiftWork: 'Work Shift', diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index 366a614b..65511382 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -649,6 +649,18 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole } }; + // Sposta la vista settimana di ±1 settimana; aggiornare visibleMonth quando + // si cambia mese fa ricaricare gli eventi del nuovo intervallo (mese ±7gg). + const goToWeek = (delta: number) => { + const next = fromIsoDate(selectedDay); + next.setDate(next.getDate() + delta * 7); + const iso = toLocalIso(next); + setSelectedDay(iso); + if (!isSameMonth(visibleMonth, iso)) { + setVisibleMonth(new Date(next.getFullYear(), next.getMonth(), 1)); + } + }; + const handleMonthChange = (day: DateData) => { const nextMonth = new Date(day.year, day.month - 1, 1); setVisibleMonth(nextMonth); @@ -875,7 +887,6 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole {t('calModeWeek')} - {weekRangeLabel} {weekHoursSummary.totalHours.toFixed(1)} h @@ -885,6 +896,28 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole + + goToWeek(-1)} + activeOpacity={0.8} + accessibilityRole="button" + accessibilityLabel={t('calPrevWeek')} + > + + + {weekRangeLabel} + goToWeek(1)} + activeOpacity={0.8} + accessibilityRole="button" + accessibilityLabel={t('calNextWeek')} + > + + + + {selectedWeekDays.map(day => { const selected = day.iso === selectedDay; const dayName = weekDaysShort[day.date.getDay()]; @@ -1271,9 +1304,11 @@ function makeStyles(c: ThemeColors) { borderWidth: c.isDark ? 1 : 0, borderColor: c.glassBorder, }, - weekHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: SPACING.md, marginBottom: 14 }, + weekHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: SPACING.md, marginBottom: SPACING.sm }, weekTitle: { color: c.primaryDark, fontSize: 20, fontWeight: '900' }, - weekRange: { color: c.textSub, fontSize: 12, fontWeight: '700', marginTop: 3, textTransform: 'uppercase' }, + weekNavRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, marginBottom: 14 }, + weekNavBtn: { width: 40, height: 36, borderRadius: RADIUS.md, alignItems: 'center', justifyContent: 'center', backgroundColor: c.primaryLight }, + weekNavLabel: { flex: 1, textAlign: 'center', color: c.textSub, fontSize: 12, fontWeight: '700', textTransform: 'uppercase' }, weekTotalPill: { backgroundColor: c.primaryLight, borderRadius: 14, paddingHorizontal: SPACING.md, paddingVertical: SPACING.sm, alignItems: 'flex-end' }, weekTotalValue: { color: c.primaryText, fontSize: 18, fontWeight: '900' }, weekTotalLabel: { ...TYPE.micro, color: c.primaryDark, marginTop: 1 }, From 72cdc3324e343fc21e4f442eafcb6dd02498d73b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 11:49:32 +0000 Subject: [PATCH 10/10] chore: release 2.7.29 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012vEkzMVahsn6d7vyL4xayt --- README.md | 2 +- android/app/build.gradle | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/utils/updateChecker.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8bd0f0ac..97f54238 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ npm run github:branches:audit APK files are published in [GitHub Releases](https://github.com/TargetMisser/AeroStaffPro/releases). -Latest stable release: **v2.7.28** +Latest stable release: **v2.7.29** To install the Android app: diff --git a/android/app/build.gradle b/android/app/build.gradle index 3608a19e..1116e92d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -105,7 +105,7 @@ android { applicationId 'com.aerostaffpro.app' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 105 + versionCode 106 versionName "2.7.29" buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" diff --git a/package-lock.json b/package-lock.json index 72190a70..6f57e250 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aerostaff-pro", - "version": "2.7.28", + "version": "2.7.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aerostaff-pro", - "version": "2.7.28", + "version": "2.7.29", "dependencies": { "@expo/metro-runtime": "~6.1.2", "@expo/vector-icons": "^15.0.3", diff --git a/package.json b/package.json index 04b56288..bdd7ba1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aerostaff-pro", - "version": "2.7.28", + "version": "2.7.29", "main": "index.ts", "scripts": { "start": "expo start", diff --git a/src/utils/updateChecker.ts b/src/utils/updateChecker.ts index 90e63739..71485a6d 100644 --- a/src/utils/updateChecker.ts +++ b/src/utils/updateChecker.ts @@ -6,7 +6,7 @@ import { nativeApplicationVersion } from 'expo-application'; * (web, tests). Kept in sync with package.json by scripts/bump-version.cjs * and enforced by scripts/release-check.cjs — do not edit by hand. */ -export const FALLBACK_APP_VERSION = '2.7.28'; +export const FALLBACK_APP_VERSION = '2.7.29'; export const APP_VERSION = nativeApplicationVersion ?? FALLBACK_APP_VERSION; const REPO = 'targetmisser/aerostaffpro';