import 'package:flutter/material.dart'; import 'package:manager_app/constants.dart'; /// Carte de contenu d'un écran d'édition : fond blanc, bordure 1 px, titre en /// `kTitleCard` précédé d'une icône. Remplace les `Card(elevation: 0, radius: 16)` /// dont le titre répétait le nom du champ qu'elles contenaient. class Pane extends StatelessWidget { const Pane({ Key? key, required this.icon, required this.title, required this.child, this.action, }) : super(key: key); final IconData icon; final String title; final Widget child; final Widget? action; @override Widget build(BuildContext context) { return Container( padding: kCardPadding, decoration: BoxDecoration( color: kSurface, border: Border.all(color: kLine), borderRadius: BorderRadius.circular(kRadiusCard), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Icon(icon, size: 14, color: kPrimaryColor), const SizedBox(width: kSpace2), Expanded(child: Text(title, style: kTitleCard)), if (action != null) action!, ], ), const SizedBox(height: kSpace4), child, ], ), ); } } /// Les deux colonnes des écrans d'édition : le travail à gauche, le rail à /// droite. Sous 900 px, tout s'empile — responsive jusqu'à la tablette, pas /// d'éditeur mobile. class EditorColumns extends StatelessWidget { const EditorColumns({Key? key, required this.main, required this.rail}) : super(key: key); final List main; final List rail; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final stacked = constraints.maxWidth < 900; final mainColumn = _stack(main); final railColumn = _stack(rail); if (stacked) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ mainColumn, const SizedBox(height: kSpace5), railColumn, ], ); } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: mainColumn), const SizedBox(width: kSpace5), SizedBox(width: 300, child: railColumn), ], ); }, ); } Widget _stack(List panes) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < panes.length; i++) ...[ if (i > 0) const SizedBox(height: kSpace5), panes[i], ], ], ); } } /// En-tête d'un écran d'édition : le retour, le titre et son contexte, puis les /// actions. Elles étaient dans une barre flottante en bas de l'écran, qui /// mangeait 60 px de contenu. class EditorHeader extends StatelessWidget { const EditorHeader({ Key? key, required this.title, required this.subtitle, required this.onBack, this.leadingIcon, this.chip, this.actions = const [], }) : super(key: key); final String title; final String subtitle; final VoidCallback onBack; final IconData? leadingIcon; final Widget? chip; final List actions; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: kSpace6, vertical: kSpace4), decoration: const BoxDecoration( color: kSurface, border: Border(bottom: BorderSide(color: kLine)), ), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back, color: kPrimaryColor, size: 20), onPressed: onBack, ), const SizedBox(width: kSpace2), if (leadingIcon != null) ...[ Icon(leadingIcon, color: kPrimaryColor, size: 20), const SizedBox(width: kSpace3), ], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: kInk), overflow: TextOverflow.ellipsis), Text(subtitle, style: const TextStyle(fontSize: 11.5, color: kInk3), overflow: TextOverflow.ellipsis), ], ), ), if (chip != null) ...[ const SizedBox(width: kSpace4), chip!, ], for (final action in actions) ...[ const SizedBox(width: kSpace3), action, ], ], ), ); } } /// Les trois boutons de la convention `confirmation_dialog.dart` : pastille, /// 13 px, w600. class PillButton extends StatelessWidget { const PillButton({ Key? key, required this.label, required this.onPressed, this.icon, this.style = PillButtonStyle.ghost, }) : super(key: key); final String label; final VoidCallback onPressed; final IconData? icon; final PillButtonStyle style; @override Widget build(BuildContext context) { const textStyle = TextStyle(fontSize: 13, fontWeight: FontWeight.w600); if (style == PillButtonStyle.ghost) { return TextButton.icon( onPressed: onPressed, icon: icon == null ? null : Icon(icon, size: 15, color: kInk2), label: Text(label, style: textStyle.copyWith(color: kInk2)), ); } return FilledButton.icon( onPressed: onPressed, icon: icon == null ? null : Icon(icon, size: 15), label: Text(label, style: textStyle), style: FilledButton.styleFrom( backgroundColor: style == PillButtonStyle.danger ? kError : kPrimaryColor, foregroundColor: kWhite, padding: const EdgeInsets.symmetric( horizontal: kSpace5, vertical: kSpace3), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(kRadiusPill)), ), ); } } enum PillButtonStyle { ghost, solid, danger } /// Bouton icône encadré de l'en-tête (export, aide…). class SquareIconButton extends StatelessWidget { const SquareIconButton({ Key? key, required this.icon, required this.onPressed, this.tooltip, }) : super(key: key); final IconData icon; final VoidCallback onPressed; final String? tooltip; @override Widget build(BuildContext context) { return Tooltip( message: tooltip ?? "", child: Material( color: kSurface, borderRadius: BorderRadius.circular(kRadiusInput), child: InkWell( borderRadius: BorderRadius.circular(kRadiusInput), onTap: onPressed, child: Container( width: 30, height: 30, decoration: BoxDecoration( border: Border.all(color: kLine), borderRadius: BorderRadius.circular(kRadiusInput), ), child: Icon(icon, size: 16, color: kInk2), ), ), ), ); } }