115 lines
3.5 KiB
Dart
115 lines
3.5 KiB
Dart
import 'package:flutter/material.dart';
|
||
import '../../../../core/theme/app_typography.dart';
|
||
import '../../../../core/theme/theme_extensions.dart';
|
||
|
||
/// Reusable widget rendering a deterministic 1–2 character monogram badge
|
||
/// derived from a Club's name. Follows automotive cockpit design guidelines.
|
||
class ClubMonogram extends StatelessWidget {
|
||
final String name;
|
||
final double size;
|
||
final double? fontSize;
|
||
final Color? backgroundColor;
|
||
final Color? textColor;
|
||
|
||
const ClubMonogram({
|
||
super.key,
|
||
required this.name,
|
||
this.size = 44.0,
|
||
this.fontSize,
|
||
this.backgroundColor,
|
||
this.textColor,
|
||
});
|
||
|
||
/// Computes a deterministic 1–2 character monogram from a raw club name.
|
||
/// Unicode-safe using Characters to handle grapheme clusters, emojis,
|
||
/// leading punctuation, non-Latin scripts, and single characters.
|
||
static String getMonogram(String rawName) {
|
||
final trimmed = rawName.trim();
|
||
if (trimmed.isEmpty) return 'C';
|
||
|
||
final words = trimmed
|
||
.split(RegExp(r'\s+'))
|
||
.where((w) => w.isNotEmpty)
|
||
.toList();
|
||
if (words.isEmpty) return 'C';
|
||
|
||
String cleanWord(String word) {
|
||
final chars = word.characters.toList();
|
||
int start = 0;
|
||
while (start < chars.length) {
|
||
final ch = chars[start];
|
||
if (_isLeadingPunctuation(ch)) {
|
||
start++;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
if (start >= chars.length) {
|
||
return word;
|
||
}
|
||
return chars.sublist(start).join();
|
||
}
|
||
|
||
if (words.length == 1) {
|
||
final cleaned = cleanWord(words.first);
|
||
final chars = cleaned.characters.take(2).toList();
|
||
if (chars.isEmpty) return 'C';
|
||
return chars.join().toUpperCase();
|
||
} else {
|
||
final cleanedWords = words.map(cleanWord).toList();
|
||
final firstGrapheme = cleanedWords[0].characters.isNotEmpty
|
||
? cleanedWords[0].characters.first
|
||
: '';
|
||
final secondGrapheme = cleanedWords[1].characters.isNotEmpty
|
||
? cleanedWords[1].characters.first
|
||
: '';
|
||
final combined = '$firstGrapheme$secondGrapheme'.trim();
|
||
if (combined.isEmpty) return 'C';
|
||
return combined.toUpperCase();
|
||
}
|
||
}
|
||
|
||
static bool _isLeadingPunctuation(String char) {
|
||
const punct = {
|
||
'-', '_', '.', ',', '!', '?', '@', '#', r'$', '%', '^', '&', '*',
|
||
'(', ')', '[', ']', '{', '}', '<', '>', '/', r'\', '|', '~', '`',
|
||
'+', '=', ':', ';', '"', '\'', '«', '»', '“', '”', '‘', '’', '•',
|
||
};
|
||
return punct.contains(char);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final colors = context.motorColors;
|
||
final monogram = getMonogram(name);
|
||
final effectiveFontSize = fontSize ?? (size * 0.40);
|
||
|
||
return Semantics(
|
||
label: 'Club monogram for $name',
|
||
child: Container(
|
||
width: size,
|
||
height: size,
|
||
decoration: BoxDecoration(
|
||
color: backgroundColor ?? colors.surfaceContainerHighest,
|
||
borderRadius: BorderRadius.circular(size * 0.28),
|
||
border: Border.all(
|
||
color: colors.outlineVariant.withValues(alpha: 0.6),
|
||
width: 1.0,
|
||
),
|
||
),
|
||
alignment: Alignment.center,
|
||
child: Text(
|
||
monogram,
|
||
style: TextStyle(
|
||
fontFamily: AppTypography.fontHeadline,
|
||
fontSize: effectiveFontSize,
|
||
fontWeight: FontWeight.w800,
|
||
letterSpacing: 0.5,
|
||
color: textColor ?? colors.cyberCyan,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|