refactor(#93): uniformiser la ligne utilisateur et afficher Modifier au survol

Met le rendu des lignes sur une seule ligne (icone, nom, infos) et n’affiche l’action Modifier qu’au hover pour alléger visuellement les listes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MARTIN Julien 2026-02-20 15:10:27 +01:00
parent 5da2ab9005
commit aec1990ec9
2 changed files with 101 additions and 66 deletions

View File

@ -89,13 +89,6 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
// TODO: Modifier admin // TODO: Modifier admin
}, },
), ),
IconButton(
icon: const Icon(Icons.delete),
tooltip: 'Supprimer',
onPressed: () {
// TODO: Supprimer admin
},
),
], ],
); );
}, },

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AdminUserCard extends StatelessWidget { class AdminUserCard extends StatefulWidget {
final String title; final String title;
final List<String> subtitleLines; final List<String> subtitleLines;
final String? avatarUrl; final String? avatarUrl;
@ -16,75 +16,117 @@ class AdminUserCard extends StatelessWidget {
this.actions = const [], this.actions = const [],
}); });
@override
State<AdminUserCard> createState() => _AdminUserCardState();
}
class _AdminUserCardState extends State<AdminUserCard> {
bool _isHovered = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( final infoLine =
margin: const EdgeInsets.only(bottom: 12), widget.subtitleLines.where((e) => e.trim().isNotEmpty).join(' ');
elevation: 0, final actionsWidth =
shape: RoundedRectangleBorder( widget.actions.isNotEmpty ? widget.actions.length * 30.0 : 0.0;
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.grey.shade300), return MouseRegion(
), onEnter: (_) => setState(() => _isHovered = true),
child: Padding( onExit: (_) => setState(() => _isHovered = false),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Material(
child: Row( color: Colors.transparent,
children: [ borderRadius: BorderRadius.circular(10),
CircleAvatar( child: InkWell(
radius: 14, onTap: () {},
backgroundColor: const Color(0xFFEDE5FA), borderRadius: BorderRadius.circular(10),
backgroundImage: hoverColor: const Color(0x149CC5C0),
avatarUrl != null ? NetworkImage(avatarUrl!) : null, child: Card(
child: avatarUrl == null margin: const EdgeInsets.only(bottom: 12),
? Icon( elevation: 0,
fallbackIcon, shape: RoundedRectangleBorder(
size: 16, borderRadius: BorderRadius.circular(10),
color: const Color(0xFF6B3FA0), side: BorderSide(color: Colors.grey.shade300),
)
: null,
), ),
const SizedBox(width: 10), child: Padding(
Expanded( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
child: Column( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( CircleAvatar(
title.isNotEmpty ? title : 'Sans nom', radius: 14,
style: const TextStyle( backgroundColor: const Color(0xFFEDE5FA),
fontWeight: FontWeight.w600, backgroundImage: widget.avatarUrl != null
fontSize: 14, ? NetworkImage(widget.avatarUrl!)
: null,
child: widget.avatarUrl == null
? Icon(
widget.fallbackIcon,
size: 16,
color: const Color(0xFF6B3FA0),
)
: null,
),
const SizedBox(width: 10),
Expanded(
child: Row(
children: [
Flexible(
fit: FlexFit.loose,
child: Text(
widget.title.isNotEmpty ? widget.title : 'Sans nom',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
infoLine,
style: const TextStyle(
color: Colors.black54,
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
), ),
), ),
const SizedBox(height: 2), if (widget.actions.isNotEmpty)
...subtitleLines.map( SizedBox(
(line) => Text( width: actionsWidth,
line, child: AnimatedOpacity(
style: const TextStyle( duration: const Duration(milliseconds: 120),
color: Colors.black54, opacity: _isHovered ? 1 : 0,
fontSize: 12, child: IgnorePointer(
ignoring: !_isHovered,
child: IconTheme(
data: const IconThemeData(size: 17),
child: IconButtonTheme(
data: IconButtonThemeData(
style: IconButton.styleFrom(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(4),
minimumSize: const Size(28, 28),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: widget.actions,
),
),
),
),
), ),
), ),
),
], ],
), ),
), ),
if (actions.isNotEmpty) ),
IconTheme(
data: const IconThemeData(size: 18),
child: IconButtonTheme(
data: IconButtonThemeData(
style: IconButton.styleFrom(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(4),
minimumSize: const Size(28, 28),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: actions,
),
),
),
],
), ),
), ),
); );