From d3a8a5c6dab402384e56e292cfc286b3ca2c477f Mon Sep 17 00:00:00 2001 From: Mikkeli Matlock Date: Sat, 23 Aug 2025 01:07:04 +0900 Subject: [PATCH] Implement centralized color scheme system with Sum of 6 greying MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- lib/config/constants/_constants.md | 48 ++++ lib/config/constants/scoring_colors.dart | 213 ++++++++++++++++++ .../widgets/scoring_grid/scoring_cell.dart | 32 +-- .../widgets/scoring_grid/scoring_grid.dart | 34 ++- .../widgets/scoring_grid/scoring_keypad.dart | 56 +---- 5 files changed, 301 insertions(+), 82 deletions(-) create mode 100644 lib/config/constants/_constants.md create mode 100644 lib/config/constants/scoring_colors.dart diff --git a/lib/config/constants/_constants.md b/lib/config/constants/_constants.md new file mode 100644 index 0000000..e3beb80 --- /dev/null +++ b/lib/config/constants/_constants.md @@ -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; +``` \ No newline at end of file diff --git a/lib/config/constants/scoring_colors.dart b/lib/config/constants/scoring_colors.dart new file mode 100644 index 0000000..521fefd --- /dev/null +++ b/lib/config/constants/scoring_colors.dart @@ -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(...); +} \ No newline at end of file diff --git a/lib/presentation/widgets/scoring_grid/scoring_cell.dart b/lib/presentation/widgets/scoring_grid/scoring_cell.dart index 4095dd3..1586473 100644 --- a/lib/presentation/widgets/scoring_grid/scoring_cell.dart +++ b/lib/presentation/widgets/scoring_grid/scoring_cell.dart @@ -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, ), ), ), diff --git a/lib/presentation/widgets/scoring_grid/scoring_grid.dart b/lib/presentation/widgets/scoring_grid/scoring_grid.dart index 1d13348..63181bd 100644 --- a/lib/presentation/widgets/scoring_grid/scoring_grid.dart +++ b/lib/presentation/widgets/scoring_grid/scoring_grid.dart @@ -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> 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, + ), ), ), ), diff --git a/lib/presentation/widgets/scoring_grid/scoring_keypad.dart b/lib/presentation/widgets/scoring_grid/scoring_keypad.dart index 698394e..a22d62e 100644 --- a/lib/presentation/widgets/scoring_grid/scoring_keypad.dart +++ b/lib/presentation/widgets/scoring_grid/scoring_keypad.dart @@ -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, ), ), );