bottleneckcrm/mobile/docs/components/ui-components.md
2025-12-17 14:36:03 +05:30

28 KiB

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
  2. Form Components
  3. Display Components
  4. Navigation Components
  5. Feedback Components
  6. 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

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

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

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

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

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<FloatingLabelInput> createState() => _FloatingLabelInputState();
}

class _FloatingLabelInputState extends State<FloatingLabelInput> {
  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

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:

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

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

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

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

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

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

class AppTabBar extends StatelessWidget {
  final List<TabItem> 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

class ExpandableFAB extends StatefulWidget {
  final Function(String) onAction;

  @override
  State<ExpandableFAB> createState() => _ExpandableFABState();
}

class _ExpandableFABState extends State<ExpandableFAB>
    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

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

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:

class SalesChart extends StatelessWidget {
  final List<SalesDataPoint> 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/