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,94 +81,268 @@ 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
child: SingleChildScrollView( ? _buildMobileLayout(context, config, screenSize)
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0), : _buildDesktopLayout(context, config, screenSize),
child: Column( // Chevrons desktop uniquement
mainAxisAlignment: MainAxisAlignment.center, if (!config.isMobile) ...[
crossAxisAlignment: CrossAxisAlignment.center, // Chevron Gauche (Retour)
children: [ Positioned(
Text( top: screenSize.height / 2 - 20,
widget.stepText, left: 40,
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), child: IconButton(
), icon: Transform(
const SizedBox(height: 20), alignment: Alignment.center,
Text( transform: Matrix4.rotationY(math.pi),
widget.title, child: Image.asset('assets/images/chevron_right.png', height: 40),
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), ),
textAlign: TextAlign.center, onPressed: () {
), if (context.canPop()) {
const SizedBox(height: 30), context.pop();
Container( } else {
width: cardWidth, context.go(widget.previousRoute);
height: cardHeight, }
decoration: BoxDecoration( },
image: DecorationImage( tooltip: 'Retour',
image: AssetImage(widget.cardColor.path), ),
fit: BoxFit.fill, ),
), // Chevron Droit (Suivant)
), Positioned(
child: Padding( top: screenSize.height / 2 - 20,
padding: const EdgeInsets.all(40.0), right: 40,
child: Column( child: IconButton(
mainAxisAlignment: MainAxisAlignment.center, icon: Image.asset('assets/images/chevron_right.png', height: 40),
children: [ onPressed: _cguAccepted ? _handleSubmit : null,
Expanded( tooltip: 'Suivant',
child: CustomDecoratedTextField( ),
controller: _textController, ),
hintText: widget.textFieldHint, ],
fieldHeight: cardHeight * 0.6, ],
maxLines: 10, ),
expandDynamically: true, );
fontSize: 18.0, }
),
), /// Layout MOBILE : Plein écran sans scroll global
const SizedBox(height: 20), Widget _buildMobileLayout(BuildContext context, DisplayConfig config, Size screenSize) {
AppCustomCheckbox( return Column(
label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité', children: [
value: _cguAccepted, // Header fixe
onChanged: (value) => setState(() => _cguAccepted = value ?? false), 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(
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
widget.stepText,
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 20),
Text(
widget.title,
style: GoogleFonts.merienda(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
_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,
height: cardHeight,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(widget.cardColor.path),
fit: BoxFit.fill,
),
),
child: Padding(
padding: const EdgeInsets.all(40.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: CustomDecoratedTextField(
controller: _textController,
hintText: widget.textFieldHint,
fieldHeight: cardHeight * 0.6,
maxLines: 10,
expandDynamically: true,
fontSize: 18.0,
),
),
const SizedBox(height: 20),
AppCustomCheckbox(
label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité',
value: _cguAccepted,
onChanged: (value) => setState(() => _cguAccepted = value ?? false),
),
],
),
),
);
}
/// 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,
),
child: Row(
children: [
Expanded(
child: HoverReliefWidget(
child: CustomNavigationButton(
text: 'Précédent',
style: NavigationButtonStyle.purple,
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go(widget.previousRoute);
}
},
width: double.infinity,
height: 50,
fontSize: 16,
), ),
), ),
), ),
// Chevron Gauche (Retour) const SizedBox(width: 16),
Positioned( Expanded(
top: screenSize.height / 2 - 20, child: HoverReliefWidget(
left: 40, child: CustomNavigationButton(
child: IconButton( text: 'Suivant',
icon: Transform( style: NavigationButtonStyle.green,
alignment: Alignment.center, onPressed: _handleSubmit,
transform: Matrix4.rotationY(math.pi), width: double.infinity,
child: Image.asset('assets/images/chevron_right.png', height: 40), height: 50,
fontSize: 16,
), ),
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',
), ),
), ),
], ],
), ),
); );
} }
/// 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;
}
}
} }