From a 7-agent design workflow (3 lenses judged + synthesised). Shipped the high-impact/low-risk tier: - message bubbles: self #1265ED (WCAG AA pass), incoming #1C2452 + hairline - deterministic per-user avatar colours (8-hue brand palette) - room tile: brighter unread preview, compact density, aligned timestamp - FAB -> New message (thumb zone), explore -> AppBar - login + welcome brand gradient, card outlines - input maxLines cap, bubble max-width viewport fraction, AppBar hairline All verified on production build in headless Chromium at mobile size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
71 lines
2.1 KiB
Dart
71 lines
2.1 KiB
Dart
// Version: 1.1.0 | Created: 2026-04-02 | Updated: 2026-07-04
|
|
// Shared avatar widget used throughout the app. Displays a cached network
|
|
// image when an HTTP avatar URL is available, or falls back to a circle
|
|
// coloured deterministically from the display name (so each person gets a
|
|
// stable, distinct disc) with the first letter of the display name.
|
|
//
|
|
// The [avatarUrl] MUST be a resolved HTTP URL — never pass an mxc:// URI.
|
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
class MatrixAvatar extends StatelessWidget {
|
|
const MatrixAvatar({
|
|
super.key,
|
|
required this.name,
|
|
this.avatarUrl,
|
|
this.radius = 20,
|
|
});
|
|
|
|
/// Display name used for the initials fallback.
|
|
final String name;
|
|
|
|
/// Resolved HTTP URL for the avatar image. Must NOT be an mxc:// URI.
|
|
final String? avatarUrl;
|
|
|
|
/// Radius of the [CircleAvatar].
|
|
final double radius;
|
|
|
|
/// Brand-compatible hues for initials avatars. All dark enough for white
|
|
/// initials to remain legible.
|
|
static const List<Color> _palette = [
|
|
Color(0xFF1265ED), // brand blue
|
|
Color(0xFF2E7DE0), // sky
|
|
Color(0xFF1E88A8), // teal
|
|
Color(0xFF3949AB), // indigo
|
|
Color(0xFF6A4CC0), // violet
|
|
Color(0xFF00897B), // green-teal
|
|
Color(0xFF5C6BC0), // periwinkle
|
|
Color(0xFFB1631F), // warm amber
|
|
];
|
|
|
|
Color get _discColour => _palette[name.hashCode.abs() % _palette.length];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final initials = name.isNotEmpty ? name[0].toUpperCase() : '?';
|
|
|
|
if (avatarUrl != null) {
|
|
return CircleAvatar(
|
|
radius: radius,
|
|
backgroundImage: CachedNetworkImageProvider(avatarUrl!),
|
|
backgroundColor: theme.colorScheme.surfaceContainerHighest,
|
|
);
|
|
}
|
|
|
|
return CircleAvatar(
|
|
radius: radius,
|
|
backgroundColor: _discColour,
|
|
child: Text(
|
|
initials,
|
|
style: TextStyle(
|
|
fontSize: radius * 0.7,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|