UI Phase 2: cosmetic leftovers — compact Profile photo card, fit Users table
- 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 <ruv@ruv.net>
This commit is contained in:
@@ -199,10 +199,12 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
|||||||
),
|
),
|
||||||
TasQColumn<Profile>(
|
TasQColumn<Profile>(
|
||||||
header: 'Role',
|
header: 'Role',
|
||||||
|
minWidth: 120,
|
||||||
cellBuilder: (context, profile) => Text(_roleLabel(profile.role)),
|
cellBuilder: (context, profile) => Text(_roleLabel(profile.role)),
|
||||||
),
|
),
|
||||||
TasQColumn<Profile>(
|
TasQColumn<Profile>(
|
||||||
header: 'Offices',
|
header: 'Offices',
|
||||||
|
minWidth: 100,
|
||||||
cellBuilder: (context, profile) {
|
cellBuilder: (context, profile) {
|
||||||
final officesAssigned = officeCountByUser[profile.id] ?? 0;
|
final officesAssigned = officeCountByUser[profile.id] ?? 0;
|
||||||
return Text(officesAssigned == 0 ? 'None' : '$officesAssigned');
|
return Text(officesAssigned == 0 ? 'None' : '$officesAssigned');
|
||||||
@@ -210,6 +212,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
|||||||
),
|
),
|
||||||
TasQColumn<Profile>(
|
TasQColumn<Profile>(
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
|
minWidth: 120,
|
||||||
cellBuilder: (context, profile) {
|
cellBuilder: (context, profile) {
|
||||||
final statusAsync = ref.watch(adminUserStatusProvider(profile.id));
|
final statusAsync = ref.watch(adminUserStatusProvider(profile.id));
|
||||||
return statusAsync.when(
|
return statusAsync.when(
|
||||||
@@ -224,6 +227,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
|||||||
),
|
),
|
||||||
TasQColumn<Profile>(
|
TasQColumn<Profile>(
|
||||||
header: 'Last active',
|
header: 'Last active',
|
||||||
|
minWidth: 150,
|
||||||
cellBuilder: (context, profile) {
|
cellBuilder: (context, profile) {
|
||||||
final lastActive = lastActiveByUser[profile.id];
|
final lastActive = lastActiveByUser[profile.id];
|
||||||
return Text(_formatLastActiveLabel(lastActive));
|
return Text(_formatLastActiveLabel(lastActive));
|
||||||
|
|||||||
@@ -280,54 +280,69 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||||||
children: [
|
children: [
|
||||||
const AppSectionHeader('Profile Photo', padding: EdgeInsets.zero),
|
const AppSectionHeader('Profile Photo', padding: EdgeInsets.zero),
|
||||||
const SizedBox(height: 16),
|
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(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
FilledButton.tonalIcon(
|
Stack(
|
||||||
onPressed: _uploadingAvatar
|
children: [
|
||||||
? null
|
Hero(
|
||||||
: () => _pickAvatar(ImageSource.gallery),
|
tag: 'profile-avatar',
|
||||||
icon: const Icon(Icons.photo_library),
|
child: CircleAvatar(
|
||||||
label: const Text('Upload'),
|
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: 20),
|
||||||
const SizedBox(width: 12),
|
Expanded(
|
||||||
FilledButton.tonalIcon(
|
child: Column(
|
||||||
onPressed: _uploadingAvatar
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
? null
|
children: [
|
||||||
: () => _pickAvatar(ImageSource.camera),
|
Text(
|
||||||
icon: const Icon(Icons.camera_alt),
|
'A clear headshot helps teammates recognize you.',
|
||||||
label: const Text('Camera'),
|
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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class TasQColumn<T> {
|
|||||||
required this.cellBuilder,
|
required this.cellBuilder,
|
||||||
this.technical = false,
|
this.technical = false,
|
||||||
this.hideOnMedium = false,
|
this.hideOnMedium = false,
|
||||||
|
this.minWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The column header text.
|
/// The column header text.
|
||||||
@@ -33,6 +34,11 @@ class TasQColumn<T> {
|
|||||||
/// Use this for lower-priority columns (e.g. Timestamp, Status) that
|
/// Use this for lower-priority columns (e.g. Timestamp, Status) that
|
||||||
/// would otherwise force a horizontal scrollbar on tablet viewports.
|
/// would otherwise force a horizontal scrollbar on tablet viewports.
|
||||||
final bool hideOnMedium;
|
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].
|
/// Builds a mobile tile for [TasQAdaptiveList].
|
||||||
@@ -380,9 +386,13 @@ class TasQAdaptiveList<T> extends StatelessWidget {
|
|||||||
const double colSpacing = 20.0;
|
const double colSpacing = 20.0;
|
||||||
final actionsColumnCount = rowActions == null ? 0 : 1;
|
final actionsColumnCount = rowActions == null ? 0 : 1;
|
||||||
final totalCols = visibleColumns.length + actionsColumnCount;
|
final totalCols = visibleColumns.length + actionsColumnCount;
|
||||||
|
final columnsMinWidth = visibleColumns.fold<double>(
|
||||||
|
0.0,
|
||||||
|
(sum, c) => sum + (c.minWidth ?? colMinW),
|
||||||
|
);
|
||||||
final minColumnsWidth =
|
final minColumnsWidth =
|
||||||
hMarginTotal +
|
hMarginTotal +
|
||||||
(visibleColumns.length * colMinW) +
|
columnsMinWidth +
|
||||||
(actionsColumnCount * actMinW) +
|
(actionsColumnCount * actMinW) +
|
||||||
(math.max(0, totalCols - 1) * colSpacing);
|
(math.max(0, totalCols - 1) * colSpacing);
|
||||||
final tableWidth = math.max(contentWidth, minColumnsWidth);
|
final tableWidth = math.max(contentWidth, minColumnsWidth);
|
||||||
|
|||||||
Reference in New Issue
Block a user