Implement centralized color scheme system with Sum of 6 greying

- Add ScoringColorScheme class with explicit text colors per scheme
- Implement traditional (vibrant) and lowSaturation (muted) color schemes
- Add disabled state colors for greying out inactive elements
- Refactor scoring_cell.dart and scoring_keypad.dart to use centralized colors
- Grey out Sum of 6 column when in 3-arrow end mode while maintaining layout
- Add comprehensive color scheme documentation and usage examples

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mikkeli Matlock
2025-08-23 01:07:04 +09:00
parent e9c8bc48b0
commit d3a8a5c6da
5 changed files with 301 additions and 82 deletions
+48
View File
@@ -0,0 +1,48 @@
# Constants Directory
This directory contains application-wide constants and configuration.
## Files
- **scoring_colors.dart**: Centralized color scheme system for all scoring-related UI components
- Provides multiple color schemes: `traditional` (vibrant) and `lowSaturation` (muted)
- Follows archery ring color conventions (gold, red, blue, black, white)
- Explicit text colors defined per scheme for intentional design control
- Disabled state colors for greying out inactive UI elements
- Single source of truth for all score-related colors with easy scheme switching
- Architecture ready for dark themes, high contrast, and colorblind-friendly schemes
## Future Expansion
Planned constants files:
- `app_constants.dart`: General app configuration (timeouts, limits, defaults)
- `layout_constants.dart`: UI spacing, sizing, and layout specifications
- `scoring_rules.dart`: Archery scoring rules and validation constants
- `export_constants.dart`: File formats, templates, and export configurations
## Usage
```dart
import 'package:archery_bookkeeper/config/constants/scoring_colors.dart';
// Get background color for any score (uses current active scheme)
Color bgColor = ScoringColors.getScoreBackgroundColor('X');
// Get appropriate text color with automatic contrast calculation
Color textColor = ScoringColors.getScoreTextColor('X');
// Check if score should be bold
bool isBold = ScoringColors.shouldUseBoldText('10');
// Access available schemes
ScoringColorScheme traditional = ScoringColors.availableTraditional;
ScoringColorScheme lowSat = ScoringColors.availableLowSaturation;
// Get disabled state colors
Color disabledBg = ScoringColors.getDisabledBackground();
Color disabledText = ScoringColors.getDisabledText();
Color disabledBorder = ScoringColors.getDisabledBorder();
// Switch schemes by changing _currentScheme in scoring_colors.dart:
// static const ScoringColorScheme _currentScheme = lowSaturation;
```
+213
View File
@@ -0,0 +1,213 @@
import 'package:flutter/material.dart';
/// Represents a complete color scheme for archery scoring components.
class ScoringColorScheme {
// Background colors
final Color goldRing; // X, 10, 9
final Color redRing; // 8, 7
final Color blueRing; // 6, 5
final Color blackRing; // 4, 3
final Color whiteRing; // 2, 1
final Color miss; // M
final Color clear; // CLEAR button
// Text colors
final Color goldText; // Text on gold ring
final Color redText; // Text on red ring
final Color blueText; // Text on blue ring
final Color blackText; // Text on black ring
final Color whiteText; // Text on white ring
final Color missText; // Text on miss
final Color clearText; // Text on CLEAR button
// Border colors
final Color selectedBorder;
final Color defaultBorder;
// Disabled/greyed-out colors for inactive elements
final Color disabledBackground;
final Color disabledText;
final Color disabledBorder;
const ScoringColorScheme({
required this.goldRing,
required this.redRing,
required this.blueRing,
required this.blackRing,
required this.whiteRing,
required this.miss,
required this.clear,
required this.goldText,
required this.redText,
required this.blueText,
required this.blackText,
required this.whiteText,
required this.missText,
required this.clearText,
required this.selectedBorder,
required this.defaultBorder,
required this.disabledBackground,
required this.disabledText,
required this.disabledBorder,
});
}
/// Centralized scoring color configuration for archery scoring components.
///
/// This class provides consistent color schemes across all scoring-related
/// widgets including scoring cells, keypad buttons, and visual indicators.
/// Supports multiple color schemes and future extensibility.
class ScoringColors {
ScoringColors._();
// Available color schemes
/// Traditional vibrant archery colors - original scheme
static const ScoringColorScheme traditional = ScoringColorScheme(
goldRing: Color(0xEEffeb50), // Bright yellow-gold
redRing: Color(0xEEed3f36), // Vibrant red
blueRing: Color(0xEE01afd8), // Bright blue
blackRing: Color(0xEE222222), // Dark grey
whiteRing: Color(0xEEFFFFFF), // Pure white
miss: Color(0xEEcccccc), // Light grey
clear: Color(0xEEff8c42), // Orange
goldText: Colors.black, // Black text on bright gold
redText: Colors.white, // White text on vibrant red
blueText: Colors.white, // White text on bright blue
blackText: Colors.white, // White text on dark grey
whiteText: Colors.black, // Black text on white background
missText: Colors.black, // Black text on light grey
clearText: Colors.white, // White text on orange
selectedBorder: Colors.blue,
defaultBorder: Color(0xEE999999),
disabledBackground: Color(0xEEf0f0f0), // Light grey for disabled elements
disabledText: Color(0xEEbbbbbb), // Medium grey for disabled text
disabledBorder: Color(0xEEdddddd), // Very light grey for disabled borders
);
/// Low saturation scheme - muted colors for reduced eye strain
static const ScoringColorScheme lowSaturation = ScoringColorScheme(
goldRing: Color(0xEEd4c5a9), // Muted gold/beige
redRing: Color(0xEEb85450), // Muted red/brown
blueRing: Color(0xEE5a8aa8), // Muted blue/grey
blackRing: Color(0xEE444444), // Lighter grey than traditional
whiteRing: Color(0xEEf5f5f5), // Off-white
miss: Color(0xEEbbbbbb), // Medium grey
clear: Color(0xEEc49969), // Muted orange/brown
goldText: Color(0xEE333333), // Dark grey on muted beige
redText: Color(0xEEf0f0f0), // Light grey on muted red
blueText: Color(0xEEf0f0f0), // Light grey on muted blue
blackText: Color(0xEEf0f0f0), // Light grey on darker grey
whiteText: Color(0xEE333333), // Dark grey on off-white
missText: Color(0xEE333333), // Dark grey on medium grey
clearText: Color(0xEEf0f0f0), // Light grey on muted orange
selectedBorder: Color(0xEE6699bb), // Muted blue border
defaultBorder: Color(0xEE888888), // Darker default border
disabledBackground: Color(0xEEe8e8e8), // Muted light grey for disabled elements
disabledText: Color(0xEEaaaaaa), // Muted medium grey for disabled text
disabledBorder: Color(0xEEcccccc), // Muted light grey for disabled borders
);
// Current active scheme - change this to switch schemes globally
static const ScoringColorScheme _currentScheme = traditional;
/// Returns the background color for a given score value.
/// Used by both scoring cells and keypad buttons for consistency.
static Color getScoreBackgroundColor(String score) {
if (score.isEmpty) return Colors.transparent;
switch (score.toUpperCase()) {
case 'X':
case '10':
case '9':
return _currentScheme.goldRing;
case '8':
case '7':
return _currentScheme.redRing;
case '6':
case '5':
return _currentScheme.blueRing;
case '4':
case '3':
return _currentScheme.blackRing;
case '2':
case '1':
return _currentScheme.whiteRing;
case 'CLEAR':
return _currentScheme.clear;
case 'M':
return _currentScheme.miss;
default:
return _currentScheme.miss;
}
}
/// Returns the appropriate text color for given score background.
/// Uses explicit text colors defined in the current color scheme.
static Color getScoreTextColor(String score) {
if (score.isEmpty) return _currentScheme.whiteText; // Use scheme's default text color
switch (score.toUpperCase()) {
case 'X':
case '10':
case '9':
return _currentScheme.goldText;
case '8':
case '7':
return _currentScheme.redText;
case '6':
case '5':
return _currentScheme.blueText;
case '4':
case '3':
return _currentScheme.blackText;
case '2':
case '1':
return _currentScheme.whiteText;
case 'CLEAR':
return _currentScheme.clearText;
case 'M':
return _currentScheme.missText;
default:
return _currentScheme.missText;
}
}
/// Returns whether the score should be displayed with bold text.
/// X and 10 scores are emphasized as highest values.
static bool shouldUseBoldText(String score) {
if (score.isEmpty) return false;
return score.toUpperCase() == 'X' || score == '10';
}
/// Returns border color for scoring cells based on selection state.
static Color getBorderColor(bool isSelected) {
return isSelected ? _currentScheme.selectedBorder : _currentScheme.defaultBorder;
}
/// Returns border width for scoring cells based on selection state.
static double getBorderWidth(bool isSelected) {
return isSelected ? 2.0 : 1.0;
}
/// Returns colors for disabled/greyed-out elements
static Color getDisabledBackground() => _currentScheme.disabledBackground;
static Color getDisabledText() => _currentScheme.disabledText;
static Color getDisabledBorder() => _currentScheme.disabledBorder;
/// Returns the currently active color scheme
static ScoringColorScheme get currentScheme => _currentScheme;
/// Convenience method to switch to low saturation scheme
/// To use: change _currentScheme = lowSaturation at the top of the class
static ScoringColorScheme get availableLowSaturation => lowSaturation;
/// Convenience method to switch to traditional scheme
/// To use: change _currentScheme = traditional at the top of the class
static ScoringColorScheme get availableTraditional => traditional;
// Future expansion: Additional color schemes
// static const ScoringColorScheme highContrast = ScoringColorScheme(...);
// static const ScoringColorScheme colorBlindFriendly = ScoringColorScheme(...);
// static const ScoringColorScheme darkTheme = ScoringColorScheme(...);
}
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../config/constants/scoring_colors.dart';
class ScoringCell extends StatelessWidget {
final String score;
@@ -12,28 +13,6 @@ class ScoringCell extends StatelessWidget {
required this.onTap,
});
Color _getScoreColor(String score) {
switch (score) {
case 'X':
case '10':
case '9':
return Color(0xEEffeb50);
case '8':
case '7':
return Color(0xEEed3f36);
case '6':
case '5':
return Color(0xEE01afd8);
case '4':
case '3':
return Color(0xEE222222);
case '2':
case '1':
return Color(0xEEFFFFFF);
default:
return Colors.transparent;
}
}
@override
Widget build(BuildContext context) {
@@ -42,10 +21,10 @@ class ScoringCell extends StatelessWidget {
child: Container(
margin: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: _getScoreColor(score),
color: ScoringColors.getScoreBackgroundColor(score),
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey.shade400,
width: isSelected ? 2 : 1,
color: ScoringColors.getBorderColor(isSelected),
width: ScoringColors.getBorderWidth(isSelected),
),
borderRadius: BorderRadius.circular(4),
),
@@ -54,7 +33,8 @@ class ScoringCell extends StatelessWidget {
score,
style: TextStyle(
fontSize: 16,
fontWeight: score == 'X' || score == '10' ? FontWeight.bold : FontWeight.normal,
color: ScoringColors.getScoreTextColor(score),
fontWeight: ScoringColors.shouldUseBoldText(score) ? FontWeight.bold : FontWeight.normal,
),
),
),
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'scoring_cell.dart';
import '../../../config/constants/scoring_colors.dart';
class ScoringGrid extends StatelessWidget {
final List<List<String>> scores;
@@ -94,7 +95,19 @@ class ScoringGrid extends StatelessWidget {
...List.generate(arrowsPerEnd, (index) =>
Expanded(child: Center(child: Text('#${index + 1}', style: const TextStyle(fontWeight: FontWeight.bold))))),
const SizedBox(width: 70, child: Center(child: Text('Sum of 3', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)))),
const SizedBox(width: 70, child: Center(child: Text('Sum of 6', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)))),
SizedBox(
width: 70,
child: Center(
child: Text(
'Sum of 6',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: arrowsPerEnd == 3 ? ScoringColors.getDisabledText() : null,
),
),
),
),
const SizedBox(width: 80, child: Center(child: Text('Accumulative', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)))),
],
),
@@ -170,15 +183,26 @@ class ScoringGrid extends StatelessWidget {
width: 70,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: row % 2 == 1 ? Colors.yellow.shade100 : Colors.transparent,
color: arrowsPerEnd == 3
? ScoringColors.getDisabledBackground()
: (row % 2 == 1 ? Colors.yellow.shade100 : Colors.transparent),
border: Border(
left: BorderSide(color: Colors.grey.shade300),
left: BorderSide(
color: arrowsPerEnd == 3
? ScoringColors.getDisabledBorder()
: Colors.grey.shade300,
),
),
),
child: Center(
child: Text(
sumOfSix > 0 ? sumOfSix.toString() : '',
style: const TextStyle(fontWeight: FontWeight.bold),
(arrowsPerEnd == 3 || sumOfSix == 0) ? '' : sumOfSix.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: arrowsPerEnd == 3
? ScoringColors.getDisabledText()
: null,
),
),
),
),
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../config/constants/scoring_colors.dart';
class ScoringKeypad extends StatelessWidget {
final Function(String) onScoreInput;
@@ -52,56 +53,9 @@ class ScoringKeypad extends StatelessWidget {
}
Widget _buildKeypadButton(BuildContext context, String score, {bool isWide = false}) {
Color buttonColor;
Color textColor = Colors.black;
// Color scheme matching your design
// TODO: Update this to match `scoring_cell.dart`
switch (score) {
case 'X':
buttonColor = Colors.amber.shade300;
break;
case '10':
buttonColor = Colors.amber.shade300;
break;
case '9':
buttonColor = Colors.yellow.shade300;
break;
case '8':
buttonColor = Colors.red.shade400;
textColor = Colors.white;
break;
case '7':
buttonColor = Colors.red.shade400;
textColor = Colors.white;
break;
case '6':
buttonColor = Colors.blue.shade400;
textColor = Colors.white;
break;
case '5':
buttonColor = Colors.blue.shade400;
textColor = Colors.white;
break;
case '4':
buttonColor = Colors.black87;
textColor = Colors.white;
break;
case '3':
buttonColor = Colors.black87;
textColor = Colors.white;
break;
case 'M':
buttonColor = Colors.grey.shade400;
break;
case 'CLEAR':
buttonColor = Colors.orange.shade400;
textColor = Colors.white;
break;
default:
buttonColor = Colors.grey.shade300;
break;
}
// Use centralized color configuration for consistency
final buttonColor = ScoringColors.getScoreBackgroundColor(score);
final textColor = ScoringColors.getScoreTextColor(score);
return ElevatedButton(
onPressed: () => onScoreInput(score),
@@ -118,7 +72,7 @@ class ScoringKeypad extends StatelessWidget {
score,
style: TextStyle(
fontSize: isWide ? 16 : (score.length > 1 ? 14 : 18),
fontWeight: FontWeight.bold,
fontWeight: ScoringColors.shouldUseBoldText(score) ? FontWeight.bold : FontWeight.w600,
),
),
);