# UI Components Reference This document provides detailed specifications for all reusable UI components in the CRM application. Use these specifications to build equivalent Flutter widgets. --- ## Table of Contents 1. [Layout Components](#layout-components) 2. [Form Components](#form-components) 3. [Display Components](#display-components) 4. [Navigation Components](#navigation-components) 5. [Feedback Components](#feedback-components) 6. [Data Visualization](#data-visualization) --- ## Layout Components ### MobileShell A wrapper component that constrains content to mobile viewport dimensions. #### Purpose - Constrain content to max 430px width - Handle safe area insets - Center content on larger screens #### Specifications | Property | Value | |----------|-------| | Max width | 430px | | Min height | 100dvh | | Background | `bg-gray-100` | | Safe areas | Top and bottom insets | #### Flutter Implementation ```dart class MobileShell extends StatelessWidget { final Widget child; @override Widget build(BuildContext context) { return Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: 430), child: SafeArea(child: child), ), ); } } ``` --- ### AppBar Custom app bar with flexible leading/trailing slots. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | title | String? | - | Title text | | showBack | bool | false | Show back button | | onBack | Function? | history.back() | Back action | | transparent | bool | false | Transparent background | | leading | Widget? | - | Leading slot content | | trailing | Widget? | - | Trailing slot content | #### Specifications | Property | Value | |----------|-------| | Height | 56px | | Background | White or transparent | | Shadow | None (clean look) | | Z-index | 40 | | Position | Sticky top | #### Visual Layout ``` [Back/Leading] [Title] [Trailing Actions] ``` #### Flutter Implementation ```dart class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { final String? title; final bool showBack; final VoidCallback? onBack; final bool transparent; final Widget? leading; final Widget? trailing; @override Size get preferredSize => Size.fromHeight(56); @override Widget build(BuildContext context) { return AppBar( backgroundColor: transparent ? Colors.transparent : Colors.white, elevation: 0, leading: showBack ? IconButton( icon: Icon(Icons.arrow_back, color: AppColors.gray900), onPressed: onBack ?? () => Navigator.pop(context), ) : leading, title: title != null ? Text(title!, style: TextStyle( color: AppColors.gray900, fontWeight: FontWeight.w600, )) : null, centerTitle: true, actions: trailing != null ? [trailing!] : null, ); } } ``` --- ### Card Container component with elevation and styling variants. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | variant | String | 'elevated' | 'elevated', 'outlined', 'filled' | | padding | String | 'md' | 'none', 'sm', 'md', 'lg' | | onclick | Function? | - | Makes card tappable | #### Variant Styles | Variant | Background | Border | Shadow | |---------|------------|--------|--------| | elevated | white | none | shadow-sm | | outlined | white | 1px gray-200 | none | | filled | gray-50 | none | none | #### Padding Sizes | Size | Value | |------|-------| | none | 0 | | sm | 12px | | md | 16px | | lg | 24px | #### Flutter Implementation ```dart class AppCard extends StatelessWidget { final CardVariant variant; final CardPadding padding; final VoidCallback? onTap; final Widget child; @override Widget build(BuildContext context) { return Material( color: variant == CardVariant.filled ? AppColors.gray50 : Colors.white, borderRadius: BorderRadius.circular(16), elevation: variant == CardVariant.elevated ? 2 : 0, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Container( padding: _getPadding(), decoration: variant == CardVariant.outlined ? BoxDecoration( border: Border.all(color: AppColors.gray200), borderRadius: BorderRadius.circular(16), ) : null, child: child, ), ), ); } } ``` --- ## Form Components ### Button Primary action button with multiple variants and states. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | variant | String | 'primary' | 'primary', 'secondary', 'ghost', 'danger' | | size | String | 'md' | 'sm', 'md', 'lg' | | fullWidth | bool | false | Full container width | | disabled | bool | false | Disabled state | | loading | bool | false | Loading state | | icon | Widget? | - | Leading icon | #### Size Specifications | Size | Height | Padding | Font Size | |------|--------|---------|-----------| | sm | 36px | 12px h | 14px | | md | 44px | 16px h | 14px | | lg | 48px | 20px h | 16px | #### Variant Colors | Variant | Background | Text | Border | |---------|------------|------|--------| | primary | primary-600 | white | none | | secondary | gray-100 | gray-700 | none | | ghost | transparent | gray-700 | none | | danger | danger-600 | white | none | #### States | State | Effect | |-------|--------| | Hover | Slight darken | | Active | Scale 0.98 | | Disabled | Opacity 50% | | Loading | Spinner replaces content | #### Flutter Implementation ```dart class AppButton extends StatelessWidget { final String label; final ButtonVariant variant; final ButtonSize size; final bool fullWidth; final bool disabled; final bool loading; final Widget? icon; final VoidCallback? onPressed; @override Widget build(BuildContext context) { return SizedBox( width: fullWidth ? double.infinity : null, height: _getHeight(), child: ElevatedButton( onPressed: disabled || loading ? null : onPressed, style: ElevatedButton.styleFrom( backgroundColor: _getBackgroundColor(), foregroundColor: _getTextColor(), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: loading ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(_getTextColor()), ), ) : Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[icon!, SizedBox(width: 8)], Text(label), ], ), ), ); } } ``` --- ### InputFloatingLabel Material Design-style input with floating label animation. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | label | String | required | Label text | | type | String | 'text' | Input type | | value | String | '' | Bindable value | | placeholder | String? | - | Placeholder text | | error | String? | - | Error message | | disabled | bool | false | Disabled state | | required | bool | false | Required indicator | | icon | Widget? | - | Leading icon | | trailing | Widget? | - | Trailing widget | #### Supported Types - text - email - password - tel - number - search #### Visual States | State | Border Color | Label Position | |-------|-------------|----------------| | Default | gray-200 | Inside (placeholder position) | | Focused | primary-500 | Floated above | | Filled | gray-200 | Floated above | | Error | danger-500 | Floated above | | Disabled | gray-100 | Varies | #### Specifications | Property | Value | |----------|-------| | Height | 56px | | Border radius | 12px | | Border width | 1px (2px focused) | | Padding | 16px horizontal | | Icon size | 20px | | Label font | 14px (floated: 12px) | #### Flutter Implementation ```dart class FloatingLabelInput extends StatefulWidget { final String label; final TextInputType keyboardType; final TextEditingController controller; final String? errorText; final bool obscureText; final Widget? prefixIcon; final Widget? suffixIcon; @override State createState() => _FloatingLabelInputState(); } class _FloatingLabelInputState extends State { final FocusNode _focusNode = FocusNode(); bool _isFocused = false; @override Widget build(BuildContext context) { return TextField( controller: widget.controller, focusNode: _focusNode, keyboardType: widget.keyboardType, obscureText: widget.obscureText, decoration: InputDecoration( labelText: widget.label, errorText: widget.errorText, prefixIcon: widget.prefixIcon, suffixIcon: widget.suffixIcon, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: AppColors.gray200), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: AppColors.primary500, width: 2), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: AppColors.danger500), ), contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), ), ); } } ``` --- ### IconButton Circular button for icon-only actions. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | variant | String | 'default' | 'default', 'primary', 'ghost' | | size | String | 'md' | 'sm', 'md', 'lg' | | disabled | bool | false | Disabled state | | label | String | required | Accessibility label | #### Size Specifications | Size | Dimensions | Icon Size | |------|------------|-----------| | sm | 32px | 16px | | md | 40px | 20px | | lg | 48px | 24px | #### Variant Styles | Variant | Background | Icon Color | |---------|------------|------------| | default | gray-100 | gray-700 | | primary | primary-600 | white | | ghost | transparent | gray-700 | #### Flutter Implementation ```dart class AppIconButton extends StatelessWidget { final IconData icon; final IconButtonVariant variant; final IconButtonSize size; final bool disabled; final String label; final VoidCallback? onPressed; @override Widget build(BuildContext context) { return Semantics( label: label, button: true, child: Material( color: _getBackgroundColor(), shape: CircleBorder(), child: InkWell( onTap: disabled ? null : onPressed, customBorder: CircleBorder(), child: Container( width: _getSize(), height: _getSize(), alignment: Alignment.center, child: Icon( icon, size: _getIconSize(), color: _getIconColor(), ), ), ), ), ); } } ``` --- ## Display Components ### Avatar User avatar with image or initials fallback. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | src | String? | - | Image URL | | name | String? | - | Name for initials | | size | String | 'md' | 'xs', 'sm', 'md', 'lg', 'xl' | #### Size Specifications | Size | Dimensions | Font Size | |------|------------|-----------| | xs | 24px | 10px | | sm | 32px | 12px | | md | 40px | 14px | | lg | 48px | 16px | | xl | 64px | 20px | #### Color Generation Deterministic color based on name hash: ```javascript const colors = [ 'bg-red-500', 'bg-orange-500', 'bg-amber-500', 'bg-green-500', 'bg-teal-500', 'bg-blue-500', 'bg-indigo-500', 'bg-purple-500' ]; const index = name.charCodeAt(0) % colors.length; ``` #### Flutter Implementation ```dart class Avatar extends StatelessWidget { final String? imageUrl; final String? name; final AvatarSize size; String get initials { if (name == null || name!.isEmpty) return '?'; final parts = name!.split(' '); if (parts.length >= 2) { return '${parts[0][0]}${parts[1][0]}'.toUpperCase(); } return name![0].toUpperCase(); } Color get backgroundColor { if (name == null) return AppColors.gray400; final colors = [ Colors.red, Colors.orange, Colors.amber, Colors.green, Colors.teal, Colors.blue, Colors.indigo, Colors.purple, ]; return colors[name!.codeUnitAt(0) % colors.length]; } @override Widget build(BuildContext context) { final dimension = _getDimension(); if (imageUrl != null) { return ClipOval( child: Image.network( imageUrl!, width: dimension, height: dimension, fit: BoxFit.cover, ), ); } return Container( width: dimension, height: dimension, decoration: BoxDecoration( color: backgroundColor, shape: BoxShape.circle, ), alignment: Alignment.center, child: Text( initials, style: TextStyle( color: Colors.white, fontSize: _getFontSize(), fontWeight: FontWeight.w600, ), ), ); } } ``` --- ### Badge Status indicator with color variants. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | variant | String | 'default' | Status variant | | size | String | 'md' | 'sm', 'md' | #### Variants | Variant | Background | Text Color | |---------|------------|------------| | new | primary-100 | primary-700 | | contacted | warning-50 | warning-700 | | qualified | success-50 | success-700 | | lost | danger-50 | danger-700 | | hot | danger-500 | white | | default | gray-100 | gray-700 | #### Size Specifications | Size | Padding | Font Size | |------|---------|-----------| | sm | 8px h, 2px v | 12px | | md | 10px h, 4px v | 14px | #### Flutter Implementation ```dart class StatusBadge extends StatelessWidget { final String label; final BadgeVariant variant; final BadgeSize size; @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.symmetric( horizontal: size == BadgeSize.sm ? 8 : 10, vertical: size == BadgeSize.sm ? 2 : 4, ), decoration: BoxDecoration( color: _getBackgroundColor(), borderRadius: BorderRadius.circular(20), ), child: Text( label, style: TextStyle( color: _getTextColor(), fontSize: size == BadgeSize.sm ? 12 : 14, fontWeight: FontWeight.w500, ), ), ); } } ``` --- ### PriorityBadge Priority indicator with icon and color. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | priority | String | required | 'low', 'medium', 'high' | | showLabel | bool | false | Show text label | | size | String | 'md' | 'sm', 'md' | #### Priority Styles | Priority | Icon | Color | |----------|------|-------| | high | AlertTriangle | danger-600 | | medium | Flag | warning-600 | | low | Minus | gray-500 | #### Flutter Implementation ```dart class PriorityBadge extends StatelessWidget { final Priority priority; final bool showLabel; final bool small; IconData get icon { switch (priority) { case Priority.high: return Icons.warning_amber; case Priority.medium: return Icons.flag; case Priority.low: return Icons.remove; } } Color get color { switch (priority) { case Priority.high: return AppColors.danger600; case Priority.medium: return AppColors.warning600; case Priority.low: return AppColors.gray500; } } @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: small ? 14 : 16, color: color), if (showLabel) ...[ SizedBox(width: 4), Text( priority.name.capitalize(), style: TextStyle( color: color, fontSize: small ? 12 : 14, fontWeight: FontWeight.w500, ), ), ], ], ); } } ``` --- ### LabelPill Colored tag/label pill with auto-color mapping. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | label | String | required | Label text | | color | String? | auto | Optional explicit color | #### Auto-Color Mapping | Keywords | Color | |----------|-------| | enterprise, tech, startup | blue | | expansion, new-business, finance | green | | renewal | yellow | | strategic, retail | purple | | hot-lead, whale | pink | | smb, early-stage | gray | #### Flutter Implementation ```dart class LabelPill extends StatelessWidget { final String label; final Color? explicitColor; Color get backgroundColor { if (explicitColor != null) return explicitColor!; final lowerLabel = label.toLowerCase(); if (['enterprise', 'tech', 'startup'].any((k) => lowerLabel.contains(k))) { return AppColors.blue100; } if (['expansion', 'new-business', 'finance'].any((k) => lowerLabel.contains(k))) { return AppColors.green100; } // ... more mappings return AppColors.gray100; } @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(12), ), child: Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: _getTextColor(), ), ), ); } } ``` --- ## Navigation Components ### BottomNav Fixed bottom navigation bar with 5 tabs. #### Tabs | Index | Icon | Label | Route | |-------|------|-------|-------| | 0 | LayoutDashboard | Dashboard | /dashboard | | 1 | Users | Leads | /leads | | 2 | Briefcase | Deals | /deals | | 3 | CheckSquare | Tasks | /tasks | | 4 | MoreHorizontal | More | /more | #### Specifications | Property | Value | |----------|-------| | Height | 64px | | Background | white | | Border top | 1px gray-100 | | Z-index | 50 | | Active color | primary-600 | | Inactive color | gray-400 | | Icon size | 24px | | Label font | 12px | #### Flutter Implementation ```dart class BottomNav extends StatelessWidget { final int currentIndex; @override Widget build(BuildContext context) { return BottomNavigationBar( currentIndex: currentIndex, type: BottomNavigationBarType.fixed, selectedItemColor: AppColors.primary600, unselectedItemColor: AppColors.gray400, selectedFontSize: 12, unselectedFontSize: 12, items: [ BottomNavigationBarItem( icon: Icon(Icons.dashboard_outlined), activeIcon: Icon(Icons.dashboard), label: 'Dashboard', ), BottomNavigationBarItem( icon: Icon(Icons.people_outline), activeIcon: Icon(Icons.people), label: 'Leads', ), BottomNavigationBarItem( icon: Icon(Icons.work_outline), activeIcon: Icon(Icons.work), label: 'Deals', ), BottomNavigationBarItem( icon: Icon(Icons.check_box_outlined), activeIcon: Icon(Icons.check_box), label: 'Tasks', ), BottomNavigationBarItem( icon: Icon(Icons.more_horiz), label: 'More', ), ], onTap: (index) => _navigateToIndex(context, index), ); } } ``` --- ### TabBar Horizontal tab switcher with underline indicator. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | tabs | List | required | Tab definitions | | activeTab | String | required | Currently active tab ID | | onTabChange | Function? | - | Tab change callback | #### Specifications | Property | Value | |----------|-------| | Height | 48px | | Tab padding | 16px horizontal | | Active indicator | 2px height, primary-600 | | Active text | primary-600, font-semibold | | Inactive text | gray-500 | #### Flutter Implementation ```dart class AppTabBar extends StatelessWidget { final List tabs; final String activeTab; final Function(String) onTabChange; @override Widget build(BuildContext context) { return Container( height: 48, decoration: BoxDecoration( border: Border( bottom: BorderSide(color: AppColors.gray200), ), ), child: Row( children: tabs.map((tab) { final isActive = tab.id == activeTab; return Expanded( child: InkWell( onTap: () => onTabChange(tab.id), child: Container( alignment: Alignment.center, decoration: BoxDecoration( border: Border( bottom: BorderSide( color: isActive ? AppColors.primary600 : Colors.transparent, width: 2, ), ), ), child: Text( tab.label, style: TextStyle( color: isActive ? AppColors.primary600 : AppColors.gray500, fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, ), ), ), ), ); }).toList(), ), ); } } ``` --- ### FAB (Floating Action Button) Expandable floating action button with multiple actions. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | onAction | Function | - | Callback with action type | #### Actions | Action | Icon | Label | |--------|------|-------| | lead | User | Add Lead | | deal | Briefcase | Add Deal | | task | CheckSquare | Add Task | #### Specifications | Property | Value | |----------|-------| | Main button size | 56px | | Mini button size | 48px | | Position | Bottom-right | | Bottom offset | 80px (above BottomNav) | | Right offset | 16px | | Expanded gap | 12px between buttons | #### States - **Collapsed:** Plus icon, primary-600 background - **Expanded:** X icon, backdrop overlay, action buttons visible #### Flutter Implementation ```dart class ExpandableFAB extends StatefulWidget { final Function(String) onAction; @override State createState() => _ExpandableFABState(); } class _ExpandableFABState extends State with SingleTickerProviderStateMixin { bool _isExpanded = false; late AnimationController _controller; @override Widget build(BuildContext context) { return Stack( alignment: Alignment.bottomRight, children: [ if (_isExpanded) GestureDetector( onTap: () => setState(() => _isExpanded = false), child: Container( color: Colors.black54, ), ), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ if (_isExpanded) ...[ _buildMiniFAB('task', Icons.check_box, 'Add Task'), SizedBox(height: 12), _buildMiniFAB('deal', Icons.work, 'Add Deal'), SizedBox(height: 12), _buildMiniFAB('lead', Icons.person, 'Add Lead'), SizedBox(height: 12), ], FloatingActionButton( onPressed: () => setState(() => _isExpanded = !_isExpanded), backgroundColor: AppColors.primary600, child: Icon(_isExpanded ? Icons.close : Icons.add), ), ], ), ], ); } } ``` --- ## Data Visualization ### ProgressRing Circular progress indicator with center text. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | percent | number | required | 0-100 percentage | | size | number | 100 | Diameter in pixels | | strokeWidth | number | 8 | Ring thickness | | color | String | 'primary-500' | Ring color | | label | String? | - | Center label text | #### Flutter Implementation ```dart class ProgressRing extends StatelessWidget { final int percent; final double size; final double strokeWidth; final Color color; final String? label; @override Widget build(BuildContext context) { return SizedBox( width: size, height: size, child: Stack( alignment: Alignment.center, children: [ CircularProgressIndicator( value: percent / 100, strokeWidth: strokeWidth, backgroundColor: AppColors.gray200, valueColor: AlwaysStoppedAnimation(color), ), Column( mainAxisSize: MainAxisSize.min, children: [ Text( '$percent%', style: TextStyle( fontSize: size * 0.2, fontWeight: FontWeight.bold, ), ), if (label != null) Text( label!, style: TextStyle( fontSize: size * 0.1, color: AppColors.gray500, ), ), ], ), ], ), ); } } ``` --- ### SalesChart Bar chart for sales trend visualization. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | data | List | required | Month, value, deals | #### Data Point Structure ```dart class SalesDataPoint { final String month; // "Jan", "Feb", etc. final int value; // Dollar amount final int deals; // Deal count } ``` #### Specifications | Property | Value | |----------|-------| | Bar color | primary-500 | | Bar radius | 4px top | | Grid lines | Horizontal, gray-100 | | X-axis labels | Month abbreviations | | Tooltip | Value + deal count | #### Flutter Implementation Use `fl_chart` package: ```dart class SalesChart extends StatelessWidget { final List data; @override Widget build(BuildContext context) { return BarChart( BarChartData( barGroups: data.asMap().entries.map((entry) { return BarChartGroupData( x: entry.key, barRods: [ BarChartRodData( toY: entry.value.value.toDouble(), color: AppColors.primary500, width: 20, borderRadius: BorderRadius.vertical(top: Radius.circular(4)), ), ], ); }).toList(), titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { return Text(data[value.toInt()].month); }, ), ), ), ), ); } } ``` --- ## Component Index | Component | Category | File Location | |-----------|----------|---------------| | MobileShell | Layout | lib/components/layout/ | | AppBar | Layout | lib/components/layout/ | | BottomNav | Navigation | lib/components/layout/ | | FAB | Navigation | lib/components/layout/ | | Card | Layout | lib/components/ui/ | | Button | Form | lib/components/ui/ | | InputFloatingLabel | Form | lib/components/ui/ | | IconButton | Form | lib/components/ui/ | | Avatar | Display | lib/components/ui/ | | Badge | Display | lib/components/ui/ | | PriorityBadge | Display | lib/components/ui/ | | LabelPill | Display | lib/components/ui/ | | TabBar | Navigation | lib/components/ui/ | | ProgressRing | Charts | lib/components/charts/ | | SalesChart | Charts | lib/components/charts/ | | LeadCard | Domain | lib/components/leads/ | | DealCard | Domain | lib/components/deals/ | | TaskRow | Domain | lib/components/tasks/ | | KanbanColumn | Domain | lib/components/deals/ | | TimelineItem | Domain | lib/components/leads/ | | Calendar | Domain | lib/components/tasks/ |