feat(front): coquille quotidien parent 3 colonnes + bandeau pastel (#166)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-23 23:40:16 +02:00
co-authored by Cursor
parent aeda636983
commit 2daf6c9cda
19 changed files with 742 additions and 652 deletions
@@ -0,0 +1,320 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:p_tits_pas/widgets/app_footer.dart';
import 'package:p_tits_pas/widgets/quotidien/quotidien_bandeau.dart';
import 'package:p_tits_pas/widgets/quotidien/quotidien_theme.dart';
/// Coquille « Cahier de liaison » quotidien 3 colonnes + bandeau (#166).
/// L’AM (#169) réutilise ce widget en injectant ses slots.
class QuotidienShell extends StatelessWidget {
final QuotidienNavSection selectedSection;
final ValueChanged<QuotidienNavSection> onSectionSelected;
final String userDisplayName;
final String? userEmail;
final VoidCallback? onProfileTap;
final VoidCallback? onSearchAmTap;
final VoidCallback? onSettingsTap;
final VoidCallback? onLogout;
/// Colonne gauche (couple + cartes + actions).
final Widget leftColumn;
/// Colonne milieu (blog).
final Widget centerColumn;
/// Colonne droite (messagerie).
final Widget rightColumn;
/// Corps affiché hors Cahier de liaison (Agenda / Contrat stubs).
final Widget? agendaBody;
final Widget? contratBody;
const QuotidienShell({
super.key,
required this.selectedSection,
required this.onSectionSelected,
required this.userDisplayName,
required this.leftColumn,
required this.centerColumn,
required this.rightColumn,
this.userEmail,
this.onProfileTap,
this.onSearchAmTap,
this.onSettingsTap,
this.onLogout,
this.agendaBody,
this.contratBody,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: QuotidienTheme.ivory,
body: Container(
decoration: QuotidienTheme.paperBackground(),
child: Column(
children: [
QuotidienBandeau(
selectedSection: selectedSection,
onSectionSelected: onSectionSelected,
userDisplayName: userDisplayName,
userEmail: userEmail,
onProfileTap: onProfileTap,
onSearchAmTap: onSearchAmTap,
onSettingsTap: onSettingsTap,
onLogout: onLogout,
),
const QuotidienPencilDivider(),
Expanded(child: _bodyForSection(context)),
const QuotidienPencilDivider(),
const AppFooter(showTopBorder: false),
],
),
),
);
}
Widget _bodyForSection(BuildContext context) {
switch (selectedSection) {
case QuotidienNavSection.liaison:
return _ThreeColumns(
left: leftColumn,
center: centerColumn,
right: rightColumn,
);
case QuotidienNavSection.agenda:
return agendaBody ??
const QuotidienStubPage(
title: 'Agenda',
message: 'Page Agenda — à venir.',
);
case QuotidienNavSection.contrat:
return contratBody ??
const QuotidienStubPage(
title: 'Contrat',
message: 'Page Contrat — à venir.',
);
}
}
}
/// Trait « crayon gris » dessiné à la main : sépare bandeau / corps / footer
/// (horizontal) et les 3 colonnes (vertical). Le PNG (2400×28) est étiré dans
/// le sens du trait seulement : sur un trait, c'est invisible.
class QuotidienPencilDivider extends StatelessWidget {
final Axis axis;
/// Épaisseur de la zone du trait (hauteur si horizontal, largeur sinon).
final double thickness;
final EdgeInsets padding;
const QuotidienPencilDivider({
super.key,
this.axis = Axis.horizontal,
this.thickness = 14,
this.padding = const EdgeInsets.symmetric(horizontal: 24),
});
const QuotidienPencilDivider.vertical({
super.key,
this.thickness = 14,
this.padding = const EdgeInsets.symmetric(vertical: 16),
}) : axis = Axis.vertical;
@override
Widget build(BuildContext context) {
final horizontal = axis == Axis.horizontal;
return Padding(
padding: padding,
child: SizedBox(
width: horizontal ? double.infinity : thickness,
height: horizontal ? thickness : double.infinity,
child: Image.asset(
horizontal
? QuotidienTheme.pencilLineAsset
: QuotidienTheme.pencilLineVerticalAsset,
fit: BoxFit.fill,
filterQuality: FilterQuality.medium,
),
),
);
}
}
class _ThreeColumns extends StatelessWidget {
final Widget left;
final Widget center;
final Widget right;
const _ThreeColumns({
required this.left,
required this.center,
required this.right,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final wide = constraints.maxWidth >= 900;
if (!wide) {
// Socle mobile temporaire (#188 = swipe dédié) : pile verticale.
return ListView(
padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
children: [
_ColumnPanel(child: left),
const QuotidienPencilDivider(
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 4),
),
_ColumnPanel(child: center),
const QuotidienPencilDivider(
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 4),
),
_ColumnPanel(child: right),
],
);
}
return Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: _ColumnPanel(child: left)),
const QuotidienPencilDivider.vertical(),
Expanded(child: _ColumnPanel(child: center)),
const QuotidienPencilDivider.vertical(),
Expanded(child: _ColumnPanel(child: right)),
],
),
);
},
);
}
}
/// Panneau colonne (carte pastel semi-transparente).
class _ColumnPanel extends StatelessWidget {
final Widget child;
const _ColumnPanel({required this.child});
@override
Widget build(BuildContext context) {
return Container(
// Pas de bordure Material : la séparation est assurée par les traits
// crayon, on garde juste un léger fond.
decoration: BoxDecoration(
color: QuotidienTheme.columnCardFill.withOpacity(0.82),
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias,
child: child,
);
}
}
/// Placeholder de colonne métier (branchable par tickets C/D/E).
class QuotidienColumnPlaceholder extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
const QuotidienColumnPlaceholder({
super.key,
required this.title,
required this.subtitle,
required this.icon,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Icon(icon, color: QuotidienTheme.lavender, size: 22),
const SizedBox(width: 8),
Text(
title,
style: GoogleFonts.merienda(
fontSize: 18,
fontWeight: FontWeight.w600,
color: QuotidienTheme.ink,
),
),
],
),
const SizedBox(height: 12),
Expanded(
child: Center(
child: Text(
subtitle,
textAlign: TextAlign.center,
style: GoogleFonts.merriweather(
fontSize: 13,
color: QuotidienTheme.muted,
height: 1.4,
),
),
),
),
],
),
);
}
}
/// Stub pleine page (Agenda / Contrat) — contenu riche hors #166.
class QuotidienStubPage extends StatelessWidget {
final String title;
final String message;
const QuotidienStubPage({
super.key,
required this.title,
required this.message,
});
@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Container(
margin: const EdgeInsets.all(24),
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 32),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.85),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: GoogleFonts.merienda(
fontSize: 22,
fontWeight: FontWeight.w600,
color: QuotidienTheme.ink,
),
),
const SizedBox(height: 12),
Text(
message,
textAlign: TextAlign.center,
style: GoogleFonts.merriweather(
fontSize: 14,
color: QuotidienTheme.muted,
),
),
],
),
),
),
);
}
}