From dab1add8a2f9bf7f2d6ae1e5d07162f59f1e68b2 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Fri, 13 Nov 2026 07:55:10 +0200 Subject: [PATCH] =?UTF-8?q?fix(mobile):=20autentificare=20=C8=99i=20onboar?= =?UTF-8?q?ding=20fideli=20manualului=20=C3=8Endr=C4=83zne=C8=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - titlurile Bricolage (Landing, Login, CreateUser, Onboarding, stări de sistem, antetele de pagină) au interlinia strânsă din manual și spațiu pentru diacritice (Î, Ș, ț), fără tăieturi pe Android/iOS - antetul paginilor trece pe două rânduri pentru titluri lungi („Completează profil voluntar”) în loc să le taie - conținutul paginilor și modalelor folosește gutter-ul de 18 pt, aliniat cu antetul - căsuțele codului: 56 pt ca un câmp, halou galben de 5 pt - butonul galben are muchia de jos mai închisă din manual, care se strânge la apăsare - butonul contur de pe navy are conturul alb plin - select-ul și data afișează valorile lungi pe două rânduri, nu cu „…” - rândurile „Trimite un cod nou / Alt email” și „sau scrie codul” se rup pe rânduri la text mare - scenele din prezentare se micșorează și pe lățime (320 pt), au colțurile cardurilor din manual, pastila de status nu mai e tăiată, iar la Reduce Motion intră cu fade --- mobile/src/components/SystemState.tsx | 3 +- mobile/src/components/auth/CodeInput.tsx | 24 +++++----- mobile/src/components/auth/SocialButton.tsx | 3 +- .../src/components/onboarding/StoryScenes.tsx | 46 +++++++++++++++---- mobile/src/design/components/Button.tsx | 18 ++++++++ mobile/src/design/components/DateField.tsx | 2 +- mobile/src/design/components/Select.tsx | 2 +- mobile/src/design/components/TextField.tsx | 10 +++- mobile/src/layouts/LayoutHeader.tsx | 7 ++- mobile/src/layouts/ModalLayout.tsx | 2 +- mobile/src/layouts/PageLayout.tsx | 6 ++- mobile/src/layouts/displayTitle.ts | 26 +++++++++++ mobile/src/screens/CreateUser.tsx | 6 +-- mobile/src/screens/Landing.tsx | 6 ++- mobile/src/screens/Login.tsx | 17 +++++-- mobile/src/screens/Onboarding.tsx | 23 +++++++--- 16 files changed, 158 insertions(+), 43 deletions(-) create mode 100644 mobile/src/layouts/displayTitle.ts diff --git a/mobile/src/components/SystemState.tsx b/mobile/src/components/SystemState.tsx index 28da7dc8..68a07345 100644 --- a/mobile/src/components/SystemState.tsx +++ b/mobile/src/components/SystemState.tsx @@ -11,6 +11,7 @@ import { AppText } from '../design/components/Text'; import { Button } from '../design/components/Button'; import { enter } from '../design/motion'; import { palette } from '../design/tokens'; +import { displayTitle } from '../layouts/displayTitle'; export interface SystemStateProps { /** small line above the title, e.g. "VIC" */ @@ -88,7 +89,7 @@ const styles = StyleSheet.create((theme) => ({ }, body: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 24 }, copy: { alignItems: 'center', gap: 10 }, - title: { fontSize: 38, lineHeight: 40, textAlign: 'center' }, + title: { ...displayTitle(38), textAlign: 'center' }, description: { maxWidth: 320 }, center: { textAlign: 'center' }, })); diff --git a/mobile/src/components/auth/CodeInput.tsx b/mobile/src/components/auth/CodeInput.tsx index a71d99f3..8284ef16 100644 --- a/mobile/src/components/auth/CodeInput.tsx +++ b/mobile/src/components/auth/CodeInput.tsx @@ -38,9 +38,7 @@ const CodeBox = ({ return ( - + {digit ?? ''} @@ -114,7 +112,10 @@ CodeInput.displayName = 'CodeInput'; export default CodeInput; -const BOX_HEIGHT = 60; +// the field height of the handbook; the focus halo is the field's 5pt one +const BOX_HEIGHT = 56; +const HALO = 5; +const BOX_RADIUS = 14; const styles = StyleSheet.create((theme) => ({ row: { flexDirection: 'row', justifyContent: 'center', gap: 8 }, @@ -122,28 +123,29 @@ const styles = StyleSheet.create((theme) => ({ cell: { flex: 1, maxWidth: 56 }, halo: { position: 'absolute', - top: -4, - left: -4, - right: -4, - bottom: -4, - borderRadius: 18, + top: -HALO, + left: -HALO, + right: -HALO, + bottom: -HALO, + borderRadius: BOX_RADIUS + HALO, backgroundColor: theme.colors.yellow, }, box: { height: BOX_HEIGHT, - borderRadius: 14, + borderRadius: BOX_RADIUS, borderWidth: 2, borderColor: 'transparent', backgroundColor: theme.colors.surface, alignItems: 'center', justifyContent: 'center', }, - filled: { backgroundColor: theme.colors.card }, error: { backgroundColor: theme.colors.errorTint }, digit: { fontFamily: theme.fontFamily.display, fontSize: 28, lineHeight: 34, + letterSpacing: -0.7, + includeFontPadding: false, color: theme.colors.ink, fontVariant: ['tabular-nums'], }, diff --git a/mobile/src/components/auth/SocialButton.tsx b/mobile/src/components/auth/SocialButton.tsx index f5cfc837..445d2506 100644 --- a/mobile/src/components/auth/SocialButton.tsx +++ b/mobile/src/components/auth/SocialButton.tsx @@ -117,7 +117,8 @@ const styles = StyleSheet.create((theme) => ({ paddingHorizontal: 18, }, light: { backgroundColor: theme.colors.card }, - outline: { borderWidth: 2, borderColor: 'rgba(255,255,255,0.7)' }, + // the handbook's outline button (2pt, full color), in white on navy + outline: { borderWidth: 2, borderColor: theme.colors.white }, disabled: { opacity: 0.45 }, icon: { width: 24, alignItems: 'center' }, label: { flex: 1, textAlign: 'center' }, diff --git a/mobile/src/components/onboarding/StoryScenes.tsx b/mobile/src/components/onboarding/StoryScenes.tsx index 3a937aa0..f8379412 100644 --- a/mobile/src/components/onboarding/StoryScenes.tsx +++ b/mobile/src/components/onboarding/StoryScenes.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import { StyleSheet, View } from 'react-native'; import Svg, { Path } from 'react-native-svg'; import Animated, { + FadeIn, useAnimatedStyle, useReducedMotion, useSharedValue, @@ -11,9 +12,16 @@ import Animated, { withTiming, } from 'react-native-reanimated'; import { useTranslation } from 'react-i18next'; -import { VText } from '../ui/VText'; +import { VText as BaseText, VTextProps } from '../ui/VText'; import { Badge } from '../ui/Badge'; import { colors, fonts, motion } from '../../common/theme/tokens'; +import { radius as dsRadius } from '../../design/tokens'; +import { duration } from '../../design/motion'; + +// The scenes are pictures: the stage is one button with its own label, so their sample text is not +// read and does not grow with Dynamic Type (it would overflow the fixed cards); the story title +// under the stage grows instead. +const VText = (props: VTextProps) => ; const EASE = motion.enter.easing; @@ -44,7 +52,10 @@ const Fly = ({ }) => { const reducedMotion = useReducedMotion(); return ( - + {children} ); @@ -268,8 +279,16 @@ const HoursScene = () => { } - to={} + from={ + + + + } + to={ + + + + } /> @@ -321,7 +340,7 @@ export const StoryScenes = [OrganizationsScene, EventScene, HoursScene, Document const styles = StyleSheet.create({ mini: { backgroundColor: colors.white, - borderRadius: 16, + borderRadius: dsRadius.row, shadowColor: '#000', shadowOpacity: 0.35, shadowRadius: 20, @@ -355,8 +374,8 @@ const styles = StyleSheet.create({ cover: { height: 150, backgroundColor: colors.teal, - borderTopLeftRadius: 16, - borderTopRightRadius: 16, + borderTopLeftRadius: dsRadius.row, + borderTopRightRadius: dsRadius.row, }, coverBadge: { position: 'absolute', @@ -375,11 +394,20 @@ const styles = StyleSheet.create({ sceneHours: { width: 290, gap: 12 }, hoursCard: { padding: 16, gap: 10 }, hoursTotal: { flexDirection: 'row', alignItems: 'baseline', gap: 6 }, - bigNumber: { fontFamily: fonts.title, fontSize: 34, lineHeight: 38, color: colors.ink }, + bigNumber: { + fontFamily: fonts.title, + fontSize: 34, + lineHeight: 38, + letterSpacing: -1.36, + fontVariant: ['tabular-nums'], + color: colors.ink, + }, bars: { flexDirection: 'row', alignItems: 'flex-end', gap: 8, height: 80 }, barSlot: { flex: 1, height: '100%', justifyContent: 'flex-end' }, bar: { width: '100%', borderTopLeftRadius: 6, borderTopRightRadius: 6, borderRadius: 3 }, - swapBadge: { width: 104, height: 24, alignItems: 'flex-end' }, + // the status pills are 28pt; both sit at the right edge of the row + swapBadge: { width: 124, height: 28 }, + badgeEnd: { flex: 1, alignItems: 'flex-end', justifyContent: 'center' }, sceneDocs: { width: 250, height: 300 }, paper: { position: 'absolute', padding: 18, gap: 8 }, paperBack: { diff --git a/mobile/src/design/components/Button.tsx b/mobile/src/design/components/Button.tsx index b585788e..3db3a4b3 100644 --- a/mobile/src/design/components/Button.tsx +++ b/mobile/src/design/components/Button.tsx @@ -120,6 +120,9 @@ export const Button = ({ : { transform: [{ scale }] }; }); const pressFill = useAnimatedStyle(() => ({ opacity: pressed.value })); + // the yellow button's darker bottom edge (inset 0 -3 navy 12%) tightens to 1pt when pressed + const edgeRest = useAnimatedStyle(() => ({ opacity: 1 - pressed.value })); + const edgePressed = useAnimatedStyle(() => ({ opacity: pressed.value })); // a 34pt button still has a 44pt target const slop = hitSlop ?? Math.max(0, Math.ceil((hit.min - dims.height) / 2)); @@ -164,6 +167,18 @@ export const Button = ({ pressFill, ]} /> + {variant === 'primary' && ( + <> + + + + )} {/* the label stays laid out (invisible) while loading, so the width does not jump */} ({ content: { flexShrink: 1, flexDirection: 'row', alignItems: 'center', gap: theme.space.xs }, hidden: { opacity: 0 }, bordered: { borderWidth: 2 }, + edge: { ...StyleSheet.absoluteFillObject, borderColor: 'rgba(1,52,79,0.12)' }, + edgeRest: { borderBottomWidth: 3 }, + edgePressed: { borderBottomWidth: 1 }, label: { flexShrink: 1, textAlign: 'center' }, spinner: { ...StyleSheet.absoluteFillObject, diff --git a/mobile/src/design/components/DateField.tsx b/mobile/src/design/components/DateField.tsx index 99fd22b7..9e707a18 100644 --- a/mobile/src/design/components/DateField.tsx +++ b/mobile/src/design/components/DateField.tsx @@ -164,7 +164,7 @@ export const DateField = ({ > ({ ]} > diff --git a/mobile/src/design/components/TextField.tsx b/mobile/src/design/components/TextField.tsx index 1a625fe0..753a6533 100644 --- a/mobile/src/design/components/TextField.tsx +++ b/mobile/src/design/components/TextField.tsx @@ -317,7 +317,15 @@ export const fieldStyles = StyleSheet.create((theme) => ({ }, inputMultiline: { minHeight: 60, textAlignVertical: 'top' }, inputDisabled: { color: theme.colors.textSecondary }, - value: { flex: 1, fontFamily: theme.fontFamily.textBold, fontSize: 15.5, lineHeight: 21 }, + // select / date value: wraps to a second line for long names, with room above and below + value: { + flex: 1, + minWidth: 0, + paddingVertical: 6, + fontFamily: theme.fontFamily.textBold, + fontSize: 15.5, + lineHeight: 21, + }, placeholder: { fontFamily: theme.fontFamily.text }, errorFold: { overflow: 'hidden' }, errorRow: { diff --git a/mobile/src/layouts/LayoutHeader.tsx b/mobile/src/layouts/LayoutHeader.tsx index 14e8ab05..a2e1556f 100644 --- a/mobile/src/layouts/LayoutHeader.tsx +++ b/mobile/src/layouts/LayoutHeader.tsx @@ -12,6 +12,7 @@ import { AppText } from '../design/components/Text'; import { springTo } from '../design/motion'; import { ChevronLeftIcon, CloseIcon, EditIcon } from '../design/icons'; import { hit, palette } from '../design/tokens'; +import { displayTitle } from './displayTitle'; export interface RoundIconButtonProps { icon: 'back' | 'close'; @@ -80,6 +81,7 @@ export const LayoutHeader = ({ }: LayoutHeaderProps) => ( {leading} + {/* long titles („Completează profil voluntar”) wrap to a second line instead of being cut */} ({ justifyContent: 'center', }, roundDark: { backgroundColor: 'rgba(255,255,255,0.14)' }, - title: { flex: 1, fontSize: 22, lineHeight: 26, letterSpacing: -0.5 }, - titleAlone: { fontSize: 24, lineHeight: 28 }, + title: { flex: 1, minWidth: 0, ...displayTitle(22, 1.1, -0.025) }, + titleAlone: displayTitle(24, 1.1, -0.025), edit: { + flexShrink: 0, minHeight: hit.min, flexDirection: 'row', alignItems: 'center', diff --git a/mobile/src/layouts/ModalLayout.tsx b/mobile/src/layouts/ModalLayout.tsx index 9f2f385c..fba06713 100644 --- a/mobile/src/layouts/ModalLayout.tsx +++ b/mobile/src/layouts/ModalLayout.tsx @@ -106,7 +106,7 @@ export default ModalLayout; const styles = StyleSheet.create((theme) => ({ screen: { flex: 1, backgroundColor: theme.colors.background }, header: { paddingTop: theme.space.m }, - childrenContainer: { flex: 1, paddingHorizontal: 16 }, + childrenContainer: { flex: 1, paddingHorizontal: theme.space.gutter }, bottomActionContainer: { width: '100%', backgroundColor: theme.colors.background, diff --git a/mobile/src/layouts/PageLayout.tsx b/mobile/src/layouts/PageLayout.tsx index e0dc97dc..4ab42bf5 100644 --- a/mobile/src/layouts/PageLayout.tsx +++ b/mobile/src/layouts/PageLayout.tsx @@ -133,7 +133,11 @@ export default PageLayout; const styles = StyleSheet.create((theme) => ({ screen: { flex: 1, backgroundColor: theme.colors.background }, - childrenContainer: { flex: 1, paddingHorizontal: 16, paddingVertical: theme.space.xs }, + childrenContainer: { + flex: 1, + paddingHorizontal: theme.space.gutter, + paddingVertical: theme.space.xs, + }, actions: { gap: theme.space.s, paddingTop: theme.space.m, diff --git a/mobile/src/layouts/displayTitle.ts b/mobile/src/layouts/displayTitle.ts new file mode 100644 index 00000000..672b9733 --- /dev/null +++ b/mobile/src/layouts/displayTitle.ts @@ -0,0 +1,26 @@ +/** + * Bricolage Grotesque title metrics for the auth, onboarding and layout titles. + * + * The font's ascent (.93em) and descent (.27em) are tall, so with the tight line height of the + * handbook (≈ .96em for large titles) the accents of Î / Ș / Ă and the commas of ș / ț reach + * outside the text box: iOS lays the glyphs from the bottom of a short line (no baseline offset + * when the line is shorter than the font), Android clips at the view's edge. The box gets that + * headroom as padding, cancelled by negative margins, so the layout keeps the tight line height + * and nothing is clipped. Letter spacing is in points, computed for the size used. + */ +export const displayTitle = (size: number, ratio = 0.96, tracking = -0.04) => { + const top = Math.ceil(size * 0.18); + const bottom = Math.ceil(size * 0.1); + return { + fontSize: size, + lineHeight: Math.round(size * ratio), + letterSpacing: Math.round(size * tracking * 100) / 100, + includeFontPadding: false, + paddingTop: top, + marginTop: -top, + paddingBottom: bottom, + marginBottom: -bottom, + }; +}; + +export default displayTitle; diff --git a/mobile/src/screens/CreateUser.tsx b/mobile/src/screens/CreateUser.tsx index 65962fd3..66fb9718 100644 --- a/mobile/src/screens/CreateUser.tsx +++ b/mobile/src/screens/CreateUser.tsx @@ -34,6 +34,7 @@ import { InternalErrors } from '../common/errors/internal-errors.class'; import { CONSTANTS, REGEX } from '../common/constants/constants'; import { renderPhoneNumberPrefix } from '../components/InputPrefixes'; import { usePaddingTop } from '../hooks/usePaddingTop'; +import { displayTitle } from '../layouts/displayTitle'; export type UserFormTypes = { firstName: string; @@ -151,7 +152,6 @@ const CreateUser = ({ navigation }: any) => { headerStyle={{ paddingTop }} > - {/* 20pt side margins: the layout gives 16 */} @@ -275,9 +275,9 @@ const CreateUser = ({ navigation }: any) => { export default CreateUser; const styles = StyleSheet.create((theme) => ({ - content: { marginHorizontal: theme.space.gutter - 16, gap: 18 }, + content: { gap: 18 }, intro: { gap: theme.space.s, paddingTop: theme.space.xxs, paddingBottom: theme.space.xxs }, - heading: { fontSize: 36, lineHeight: 38 }, + heading: displayTitle(36), // the consent sits on a white card so it reads as one block terms: { backgroundColor: theme.colors.card, diff --git a/mobile/src/screens/Landing.tsx b/mobile/src/screens/Landing.tsx index b43fb10a..6e2aa113 100644 --- a/mobile/src/screens/Landing.tsx +++ b/mobile/src/screens/Landing.tsx @@ -21,6 +21,7 @@ import { MailIcon } from '../design/icons'; import { palette } from '../design/tokens'; import { hasSeenOnboarding } from '../common/utils/onboarding'; import { isMigrationNoticeActive } from '../common/utils/migration-notice'; +import { displayTitle } from '../layouts/displayTitle'; const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -251,8 +252,9 @@ const styles = StyleSheet.create((theme) => ({ content: { flexGrow: 1, paddingHorizontal: 24 }, copy: { gap: 12 }, copyCompact: { gap: 8 }, - title: { fontSize: 46, lineHeight: 46, maxWidth: 340 }, - titleCompact: { fontSize: 38, lineHeight: 39 }, + // the masthead headline: tight like the handbook's h1, with headroom for Î / ț (see displayTitle) + title: { ...displayTitle(46, 0.92), maxWidth: 340 }, + titleCompact: displayTitle(38, 0.94), subtitle: { color: 'rgba(255,255,255,0.78)', fontFamily: theme.fontFamily.textBold, diff --git a/mobile/src/screens/Login.tsx b/mobile/src/screens/Login.tsx index 88ebf362..2f4e06da 100644 --- a/mobile/src/screens/Login.tsx +++ b/mobile/src/screens/Login.tsx @@ -21,6 +21,7 @@ import { enter } from '../design/motion'; import { MailIcon } from '../design/icons'; import { palette } from '../design/tokens'; import { isMigrationNoticeActive } from '../common/utils/migration-notice'; +import { displayTitle } from '../layouts/displayTitle'; // A new code can be asked for once a minute (the backend answers AUTH_010 sooner). export const RESEND_AFTER_SECONDS = 60; @@ -229,7 +230,7 @@ const Login = ({ navigation, route }: any) => { )} - + {t('code.or_type')} @@ -251,7 +252,11 @@ const Login = ({ navigation, route }: any) => { {secondsLeft > 0 ? ( - + {t('code.resend_in', { seconds: secondsLeft })} ) : ( @@ -360,7 +365,7 @@ const styles = StyleSheet.create((theme) => ({ // the round button keeps its own 44pt, not the column's width back: { alignSelf: 'flex-start' }, intro: { gap: 10 }, - title: { fontSize: 40, lineHeight: 42 }, + title: displayTitle(40), mailHex: { width: 64, height: 70, alignItems: 'center', justifyContent: 'center' }, notice: { backgroundColor: theme.colors.card, @@ -369,12 +374,18 @@ const styles = StyleSheet.create((theme) => ({ }, divider: { flexDirection: 'row', alignItems: 'center', gap: 12 }, rule: { flex: 1, height: StyleSheet.hairlineWidth, backgroundColor: theme.colors.line }, + // wraps at the largest text sizes on a 320pt phone instead of pushing "Alt email" off screen resendRow: { minHeight: 44, flexDirection: 'row', + flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center', + columnGap: 16, + rowGap: 8, }, + shrink: { flexShrink: 1 }, + dividerText: { flexShrink: 1, textAlign: 'center' }, tabular: { fontVariant: ['tabular-nums'] }, underline: { textDecorationLine: 'underline' }, footer: { diff --git a/mobile/src/screens/Onboarding.tsx b/mobile/src/screens/Onboarding.tsx index 9cb40134..676ba68f 100644 --- a/mobile/src/screens/Onboarding.tsx +++ b/mobile/src/screens/Onboarding.tsx @@ -33,6 +33,7 @@ import { VicLogo } from '../components/brand/VicLogo'; import { HexMotifShape } from '../components/auth/LoginHero'; import { StoryScenes } from '../components/onboarding/StoryScenes'; import { markOnboardingSeen } from '../common/utils/onboarding'; +import { displayTitle } from '../layouts/displayTitle'; export const STORY_DURATION = 3800; @@ -137,11 +138,16 @@ const Onboarding = ({ navigation }: any) => { const Scene = StoryScenes[index]; const story = STORIES[index]; - // The scene shrinks to the room left between the header and the text: on a small phone - // (iPhone SE, Android 360×640) the text never covers it. + // The scene shrinks to the room left between the header and the text, and to the screen width: + // on a small phone (iPhone SE, Android 360×640, 320pt) the text never covers it and it never + // runs off the sides. const [stageHeight, setStageHeight] = useState(0); - const [sceneHeight, setSceneHeight] = useState(0); - const sceneScale = stageHeight && sceneHeight ? Math.min(1, (stageHeight - 16) / sceneHeight) : 1; + const [scene, setScene] = useState({ width: 0, height: 0 }); + const sceneScale = Math.min( + 1, + stageHeight && scene.height ? (stageHeight - 16) / scene.height : 1, + scene.width ? (width - 32) / scene.width : 1, + ); const motifSize = Math.min(340, width * 0.9); return ( @@ -190,7 +196,12 @@ const Onboarding = ({ navigation }: any) => { setSceneHeight(event.nativeEvent.layout.height)} + onLayout={(event) => + setScene({ + width: event.nativeEvent.layout.width, + height: event.nativeEvent.layout.height, + }) + } > @@ -279,7 +290,7 @@ const styles = StyleSheet.create((theme) => ({ }, motif: { position: 'absolute' }, copy: { paddingHorizontal: 24, paddingTop: 8, paddingBottom: 16, gap: 10, minHeight: 112 }, - title: { fontSize: 34, lineHeight: 36 }, + title: displayTitle(34), foot: { paddingHorizontal: 24, gap: 14 }, dots: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 4 }, dot: {