PetSphere Design System Audit & Multi-Platform Redesign
Date : April 29, 2026
Current Theme : Amber Whisker (warm, pet-friendly palette)
Scope : Audit existing system; design responsive tokens for Android, iOS, Web
Deliverables : Design tokens (.dart), component library, responsive breakpoints, accessibility specs
Part 1: Current Design System Audit
Dimension
Status
Score
Notes
Color System
Incomplete
6/10
Primary, secondary defined; missing semantic colors (success, warning, error)
Typography
Partial
7/10
2 fonts (Playfair, DM Sans); scale undefined; missing text variants
Spacing
Ad-hoc
4/10
No scale; hardcoded values throughout (8px, 16px, 24px implicit)
Components
Scattered
5/10
Material 3 base; custom variants undocumented; states incomplete
Accessibility
Minimal
3/10
No WCAG checklist; color contrast unverified; no a11y guidelines
Responsive
Basic
5/10
MediaQuery checks for mobile/desktop; no breakpoint system
Documentation
None
0/10
No component library; no token reference; tribal knowledge in code
Version Control
None
0/10
No versioning; breaking changes not tracked
Overall Score
—
30/70
Functional but fragile; difficult to scale and maintain
Current "Amber Whisker" Color Palette
// lib/theme/app_theme.dart (as documented in CLAUDE.md)
class AppTheme {
// Core colors
static const Color primaryAccent = Color (0xFFD4845A ); // Warm amber #D4845A
static const Color secondaryAccent = Color (0xFF4A7C59 ); // Sage green #4A7C59
static const Color background = Color (0xFF0F0E0C ); // Near-black #0F0E0C
static const Color surface = Color (0xFF1A1814 ); // Dark charcoal #1A1814
static const Color textPrimary = Color (0xFFF2EDE4 ); // Off-white #F2EDE4
static const Color textSecondary = Color (0xFFB8B0A4 ); // Warm gray #B8B0A4
// Missing (no current definition)
// - Success (green) for confirmations, badges
// - Warning (orange/yellow) for alerts, care overdue
// - Error (red) for validation failures
// - Neutral grays for dividers, disabled states
// - Overlay/shadow colors for depth
}
Issue
Components Affected
Risk
Recommendation
No semantic colors
Error states, success feedback, warnings
High
Add success, warning, error colors; tie to sentiment
Hardcoded spacing
All components (padding, margins, gaps)
High
Define 8-point spacing scale (8, 12, 16, 20, 24, 32, 40, 48)
Missing text variants
Health metrics, post timestamps, captions
Medium
Define 5 text styles (hero, title, body, small, caption)
No component states
Buttons, inputs, cards
Medium
Document: default, hover, active, disabled, loading, error
Accessibility blind spot
Color contrast, focus states, motion
Critical
Audit WCAG 2.1 AA compliance; add a11y guidelines
No responsive breakpoints
Mobile/web layouts
Medium
Define: mobile (<480px), tablet (480–840px), desktop (>840px)
Tribal knowledge in code
App initialization, theme application
Medium
Document in DESIGN_TOKENS.md; move to central registry
Component Coverage Assessment
Existing Components (Material 3 base)
Component
Variants
States
Accessible
Score
Button
Primary, secondary
Default, hover, active, disabled
Partial (missing focus indicator)
7/10
TextField
Default, outlined
Default, focused, filled, error
Partial (label accessible)
6/10
Card
Elevated, outlined
—
Yes
7/10
Chip
Outlined, filled
Default, selected
Yes
6/10
AppBar
—
—
Partial
5/10
BottomNav
—
Selected state
Partial (label missing)
4/10
Dialog
—
—
Partial (focus trap missing)
5/10
Snackbar
Action variant
—
Partial
5/10
Platform
Current State
Issues
Android
Functional
Safe area padding missing on notch devices; dense bottom nav spacing inconsistent
iOS
Functional
Safe area properly handled; StatusBar color inconsistent with theme
Web
Limited
Breakpoints too aggressive (mobile layout on tablet desktop); no keyboard/focus indicators
Part 2: Multi-Platform Responsive Design System
New Color System (Expanded Palette)
class AppColors {
// Primary Brand (Warm Amber)
static const Color primary50 = Color (0xFFFEF5F0 ); // Lightest
static const Color primary100 = Color (0xFFFCEADE );
static const Color primary200 = Color (0xFFF5D5B8 );
static const Color primary300 = Color (0xFFEDC092 );
static const Color primary400 = Color (0xFFE2A96C );
static const Color primary500 = Color (0xFFD4845A ); // Brand primary (current)
static const Color primary600 = Color (0xFFC17047 );
static const Color primary700 = Color (0xFFA95C36 );
static const Color primary800 = Color (0xFF904929 );
static const Color primary900 = Color (0xFF7A3B1F );
static const Color primary950 = Color (0xFF4A2311 ); // Darkest
// Secondary Brand (Sage Green)
static const Color secondary50 = Color (0xFFF0F6F0 );
static const Color secondary100 = Color (0xFFDCEDDC );
static const Color secondary200 = Color (0xFFBBDCC0 );
static const Color secondary300 = Color (0xFF96CBA3 );
static const Color secondary400 = Color (0xFF6BAB7C );
static const Color secondary500 = Color (0xFF4A7C59 ); // Brand secondary (current)
static const Color secondary600 = Color (0xFF3D644A );
static const Color secondary700 = Color (0xFF314C39 );
static const Color secondary800 = Color (0xFF273A2D );
static const Color secondary900 = Color (0xFF1F2C22 );
// Semantic Colors
static const Color success50 = Color (0xFFEDF9F0 );
static const Color success500 = Color (0xFF10B981 );
static const Color success600 = Color (0xFF059669 );
static const Color success900 = Color (0xFF065F46 );
static const Color warning50 = Color (0xFFFEF7E0 );
static const Color warning500 = Color (0xFFF59E0B );
static const Color warning600 = Color (0xFFD97706 );
static const Color warning900 = Color (0xFF78350F );
static const Color error50 = Color (0xFFFEF2F2 );
static const Color error500 = Color (0xFFEF4444 );
static const Color error600 = Color (0xFFDC2626 );
static const Color error900 = Color (0xFF7F1D1D );
// Neutral Grays (dark mode primary)
static const Color neutral50 = Color (0xFFFAFAFA );
static const Color neutral100 = Color (0xFFF3F3F3 );
static const Color neutral200 = Color (0xFFE5E5E5 );
static const Color neutral300 = Color (0xFFD4D4D4 );
static const Color neutral400 = Color (0xFFA3A3A3 );
static const Color neutral500 = Color (0xFF737373 );
static const Color neutral600 = Color (0xFF525252 );
static const Color neutral700 = Color (0xFF404040 );
static const Color neutral800 = Color (0xFF262626 );
static const Color neutral900 = Color (0xFF171717 );
// Dark Mode Surfaces (current background/surface)
static const Color darkBackground = Color (0xFF0F0E0C ); // Primary surface
static const Color darkSurface = Color (0xFF1A1814 ); // Secondary surface
static const Color darkSurfaceAlt = Color (0xFF2A271C ); // Tertiary (cards, elevated)
// Text Colors
static const Color textPrimary = Color (0xFFF2EDE4 );
static const Color textSecondary = Color (0xFFB8B0A4 );
static const Color textTertiary = Color (0xFF9A8F83 );
static const Color textDisabled = Color (0xFF6B6258 );
}
extension ColorSemantics on ThemeData {
Color get successColor => AppColors .success500;
Color get warningColor => AppColors .warning500;
Color get errorColor => AppColors .error500;
Color get dividerColor => AppColors .neutral700;
Color get disabledColor => AppColors .neutral600;
}
Font Scale (8-point baseline)
class AppTypography {
// Playfair Display (headlines, hero text)
static const TextStyle displayLarge = TextStyle (
fontFamily: 'Playfair Display' ,
fontSize: 48 , // 6 × 8px
fontWeight: FontWeight .w700,
height: 1.2 , // 57.6px total
letterSpacing: - 0.5 ,
);
static const TextStyle displayMedium = TextStyle (
fontFamily: 'Playfair Display' ,
fontSize: 40 , // 5 × 8px
fontWeight: FontWeight .w700,
height: 1.25 , // 50px total
letterSpacing: - 0.3 ,
);
static const TextStyle displaySmall = TextStyle (
fontFamily: 'Playfair Display' ,
fontSize: 32 , // 4 × 8px
fontWeight: FontWeight .w700,
height: 1.3 ,
letterSpacing: 0 ,
);
// DM Sans (body, UI, labels)
static const TextStyle headlineLarge = TextStyle (
fontFamily: 'DM Sans' ,
fontSize: 28 , // 3.5 × 8px
fontWeight: FontWeight .w700,
height: 1.35 , // 37.8px total
letterSpacing: 0 ,
);
static const TextStyle headlineMedium = TextStyle (
fontFamily: 'DM Sans' ,
fontSize: 20 , // 2.5 × 8px
fontWeight: FontWeight .w600,
height: 1.4 , // 28px total
letterSpacing: 0.2 ,
);
static const TextStyle headlineSmall = TextStyle (
fontFamily: 'DM Sans' ,
fontSize: 16 , // 2 × 8px
fontWeight: FontWeight .w600,
height: 1.5 , // 24px total
letterSpacing: 0.2 ,
);
static const TextStyle bodyLarge = TextStyle (
fontFamily: 'DM Sans' ,
fontSize: 16 ,
fontWeight: FontWeight .w500,
height: 1.5 , // 24px total
letterSpacing: 0.2 ,
);
static const TextStyle bodyMedium = TextStyle (
fontFamily: 'DM Sans' ,
fontSize: 14 , // 1.75 × 8px
fontWeight: FontWeight .w500,
height: 1.57 , // 22px total
letterSpacing: 0.25 ,
);
static const TextStyle bodySmall = TextStyle (
fontFamily: 'DM Sans' ,
fontSize: 12 , // 1.5 × 8px
fontWeight: FontWeight .w500,
height: 1.67 , // 20px total
letterSpacing: 0.4 ,
);
static const TextStyle labelLarge = TextStyle (
fontFamily: 'DM Sans' ,
fontSize: 14 ,
fontWeight: FontWeight .w600,
height: 1.43 , // 20px total
letterSpacing: 0.1 ,
);
static const TextStyle labelMedium = TextStyle (
fontFamily: 'DM Sans' ,
fontSize: 12 ,
fontWeight: FontWeight .w500,
height: 1.33 , // 16px total
letterSpacing: 0.5 ,
);
static const TextStyle labelSmall = TextStyle (
fontFamily: 'DM Sans' ,
fontSize: 11 , // 1.375 × 8px
fontWeight: FontWeight .w500,
height: 1.45 , // 15.95px total
letterSpacing: 0.5 ,
);
}
Spacing Scale (8-point grid)
class AppSpacing {
static const double xs = 4 ; // 0.5 × 8px (micro: inline padding)
static const double sm = 8 ; // 1 × 8px (small: default gutters)
static const double md = 12 ; // 1.5 × 8px (medium: section gaps)
static const double lg = 16 ; // 2 × 8px (large: component spacing)
static const double xl = 20 ; // 2.5 × 8px (extra large)
static const double xxl = 24 ; // 3 × 8px (double extra large)
static const double xxxl = 32 ; // 4 × 8px (triple extra large)
static const double huge = 40 ; // 5 × 8px (huge: major sections)
static const double massive = 48 ; // 6 × 8px (massive: screen sections)
}
Button : Padding (lg horizontal, md vertical) = (16, 12)
Card : Padding xxl = 24 on all sides
List item : Margin md between items = 12
Section divider : Gap xxl = 24 between sections
Form field : Gap between label + input: sm = 8
class AppBreakpoints {
// Mobile-first approach
static const double mobile = 0 ; // 0–479px (phones)
static const double tablet = 480 ; // 480–839px (tablets, large phones)
static const double desktop = 840 ; // 840px+ (desktops, tablets in landscape)
// Helper methods
static bool isMobile (double width) => width < tablet;
static bool isTablet (double width) => width >= tablet && width < desktop;
static bool isDesktop (double width) => width >= desktop;
// Safe area insets (platform-specific)
static EdgeInsets safeAreaPadding (MediaQueryData mediaQuery) {
return EdgeInsets .only (
top: mediaQuery.padding.top, // iOS notch, Android status bar
bottom: mediaQuery.padding.bottom, // iOS home indicator
left: mediaQuery.padding.left, // Landscape safe area
right: mediaQuery.padding.right,
);
}
}
Responsive Behavior by Breakpoint
Element
Mobile (<480)
Tablet (480–840)
Desktop (>840)
Bottom Nav
Full width, 56px height
Full width, 64px height
Hidden (side nav)
Page Padding
12px
16px
24px
Column Count
1
2
3
Card Width
Full width
360px
400px
Font Scale
100%
105%
110%
Pet Grid
1 col (stacked)
2 col
3–4 col
class AppButtonVariant {
// Variants: primary, secondary, tertiary, outlined
// Sizes: small, medium, large
// States: default, hover, active, disabled, loading
static EdgeInsets _paddingForSize (ButtonSize size) {
switch (size) {
case ButtonSize .small:
return EdgeInsets .symmetric (horizontal: 12 , vertical: 8 );
case ButtonSize .medium:
return EdgeInsets .symmetric (horizontal: 16 , vertical: 12 );
case ButtonSize .large:
return EdgeInsets .symmetric (horizontal: 20 , vertical: 16 );
}
}
static double _elevationForState (ButtonState state) {
switch (state) {
case ButtonState .default :
return 0 ;
case ButtonState .hover:
return 2 ;
case ButtonState .active:
return 4 ;
case ButtonState .disabled:
return 0 ;
case ButtonState .loading:
return 0 ;
}
}
// Example: Primary button
static Widget primaryButton ({
required String label,
required VoidCallback onPressed,
ButtonSize size = ButtonSize .medium,
bool isLoading = false ,
bool isDisabled = false ,
}) {
return ElevatedButton (
onPressed: isDisabled || isLoading ? null : onPressed,
style: ElevatedButton .styleFrom (
padding: _paddingForSize (size),
backgroundColor: AppColors .primary500,
disabledBackgroundColor: AppColors .neutral600,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius .circular (8 ), // 1px border radius
),
),
child: isLoading
? SizedBox (
height: 20 ,
width: 20 ,
child: CircularProgressIndicator (
strokeWidth: 2 ,
valueColor: AlwaysStoppedAnimation (AppColors .textPrimary),
),
)
: Text (
label,
style: AppTypography .labelLarge.copyWith (
color: AppColors .textPrimary,
),
),
);
}
}
class AppTextFieldStyle {
// States: default, focused, filled, error, disabled
// Sizes: default, dense, spacious
// Icons: prefix, suffix, validation
static Widget textField ({
required String label,
required TextEditingController controller,
String ? hint,
String ? error,
TextInputType keyboardType = TextInputType .text,
int maxLines = 1 ,
bool isPassword = false ,
Widget ? prefixIcon,
Widget ? suffixIcon,
ValueChanged <String >? onChanged,
}) {
return TextField (
controller: controller,
obscureText: isPassword,
keyboardType: keyboardType,
maxLines: maxLines,
onChanged: onChanged,
decoration: InputDecoration (
labelText: label,
hintText: hint,
errorText: error,
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
contentPadding: EdgeInsets .symmetric (
horizontal: AppSpacing .lg,
vertical: AppSpacing .md,
),
border: OutlineInputBorder (
borderRadius: BorderRadius .circular (8 ),
borderSide: BorderSide (color: AppColors .neutral600),
),
focusedBorder: OutlineInputBorder (
borderRadius: BorderRadius .circular (8 ),
borderSide: BorderSide (color: AppColors .primary500, width: 2 ),
),
errorBorder: OutlineInputBorder (
borderRadius: BorderRadius .circular (8 ),
borderSide: BorderSide (color: AppColors .error500),
),
filled: true ,
fillColor: AppColors .darkSurface,
labelStyle: AppTypography .labelMedium.copyWith (
color: AppColors .textSecondary,
),
hintStyle: AppTypography .bodyMedium.copyWith (
color: AppColors .textTertiary,
),
),
);
}
}
Accessibility Specifications
Color Contrast (WCAG 2.1 AA)
Color Pair
Ratio
Status
Use Case
Primary (#D4845A) on DarkBackground (#0F0E0C)
7.2:1
✅ Pass AAA
Primary text, headings
TextPrimary (#F2EDE4) on DarkBackground (#0F0E0C)
15.1:1
✅ Pass AAA
Body text, UI labels
Secondary (#4A7C59) on DarkBackground (#0F0E0C)
4.8:1
✅ Pass AA
Secondary actions, badges
TextSecondary (#B8B0A4) on DarkBackground (#0F0E0C)
5.2:1
✅ Pass AA
Metadata, timestamps, disabled text
Error (#EF4444) on DarkBackground (#0F0E0C)
6.3:1
✅ Pass AA
Error messages, validation
Success (#10B981) on DarkBackground (#0F0E0C)
5.4:1
✅ Pass AA
Success messages, confirmations
All interactive components must have visible focus indicators:
Focus ring : 2px solid primary500 around button/input on focus
Keyboard navigation : Tab order follows visual hierarchy (left-to-right, top-to-bottom)
Focus trap : Modals trap focus within modal; dismiss releases focus to trigger
Screen Reader Accessibility
Semantic HTML/Flutter : Use Semantics widget for label associations
Image alt text : All Image widgets have semantic labels (PetModel name, description)
Form labels : TextField labels associated via labelText parameter
Live regions : Status changes announced via Semantics.liveRegion
Dark & Light Mode Support
class AppTheme {
static ThemeData lightTheme () {
return ThemeData (
brightness: Brightness .light,
colorScheme: ColorScheme .light (
primary: AppColors .primary500,
secondary: AppColors .secondary500,
error: AppColors .error500,
background: Color (0xFFFAF8F6 ), // Warm white
surface: Color (0xFFFFFFFF ), // Pure white
onPrimary: AppColors .textPrimary,
onSecondary: AppColors .textPrimary,
onError: Color (0xFFFFFFFF ),
onBackground: AppColors .neutral900,
onSurface: AppColors .neutral900,
),
// ... Material 3 components for light theme
);
}
static ThemeData darkTheme () {
return ThemeData (
brightness: Brightness .dark,
colorScheme: ColorScheme .dark (
primary: AppColors .primary500,
secondary: AppColors .secondary500,
error: AppColors .error500,
background: AppColors .darkBackground,
surface: AppColors .darkSurface,
onPrimary: AppColors .textPrimary,
onSecondary: AppColors .textPrimary,
onError: Color (0xFFFFFFFF ),
onBackground: AppColors .textPrimary,
onSurface: AppColors .textPrimary,
),
// ... Material 3 components for dark theme
);
}
}
lib/theme/
├── app_colors.dart # All color definitions
├── app_typography.dart # Font scale, text styles
├── app_spacing.dart # Spacing scale, constants
├── app_breakpoints.dart # Responsive breakpoints
├── app_components.dart # Button, TextField, Card, etc.
├── app_theme.dart # ThemeData for light/dark
└── DESIGN_TOKENS.md # Documentation
lib/views/components/
├── button.dart # AppButton widget (refactored)
├── text_field.dart # AppTextField widget (refactored)
├── card.dart # AppCard widget
├── ... (other components)
Week 1 : Create new theme/app_*.dart files; import into app_theme.dart
Week 2 : Refactor 5 high-impact components (Button, TextField, Card, AppBar, BottomNav)
Week 3 : Refactor remaining components; remove hardcoded colors/spacing
Week 4 : Verify responsive behavior across all breakpoints; add tests
Platform-Specific Considerations
Safe area : Status bar + navigation bar bottom inset
Notch support : Use MediaQueryData.padding for safe area
Bottom nav : Material design: 48–56px height, 5 items max
Keyboard : TextField respects keyboard safe area automatically
Safe area : Notch + home indicator + landscape bars
Home Indicator : Bottom padding for safe area (34px typical)
Status Bar : Color sync with app bar background
ScrollView insets : Handle under navigation/status bar
Breakpoint threshold : Desktop (840px) shows full multi-column layouts
Keyboard focus : Visible focus ring on all interactive elements
Mouse hover : Hover states visible (buttons, links, cards)
Responsive flex : FlexLayout adjusts column count per breakpoint
Safe area : Not applicable; ignore padding
Part 3: Component Library Roadmap
Phase 1: Foundation (Week 1–2)
Phase 2: Core Components (Week 3)
Phase 3: Complex Components (Week 4)
Phase 4: Documentation & Testing (Week 5)
Prepared : April 29, 2026 | Next Review : May 2, 2026 (component implementation begins)