From e7706058b76e4ca0c68ec2f143d5f093211a19df Mon Sep 17 00:00:00 2001 From: Marc Rejohn Castillano Date: Sat, 26 Sep 2026 23:35:19 +0800 Subject: [PATCH] =?UTF-8?q?UI=20Phase=202:=20cosmetic=20leftovers=20?= =?UTF-8?q?=E2=80=94=20compact=20Profile=20photo=20card,=20fit=20Users=20t?= =?UTF-8?q?able?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Profile Photo card: replace the tall centered avatar+buttons stack with a compact horizontal row (avatar left, caption + Upload/Camera buttons right), roughly halving the card height and reclaiming the dead vertical space. - TasQColumn gains an optional `minWidth`; the adaptive table now sums per-column minimums instead of a flat 200 px each. User Management sets narrow widths on its short columns (Role/Offices/Status/Last active) so all six columns fit at desktop width without the horizontal scroll that was clipping the Status column. Other tables keep the 200 px default (unchanged). Verified: flutter analyze lib clean; flutter build web green; visual QA (Profile, User Management) in dark. Co-Authored-By: claude-flow --- lib/screens/admin/user_management_screen.dart | 4 + lib/screens/profile/profile_screen.dart | 103 ++++++++++-------- lib/widgets/tasq_adaptive_list.dart | 12 +- 3 files changed, 74 insertions(+), 45 deletions(-) diff --git a/lib/screens/admin/user_management_screen.dart b/lib/screens/admin/user_management_screen.dart index d3393e4e..0e9ee59b 100644 --- a/lib/screens/admin/user_management_screen.dart +++ b/lib/screens/admin/user_management_screen.dart @@ -199,10 +199,12 @@ class _UserManagementScreenState extends ConsumerState { ), TasQColumn( header: 'Role', + minWidth: 120, cellBuilder: (context, profile) => Text(_roleLabel(profile.role)), ), TasQColumn( header: 'Offices', + minWidth: 100, cellBuilder: (context, profile) { final officesAssigned = officeCountByUser[profile.id] ?? 0; return Text(officesAssigned == 0 ? 'None' : '$officesAssigned'); @@ -210,6 +212,7 @@ class _UserManagementScreenState extends ConsumerState { ), TasQColumn( header: 'Status', + minWidth: 120, cellBuilder: (context, profile) { final statusAsync = ref.watch(adminUserStatusProvider(profile.id)); return statusAsync.when( @@ -224,6 +227,7 @@ class _UserManagementScreenState extends ConsumerState { ), TasQColumn( header: 'Last active', + minWidth: 150, cellBuilder: (context, profile) { final lastActive = lastActiveByUser[profile.id]; return Text(_formatLastActiveLabel(lastActive)); diff --git a/lib/screens/profile/profile_screen.dart b/lib/screens/profile/profile_screen.dart index 626fc573..4af9e696 100644 --- a/lib/screens/profile/profile_screen.dart +++ b/lib/screens/profile/profile_screen.dart @@ -280,54 +280,69 @@ class _ProfileScreenState extends ConsumerState { children: [ const AppSectionHeader('Profile Photo', padding: EdgeInsets.zero), const SizedBox(height: 16), - Center( - child: Stack( - children: [ - Hero( - tag: 'profile-avatar', - child: CircleAvatar( - radius: 56, - backgroundColor: colors.surfaceContainerHighest, - backgroundImage: avatarUrl != null - ? NetworkImage(avatarUrl) - : null, - child: avatarUrl == null - ? Icon( - Icons.person, - size: 48, - color: colors.onSurfaceVariant, - ) - : null, - ), - ), - if (_uploadingAvatar) - const Positioned.fill( - child: Center(child: CircularProgressIndicator()), - ), - ], - ), - ), - const SizedBox(height: 12), Row( - mainAxisAlignment: MainAxisAlignment.center, children: [ - FilledButton.tonalIcon( - onPressed: _uploadingAvatar - ? null - : () => _pickAvatar(ImageSource.gallery), - icon: const Icon(Icons.photo_library), - label: const Text('Upload'), + Stack( + children: [ + Hero( + tag: 'profile-avatar', + child: CircleAvatar( + radius: 44, + backgroundColor: colors.surfaceContainerHighest, + backgroundImage: avatarUrl != null + ? NetworkImage(avatarUrl) + : null, + child: avatarUrl == null + ? Icon( + Icons.person, + size: 40, + color: colors.onSurfaceVariant, + ) + : null, + ), + ), + if (_uploadingAvatar) + const Positioned.fill( + child: Center(child: CircularProgressIndicator()), + ), + ], ), - if (!kIsWeb) ...[ - const SizedBox(width: 12), - FilledButton.tonalIcon( - onPressed: _uploadingAvatar - ? null - : () => _pickAvatar(ImageSource.camera), - icon: const Icon(Icons.camera_alt), - label: const Text('Camera'), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'A clear headshot helps teammates recognize you.', + style: theme.textTheme.bodySmall?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 12, + runSpacing: 8, + children: [ + FilledButton.tonalIcon( + onPressed: _uploadingAvatar + ? null + : () => _pickAvatar(ImageSource.gallery), + icon: const Icon(Icons.photo_library), + label: const Text('Upload'), + ), + if (!kIsWeb) + FilledButton.tonalIcon( + onPressed: _uploadingAvatar + ? null + : () => _pickAvatar(ImageSource.camera), + icon: const Icon(Icons.camera_alt), + label: const Text('Camera'), + ), + ], + ), + ], ), - ], + ), ], ), ], diff --git a/lib/widgets/tasq_adaptive_list.dart b/lib/widgets/tasq_adaptive_list.dart index d0420a7e..5268749c 100644 --- a/lib/widgets/tasq_adaptive_list.dart +++ b/lib/widgets/tasq_adaptive_list.dart @@ -18,6 +18,7 @@ class TasQColumn { required this.cellBuilder, this.technical = false, this.hideOnMedium = false, + this.minWidth, }); /// The column header text. @@ -33,6 +34,11 @@ class TasQColumn { /// Use this for lower-priority columns (e.g. Timestamp, Status) that /// would otherwise force a horizontal scrollbar on tablet viewports. final bool hideOnMedium; + + /// Minimum comfortable width for this column, in logical pixels. When null a + /// default of 200 is used. Set a smaller value for short columns (a count, a + /// status pill, a short enum) so the table fits without a horizontal scroll. + final double? minWidth; } /// Builds a mobile tile for [TasQAdaptiveList]. @@ -380,9 +386,13 @@ class TasQAdaptiveList extends StatelessWidget { const double colSpacing = 20.0; final actionsColumnCount = rowActions == null ? 0 : 1; final totalCols = visibleColumns.length + actionsColumnCount; + final columnsMinWidth = visibleColumns.fold( + 0.0, + (sum, c) => sum + (c.minWidth ?? colMinW), + ); final minColumnsWidth = hMarginTotal + - (visibleColumns.length * colMinW) + + columnsMinWidth + (actionsColumnCount * actMinW) + (math.max(0, totalCols - 1) * colSpacing); final tableWidth = math.max(contentWidth, minColumnsWidth);