1109 lines
28 KiB
Markdown
1109 lines
28 KiB
Markdown
# 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<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
|
|
```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<Tab> | 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<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
|
|
```dart
|
|
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
|
|
```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<DataPoint> | 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<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/ |
|