feat(#78): Migrer PresentationFormScreen vers infrastructure multi-modes

Adaptation responsive du formulaire de présentation (AM Step 3) :
- Desktop : Layout horizontal avec scroll global (format 2:1)
- Mobile : Layout plein écran sans scroll global
  - Header fixe (titre + étape)
  - Carte occupe tout l'espace vertical disponible
  - Seul le champ texte interne est scrollable
  - Boutons fixes en bas
- Checkbox CGU adaptée (texte raccourci + scale 0.85 en mobile)
- Chevrons uniquement en mode desktop

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MARTIN Julien 2026-02-04 11:19:43 +01:00
parent f8bd911c02
commit bdecbc2c1d

View File

@ -5,9 +5,15 @@ import 'dart:math' as math;
import 'custom_decorated_text_field.dart'; import 'custom_decorated_text_field.dart';
import 'app_custom_checkbox.dart'; import 'app_custom_checkbox.dart';
import 'custom_navigation_button.dart';
import 'hover_relief_widget.dart';
import '../models/card_assets.dart'; import '../models/card_assets.dart';
import '../config/display_config.dart';
/// Widget générique pour le formulaire de présentation avec texte libre + CGU
/// Supporte mode éditable et readonly, responsive mobile/desktop
class PresentationFormScreen extends StatefulWidget { class PresentationFormScreen extends StatefulWidget {
final DisplayMode mode;
final String stepText; // Ex: "Étape 3/4" ou "Étape 4/5" final String stepText; // Ex: "Étape 3/4" ou "Étape 4/5"
final String title; // Ex: "Présentation et Conditions" ou "Motivation de votre demande" final String title; // Ex: "Présentation et Conditions" ou "Motivation de votre demande"
final CardColorHorizontal cardColor; final CardColorHorizontal cardColor;
@ -19,6 +25,7 @@ class PresentationFormScreen extends StatefulWidget {
const PresentationFormScreen({ const PresentationFormScreen({
super.key, super.key,
this.mode = DisplayMode.editable,
required this.stepText, required this.stepText,
required this.title, required this.title,
required this.cardColor, required this.cardColor,
@ -66,9 +73,7 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
final cardWidth = screenSize.width * 0.6; final config = DisplayConfig.fromContext(context, mode: widget.mode);
final double imageAspectRatio = 2.0;
final cardHeight = cardWidth / imageAspectRatio;
return Scaffold( return Scaffold(
body: Stack( body: Stack(
@ -76,7 +81,92 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
Positioned.fill( Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat), child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
), ),
Center( config.isMobile
? _buildMobileLayout(context, config, screenSize)
: _buildDesktopLayout(context, config, screenSize),
// Chevrons desktop uniquement
if (!config.isMobile) ...[
// Chevron Gauche (Retour)
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(math.pi),
child: Image.asset('assets/images/chevron_right.png', height: 40),
),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go(widget.previousRoute);
}
},
tooltip: 'Retour',
),
),
// Chevron Droit (Suivant)
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: _cguAccepted ? _handleSubmit : null,
tooltip: 'Suivant',
),
),
],
],
),
);
}
/// Layout MOBILE : Plein écran sans scroll global
Widget _buildMobileLayout(BuildContext context, DisplayConfig config, Size screenSize) {
return Column(
children: [
// Header fixe
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Column(
children: [
Text(
widget.stepText,
style: GoogleFonts.merienda(
fontSize: 13,
color: Colors.black54,
),
),
const SizedBox(height: 6),
Text(
widget.title,
style: GoogleFonts.merienda(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 16),
// Carte qui prend tout l'espace restant
Expanded(
child: _buildMobileCard(context, config, screenSize),
),
// Boutons en bas
const SizedBox(height: 20),
_buildMobileButtons(context, config, screenSize),
const SizedBox(height: 10),
],
);
}
/// Layout DESKTOP : Avec scroll
Widget _buildDesktopLayout(BuildContext context, DisplayConfig config, Size screenSize) {
return Center(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0), padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0),
child: Column( child: Column(
@ -90,11 +180,28 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text(
widget.title, widget.title,
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), style: GoogleFonts.merienda(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
Container( _buildDesktopCard(context, config, screenSize),
],
),
),
);
}
/// Carte DESKTOP : Format horizontal 2:1
Widget _buildDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) {
final cardWidth = screenSize.width * 0.6;
final double imageAspectRatio = 2.0;
final cardHeight = cardWidth / imageAspectRatio;
return Container(
width: cardWidth, width: cardWidth,
height: cardHeight, height: cardHeight,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -127,21 +234,67 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
], ],
), ),
), ),
);
}
/// Carte MOBILE : Prend tout l'espace disponible
Widget _buildMobileCard(BuildContext context, DisplayConfig config, Size screenSize) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(_getVerticalCardAsset()),
fit: BoxFit.fill,
),
),
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20),
child: Column(
children: [
// Champ de texte qui prend l'espace disponible et scrollable
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
return CustomDecoratedTextField(
controller: _textController,
hintText: widget.textFieldHint,
fieldHeight: constraints.maxHeight,
maxLines: 100, // Grande valeur pour permettre le scroll
expandDynamically: false,
fontSize: 14.0,
);
},
),
),
const SizedBox(height: 16),
// Checkbox en bas
Transform.scale(
scale: 0.85,
child: AppCustomCheckbox(
label: 'J\'accepte les CGU et la\nPolitique de confidentialité',
value: _cguAccepted,
onChanged: (value) => setState(() => _cguAccepted = value ?? false),
),
), ),
], ],
), ),
), ),
);
}
/// Boutons mobile
Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: screenSize.width * 0.05,
), ),
// Chevron Gauche (Retour) child: Row(
Positioned( children: [
top: screenSize.height / 2 - 20, Expanded(
left: 40, child: HoverReliefWidget(
child: IconButton( child: CustomNavigationButton(
icon: Transform( text: 'Précédent',
alignment: Alignment.center, style: NavigationButtonStyle.purple,
transform: Matrix4.rotationY(math.pi),
child: Image.asset('assets/images/chevron_right.png', height: 40),
),
onPressed: () { onPressed: () {
if (context.canPop()) { if (context.canPop()) {
context.pop(); context.pop();
@ -149,21 +302,47 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
context.go(widget.previousRoute); context.go(widget.previousRoute);
} }
}, },
tooltip: 'Retour', width: double.infinity,
height: 50,
fontSize: 16,
), ),
), ),
// Chevron Droit (Suivant) ),
Positioned( const SizedBox(width: 16),
top: screenSize.height / 2 - 20, Expanded(
right: 40, child: HoverReliefWidget(
child: IconButton( child: CustomNavigationButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40), text: 'Suivant',
onPressed: _cguAccepted ? _handleSubmit : null, style: NavigationButtonStyle.green,
tooltip: 'Suivant', onPressed: _handleSubmit,
width: double.infinity,
height: 50,
fontSize: 16,
),
), ),
), ),
], ],
), ),
); );
} }
/// Retourne l'asset de carte vertical correspondant à la couleur
String _getVerticalCardAsset() {
switch (widget.cardColor) {
case CardColorHorizontal.blue:
return CardColorVertical.blue.path;
case CardColorHorizontal.green:
return CardColorVertical.green.path;
case CardColorHorizontal.lavender:
return CardColorVertical.lavender.path;
case CardColorHorizontal.lime:
return CardColorVertical.lime.path;
case CardColorHorizontal.peach:
return CardColorVertical.peach.path;
case CardColorHorizontal.pink:
return CardColorVertical.pink.path;
case CardColorHorizontal.red:
return CardColorVertical.red.path;
}
}
} }