255 lines
7.1 KiB
Dart
255 lines
7.1 KiB
Dart
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<Widget> main;
|
|
final List<Widget> 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<Widget> 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<Widget> 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),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|