m14
This commit is contained in:
parent
3d7b269ff1
commit
12e6398d01
11 changed files with 825 additions and 775 deletions
696
mobile/DESIGN_SYSTEM.md
Normal file
696
mobile/DESIGN_SYSTEM.md
Normal file
|
|
@ -0,0 +1,696 @@
|
|||
# BottleCRM Mobile Design System
|
||||
|
||||
A comprehensive design guide for the BottleCRM mobile app, inspired by modern CRM apps like HubSpot.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Design Principles](#design-principles)
|
||||
2. [Color Palette](#color-palette)
|
||||
3. [Typography](#typography)
|
||||
4. [Spacing & Layout](#spacing--layout)
|
||||
5. [Components](#components)
|
||||
6. [Icons](#icons)
|
||||
7. [Patterns](#patterns)
|
||||
8. [Accessibility](#accessibility)
|
||||
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
### Core Values
|
||||
|
||||
1. **Clarity First** - Every element should have a clear purpose
|
||||
2. **Scannable** - Users should quickly find what they need
|
||||
3. **Minimal Friction** - Reduce taps to complete actions
|
||||
4. **Consistent** - Same patterns across all screens
|
||||
5. **Professional** - Clean, modern, business-appropriate
|
||||
|
||||
### Design Philosophy
|
||||
|
||||
- **Flat Design** - No heavy shadows, use borders for depth
|
||||
- **White Space** - Generous spacing for readability
|
||||
- **High Contrast** - Clear text hierarchy
|
||||
- **Touch Friendly** - Minimum 44px touch targets
|
||||
|
||||
---
|
||||
|
||||
## Color Palette
|
||||
|
||||
### Primary Colors
|
||||
|
||||
| Name | Hex | Usage |
|
||||
|------|-----|-------|
|
||||
| **Primary 600** | `#2563EB` | Primary actions, links, selected states |
|
||||
| **Primary 500** | `#3B82F6` | Hover states, secondary emphasis |
|
||||
| **Primary 100** | `#DBEAFE` | Light backgrounds, chips |
|
||||
| **Primary 50** | `#EFF6FF` | Subtle backgrounds |
|
||||
|
||||
### Accent Colors
|
||||
|
||||
| Name | Hex | Usage |
|
||||
|------|-----|-------|
|
||||
| **Accent** | `#F97316` | FAB, important CTAs, notifications |
|
||||
| **Accent Light** | `#FED7AA` | Accent backgrounds, highlights |
|
||||
|
||||
### Semantic Colors
|
||||
|
||||
| Name | Hex | Usage |
|
||||
|------|-----|-------|
|
||||
| **Success 500** | `#22C55E` | Won deals, completed tasks |
|
||||
| **Success 100** | `#DCFCE7` | Success backgrounds |
|
||||
| **Warning 500** | `#F59E0B` | Attention needed, medium priority |
|
||||
| **Warning 100** | `#FEF3C7` | Warning backgrounds |
|
||||
| **Danger 500** | `#EF4444` | Errors, overdue, lost deals |
|
||||
| **Danger 100** | `#FEE2E2` | Error backgrounds |
|
||||
|
||||
### Neutral Colors
|
||||
|
||||
| Name | Hex | Usage |
|
||||
|------|-----|-------|
|
||||
| **Gray 900** | `#0F172A` | Primary text |
|
||||
| **Gray 700** | `#374151` | Secondary text |
|
||||
| **Gray 500** | `#6B7280` | Tertiary text, placeholders |
|
||||
| **Gray 300** | `#D1D5DB` | Borders, dividers |
|
||||
| **Gray 200** | `#E5E7EB` | Card borders, separators |
|
||||
| **Gray 100** | `#F3F4F6` | Backgrounds, disabled states |
|
||||
| **Gray 50** | `#F9FAFB` | Page background |
|
||||
| **White** | `#FFFFFF` | Cards, surfaces |
|
||||
|
||||
### Status Colors for CRM
|
||||
|
||||
```
|
||||
Lead Status:
|
||||
- New → Primary 500 (#3B82F6)
|
||||
- Contacted → Warning 500 (#F59E0B)
|
||||
- Qualified → Success 500 (#22C55E)
|
||||
- Lost → Danger 500 (#EF4444)
|
||||
|
||||
Deal Stages:
|
||||
- Prospecting → Gray 400 (#9CA3AF)
|
||||
- Qualified → Primary 500 (#3B82F6)
|
||||
- Proposal → Purple 500 (#A855F7)
|
||||
- Negotiation → Warning 500 (#F59E0B)
|
||||
- Closed Won → Success 500 (#22C55E)
|
||||
- Closed Lost → Danger 500 (#EF4444)
|
||||
|
||||
Priority:
|
||||
- Low → Gray 400 (#9CA3AF)
|
||||
- Medium → Warning 500 (#F59E0B)
|
||||
- High → Danger 500 (#EF4444)
|
||||
- Urgent → Purple 500 (#A855F7)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Family
|
||||
|
||||
**Primary Font:** Inter (Google Fonts)
|
||||
- Designed for screens
|
||||
- Excellent readability at small sizes
|
||||
- 9 weights available
|
||||
- Free and open source
|
||||
|
||||
**Fallback:** System fonts (SF Pro on iOS, Roboto on Android)
|
||||
|
||||
### Type Scale
|
||||
|
||||
| Style | Size | Weight | Line Height | Letter Spacing | Usage |
|
||||
|-------|------|--------|-------------|----------------|-------|
|
||||
| **Display** | 28px | Bold (700) | 1.2 | -0.5px | KPIs, large numbers |
|
||||
| **H1** | 22px | Semi-bold (600) | 1.3 | -0.3px | Page titles |
|
||||
| **H2** | 18px | Semi-bold (600) | 1.4 | -0.2px | Section headers |
|
||||
| **H3** | 16px | Semi-bold (600) | 1.4 | 0 | Card titles, list item titles |
|
||||
| **Body Large** | 16px | Regular (400) | 1.5 | 0 | Important body text |
|
||||
| **Body** | 15px | Regular (400) | 1.5 | 0 | Default body text |
|
||||
| **Body Small** | 14px | Regular (400) | 1.4 | 0 | Secondary text |
|
||||
| **Caption** | 12px | Regular (400) | 1.3 | 0 | Timestamps, helper text |
|
||||
| **Overline** | 11px | Semi-bold (600) | 1.3 | 0.5px | Section labels (uppercase) |
|
||||
|
||||
### Number Typography
|
||||
|
||||
| Style | Size | Weight | Usage |
|
||||
|-------|------|--------|-------|
|
||||
| **Number Large** | 28px | Bold (700) | Dashboard KPIs |
|
||||
| **Number Medium** | 20px | Semi-bold (600) | Card metrics |
|
||||
| **Number Small** | 14px | Medium (500) | Inline numbers |
|
||||
|
||||
Use `FontFeature.tabularFigures()` for aligned numbers in tables/lists.
|
||||
|
||||
### Text Colors
|
||||
|
||||
| Type | Color | Hex |
|
||||
|------|-------|-----|
|
||||
| Primary | Gray 900 | `#0F172A` |
|
||||
| Secondary | Gray 500 | `#6B7280` |
|
||||
| Tertiary | Gray 400 | `#9CA3AF` |
|
||||
| Disabled | Gray 400 @ 50% | `#9CA3AF80` |
|
||||
| Link | Primary 600 | `#2563EB` |
|
||||
| Error | Danger 500 | `#EF4444` |
|
||||
|
||||
---
|
||||
|
||||
## Spacing & Layout
|
||||
|
||||
### Spacing Scale (4px base)
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| `xs` | 4px | Tight gaps, icon padding |
|
||||
| `sm` | 8px | Small gaps, list item spacing |
|
||||
| `md` | 12px | Default component padding |
|
||||
| `lg` | 16px | Section padding, card padding |
|
||||
| `xl` | 20px | Large gaps |
|
||||
| `xxl` | 24px | Section spacing |
|
||||
| `xxxl` | 32px | Page sections |
|
||||
|
||||
### Page Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Status Bar (system) │
|
||||
├─────────────────────────────────────┤
|
||||
│ App Bar (52-56px) │
|
||||
│ ← Title Actions │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ Content Area │ │
|
||||
│ │ Padding: 16px horizontal │ │
|
||||
│ │ │ │
|
||||
│ │ Section Gap: 20-24px │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌───┐ │
|
||||
│ │FAB│ │
|
||||
│ └───┘ │
|
||||
├─────────────────────────────────────┤
|
||||
│ Bottom Navigation (56-60px) │
|
||||
│ Home Contacts Deals Tasks More │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Component Heights
|
||||
|
||||
| Component | Height |
|
||||
|-----------|--------|
|
||||
| App Bar | 52-56px |
|
||||
| Bottom Nav | 56-60px |
|
||||
| Button Large | 48px |
|
||||
| Button Medium | 44px |
|
||||
| Button Small | 36px |
|
||||
| Input Field | 48px |
|
||||
| List Item | 64-72px |
|
||||
| Quick Action Button | 56px |
|
||||
| FAB | 56px |
|
||||
| Tab Bar | 48px |
|
||||
| Search Bar | 44px |
|
||||
| Filter Chip | 36px |
|
||||
|
||||
### Border Radius
|
||||
|
||||
| Size | Value | Usage |
|
||||
|------|-------|-------|
|
||||
| XS | 4px | Small elements, inputs |
|
||||
| SM | 6px | Buttons, chips |
|
||||
| MD | 8px | Cards, dialogs |
|
||||
| LG | 12px | Large cards, modals |
|
||||
| XL | 16px | Bottom sheets |
|
||||
| Full | 9999px | Pills, avatars, FAB |
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### Cards
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Card Title Action ⋮ │ ← 16px padding
|
||||
│ Subtitle or count │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ Card Content │
|
||||
│ │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
Style:
|
||||
- Background: White
|
||||
- Border: 1px solid Gray 200
|
||||
- Border Radius: 8px
|
||||
- Shadow: NONE (flat design)
|
||||
- Padding: 16px
|
||||
```
|
||||
|
||||
### List Items
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ○ Title Text ⋮ │
|
||||
│ Subtitle or description │
|
||||
│ 📅 Date/time [Badge] │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
Style:
|
||||
- Height: 64-72px
|
||||
- Padding: 16px horizontal, 12px vertical
|
||||
- Border Bottom: 1px solid Gray 200
|
||||
- Icon/Avatar: 40px, left aligned
|
||||
- Action: Right aligned
|
||||
```
|
||||
|
||||
### Buttons
|
||||
|
||||
**Primary Button (Filled)**
|
||||
```
|
||||
Background: Primary 600
|
||||
Text: White, 15px Semi-bold
|
||||
Height: 48px
|
||||
Border Radius: 8px
|
||||
Padding: 16px horizontal
|
||||
```
|
||||
|
||||
**Secondary Button (Outlined)**
|
||||
```
|
||||
Background: Transparent
|
||||
Border: 1px solid Gray 300
|
||||
Text: Gray 900, 15px Semi-bold
|
||||
Height: 48px
|
||||
Border Radius: 8px
|
||||
```
|
||||
|
||||
**Text Button**
|
||||
```
|
||||
Background: Transparent
|
||||
Text: Primary 600, 15px Semi-bold
|
||||
Height: 44px
|
||||
Padding: 8px horizontal
|
||||
```
|
||||
|
||||
**FAB (Floating Action Button)**
|
||||
```
|
||||
Background: Accent (#F97316)
|
||||
Icon: White, 24px
|
||||
Size: 56px
|
||||
Border Radius: 16px (or full)
|
||||
Shadow: Subtle elevation
|
||||
Position: Bottom-right, 16px from edges
|
||||
```
|
||||
|
||||
### Quick Action Buttons
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ 🏠 │ │ 📊 │ │ 👥 │ │ 📞 │
|
||||
│ Feed │ │Dashboard│ │ Leads │ │ Calls │
|
||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||||
|
||||
Style:
|
||||
- Size: 72px width, 56px height
|
||||
- Background: White
|
||||
- Border: 1px solid Gray 200
|
||||
- Border Radius: 8px
|
||||
- Icon: 24px, Gray 700
|
||||
- Label: 11px, Gray 500
|
||||
- Gap between: 12px
|
||||
- Horizontal scroll if overflow
|
||||
```
|
||||
|
||||
### Chips & Badges
|
||||
|
||||
**Filter Chip**
|
||||
```
|
||||
Height: 36px
|
||||
Padding: 12px horizontal
|
||||
Border: 1px solid Primary 600
|
||||
Border Radius: 18px (full)
|
||||
Text: Primary 600, 13px Medium
|
||||
Background: Transparent (or Primary 50 when selected)
|
||||
```
|
||||
|
||||
**Status Badge**
|
||||
```
|
||||
Height: 22px
|
||||
Padding: 8px horizontal
|
||||
Border Radius: 11px (full)
|
||||
Text: 11px Semi-bold, White
|
||||
Background: Status color
|
||||
```
|
||||
|
||||
**Contact Chip**
|
||||
```
|
||||
Height: 32px
|
||||
Padding: 12px horizontal
|
||||
Border Radius: 16px
|
||||
Background: Primary 100
|
||||
Text: Primary 700, 13px Medium
|
||||
```
|
||||
|
||||
### Bottom Navigation
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 🏠 👤 💼 ✓ 🔍 │
|
||||
│ Home Contacts Deals Tasks Search │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
Style:
|
||||
- Height: 56-60px
|
||||
- Background: White
|
||||
- Border Top: 1px solid Gray 200
|
||||
- Icon: 24px
|
||||
- Label: 11px
|
||||
- Active: Primary 600
|
||||
- Inactive: Gray 400
|
||||
- 5 items maximum
|
||||
```
|
||||
|
||||
### Input Fields
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Label │
|
||||
├─────────────────────────────────────┤
|
||||
│ Placeholder text... │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
Style:
|
||||
- Height: 48px
|
||||
- Background: White
|
||||
- Border: 1px solid Gray 300
|
||||
- Border Radius: 8px
|
||||
- Padding: 16px horizontal
|
||||
- Label: 13px Medium, Gray 700
|
||||
- Input Text: 15px Regular, Gray 900
|
||||
- Placeholder: 15px Regular, Gray 400
|
||||
- Focus Border: Primary 600
|
||||
- Error Border: Danger 500
|
||||
```
|
||||
|
||||
### Section Header
|
||||
|
||||
```
|
||||
Guided actions View all →
|
||||
Subtitle text here
|
||||
|
||||
Style:
|
||||
- Title: 16px Semi-bold, Gray 900
|
||||
- Subtitle: 13px Regular, Gray 500
|
||||
- Link: 14px Medium, Primary 600
|
||||
- Margin Bottom: 12px
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Icons
|
||||
|
||||
### Icon Sizes
|
||||
|
||||
| Size | Pixels | Usage |
|
||||
|------|--------|-------|
|
||||
| XS | 16px | Inline, badges |
|
||||
| SM | 20px | List items, buttons |
|
||||
| MD | 24px | Default, nav items |
|
||||
| LG | 28px | Emphasis |
|
||||
| XL | 32px | Empty states |
|
||||
| XXL | 48px | Illustrations |
|
||||
|
||||
### Icon Style
|
||||
|
||||
- **Library:** Lucide Icons (consistent with web app)
|
||||
- **Stroke Width:** 1.5-2px
|
||||
- **Style:** Outlined (not filled)
|
||||
- **Color:** Inherit from text or specific semantic color
|
||||
|
||||
### Common Icons
|
||||
|
||||
```
|
||||
Navigation:
|
||||
- Home: home
|
||||
- Contacts: users
|
||||
- Deals: briefcase
|
||||
- Tasks: check-square
|
||||
- Search: search
|
||||
- Settings: settings
|
||||
- More: more-horizontal
|
||||
|
||||
Actions:
|
||||
- Add: plus
|
||||
- Edit: pencil
|
||||
- Delete: trash-2
|
||||
- Share: share
|
||||
- Filter: filter
|
||||
- Sort: arrow-up-down
|
||||
- Refresh: refresh-cw
|
||||
|
||||
Communication:
|
||||
- Email: mail
|
||||
- Call: phone
|
||||
- Meeting: calendar
|
||||
- Note: file-text
|
||||
|
||||
Status:
|
||||
- Success: check-circle
|
||||
- Warning: alert-triangle
|
||||
- Error: x-circle
|
||||
- Info: info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patterns
|
||||
|
||||
### Dashboard Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Page Title │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Quick Actions Row - Horizontal] │
|
||||
├─────────────────────────────────────┤
|
||||
│ Section Header View all│
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ Metric Card / Chart │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
├─────────────────────────────────────┤
|
||||
│ Section Header View all│
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ List Item 1 │ │
|
||||
│ ├─────────────────────────────────┤ │
|
||||
│ │ List Item 2 │ │
|
||||
│ ├─────────────────────────────────┤ │
|
||||
│ │ List Item 3 │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### List View Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ← Page Title 🔍 ⋮ │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Filter Chips - Horizontal Scroll] │
|
||||
├─────────────────────────────────────┤
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ List Item with Avatar │ │
|
||||
│ │ Title → │ │
|
||||
│ │ Subtitle Status Badge │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ List Item │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ [+ FAB] │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Detail View Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ← Back Title Edit ⋮ │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ [Avatar / Icon - 64px] │
|
||||
│ Primary Name │
|
||||
│ Secondary Info │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ [Action Buttons Row] │ │
|
||||
│ │ 📧 Email 📞 Call 📝 Note │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Tab Bar: Details | Activity | ... ]│
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ Field Label │
|
||||
│ Field Value │
|
||||
│ ─────────────────────────────────── │
|
||||
│ Field Label │
|
||||
│ Field Value │
|
||||
│ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Form Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ← Cancel Title Save │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ Section Label │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ Input Field │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ Input Field │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Section Label │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ Dropdown / Picker │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
Spacing:
|
||||
- Section gap: 24px
|
||||
- Field gap: 16px
|
||||
- Label to field: 8px
|
||||
```
|
||||
|
||||
### Empty State
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ │
|
||||
│ │
|
||||
│ [Icon - 48px] │
|
||||
│ │
|
||||
│ No items yet │
|
||||
│ Description text goes here │
|
||||
│ explaining what to do next │
|
||||
│ │
|
||||
│ [ Primary Action ] │
|
||||
│ │
|
||||
│ │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
Style:
|
||||
- Icon: Gray 300, 48px
|
||||
- Title: 18px Semi-bold, Gray 700
|
||||
- Description: 14px Regular, Gray 500, centered
|
||||
- Max width: 280px
|
||||
```
|
||||
|
||||
### Loading States
|
||||
|
||||
**Skeleton Loading**
|
||||
```
|
||||
Use animated placeholder shapes matching content layout:
|
||||
- Rectangles for text (with rounded corners)
|
||||
- Circles for avatars
|
||||
- Animation: Shimmer effect (light gray pulse)
|
||||
```
|
||||
|
||||
**Inline Loading**
|
||||
```
|
||||
- Small spinner (20px) next to action
|
||||
- Or three-dot animation
|
||||
```
|
||||
|
||||
**Full Screen Loading**
|
||||
```
|
||||
- Centered spinner (40px)
|
||||
- Optional: Brand logo animation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
### Minimum Requirements
|
||||
|
||||
1. **Touch Targets:** Minimum 44x44px for all interactive elements
|
||||
2. **Color Contrast:** 4.5:1 ratio for normal text, 3:1 for large text
|
||||
3. **Focus Indicators:** Visible focus states for keyboard navigation
|
||||
4. **Screen Readers:** Proper semantic labels and hints
|
||||
|
||||
### Color Contrast Ratios
|
||||
|
||||
| Combination | Ratio | Status |
|
||||
|-------------|-------|--------|
|
||||
| Gray 900 on White | 15.5:1 | ✅ AAA |
|
||||
| Gray 500 on White | 4.6:1 | ✅ AA |
|
||||
| Primary 600 on White | 4.5:1 | ✅ AA |
|
||||
| White on Primary 600 | 4.5:1 | ✅ AA |
|
||||
| White on Accent | 3.1:1 | ⚠️ AA Large |
|
||||
|
||||
### Text Scaling
|
||||
|
||||
- Support system text scaling up to 200%
|
||||
- Use flexible layouts that accommodate larger text
|
||||
- Test with accessibility settings enabled
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Flutter Specifics
|
||||
|
||||
```dart
|
||||
// Theme setup
|
||||
ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: Color(0xFF2563EB),
|
||||
secondary: Color(0xFFF97316),
|
||||
surface: Colors.white,
|
||||
background: Color(0xFFF9FAFB),
|
||||
error: Color(0xFFEF4444),
|
||||
),
|
||||
textTheme: GoogleFonts.interTextTheme(),
|
||||
)
|
||||
|
||||
// No elevation/shadows on cards
|
||||
Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: Color(0xFFE5E7EB)),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Do's and Don'ts
|
||||
|
||||
**Do:**
|
||||
- Use consistent spacing from the scale
|
||||
- Keep cards flat with borders
|
||||
- Use semantic colors for status
|
||||
- Maintain touch target sizes
|
||||
- Test on multiple screen sizes
|
||||
|
||||
**Don't:**
|
||||
- Use shadows on cards (flat design)
|
||||
- Mix different border radius sizes
|
||||
- Use colors outside the palette
|
||||
- Create touch targets smaller than 44px
|
||||
- Hardcode pixel values (use tokens)
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0 | Jan 2026 | Initial design system based on HubSpot mobile app analysis |
|
||||
|
||||
---
|
||||
|
||||
*This design system is a living document. Update as the app evolves.*
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// BottleCRM Color System
|
||||
/// A professional, modern color palette optimized for sales workflows
|
||||
/// A professional, modern color palette optimized for CRM workflows
|
||||
/// Inspired by HubSpot mobile app design patterns
|
||||
class AppColors {
|
||||
AppColors._();
|
||||
|
||||
|
|
@ -19,6 +20,18 @@ class AppColors {
|
|||
static const Color primary800 = Color(0xFF1E40AF);
|
||||
static const Color primary900 = Color(0xFF1E3A8A);
|
||||
|
||||
// ============================================
|
||||
// ACCENT PALETTE (Coral/Orange - CTAs & FAB)
|
||||
// ============================================
|
||||
static const Color accent50 = Color(0xFFFFF7ED);
|
||||
static const Color accent100 = Color(0xFFFFEDD5);
|
||||
static const Color accent200 = Color(0xFFFED7AA);
|
||||
static const Color accent300 = Color(0xFFFDBA74);
|
||||
static const Color accent400 = Color(0xFFFB923C);
|
||||
static const Color accent500 = Color(0xFFF97316); // Main accent color
|
||||
static const Color accent600 = Color(0xFFEA580C);
|
||||
static const Color accent700 = Color(0xFFC2410C);
|
||||
|
||||
// ============================================
|
||||
// SUCCESS (Green - Wins & Completions)
|
||||
// ============================================
|
||||
|
|
@ -71,6 +84,18 @@ class AppColors {
|
|||
static const Color purple600 = Color(0xFF9333EA);
|
||||
static const Color purple700 = Color(0xFF7C3AED);
|
||||
|
||||
// ============================================
|
||||
// TEAL (For Links & Dates - HubSpot style)
|
||||
// ============================================
|
||||
static const Color teal50 = Color(0xFFF0FDFA);
|
||||
static const Color teal100 = Color(0xFFCCFBF1);
|
||||
static const Color teal200 = Color(0xFF99F6E4);
|
||||
static const Color teal300 = Color(0xFF5EEAD4);
|
||||
static const Color teal400 = Color(0xFF2DD4BF);
|
||||
static const Color teal500 = Color(0xFF14B8A6);
|
||||
static const Color teal600 = Color(0xFF0D9488);
|
||||
static const Color teal700 = Color(0xFF0F766E);
|
||||
|
||||
// ============================================
|
||||
// NEUTRAL GRAYS
|
||||
// ============================================
|
||||
|
|
@ -89,7 +114,7 @@ class AppColors {
|
|||
// SURFACES
|
||||
// ============================================
|
||||
static const Color surface = Color(0xFFFFFFFF);
|
||||
static const Color surfaceDim = Color(0xFFF8FAFC);
|
||||
static const Color surfaceDim = Color(0xFFF9FAFB); // Page background
|
||||
static const Color surfaceBright = Color(0xFFFFFFFF);
|
||||
|
||||
// ============================================
|
||||
|
|
@ -131,6 +156,7 @@ class AppColors {
|
|||
static const Color priorityLow = gray400;
|
||||
static const Color priorityMedium = warning500;
|
||||
static const Color priorityHigh = danger500;
|
||||
static const Color priorityUrgent = purple500;
|
||||
|
||||
// ============================================
|
||||
// AVATAR BACKGROUND COLORS
|
||||
|
|
|
|||
|
|
@ -110,16 +110,16 @@ class AppTheme {
|
|||
}),
|
||||
),
|
||||
|
||||
// Floating Action Button
|
||||
// Floating Action Button (Accent color like HubSpot)
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: AppColors.primary600,
|
||||
backgroundColor: AppColors.accent500,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 4,
|
||||
focusElevation: 6,
|
||||
hoverElevation: 6,
|
||||
highlightElevation: 8,
|
||||
elevation: 2,
|
||||
focusElevation: 4,
|
||||
hoverElevation: 4,
|
||||
highlightElevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: AppLayout.borderRadiusLg,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
|
||||
|
|
@ -214,13 +214,14 @@ class AppTheme {
|
|||
suffixIconColor: AppColors.gray400,
|
||||
),
|
||||
|
||||
// Card
|
||||
// Card (Flat design with border, no shadow)
|
||||
cardTheme: CardThemeData(
|
||||
color: AppColors.card,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: AppLayout.borderRadiusLg,
|
||||
borderRadius: AppLayout.borderRadiusMd,
|
||||
side: const BorderSide(color: AppColors.border, width: 1),
|
||||
),
|
||||
),
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import 'package:google_fonts/google_fonts.dart';
|
|||
import 'app_colors.dart';
|
||||
|
||||
/// BottleCRM Typography System
|
||||
/// Using Plus Jakarta Sans - a modern, professional geometric sans-serif
|
||||
/// that's distinctive yet highly readable for data-dense interfaces
|
||||
/// Using Inter - a font designed specifically for screens
|
||||
/// with excellent readability at small sizes
|
||||
class AppTypography {
|
||||
AppTypography._();
|
||||
|
||||
|
|
@ -12,10 +12,10 @@ class AppTypography {
|
|||
// DISPLAY & HEADINGS
|
||||
// ============================================
|
||||
|
||||
/// Display - Hero text, large numbers
|
||||
/// 32px, Bold, 1.2 line height
|
||||
static TextStyle get display => GoogleFonts.plusJakartaSans(
|
||||
fontSize: 32,
|
||||
/// Display - KPIs, large numbers, hero text
|
||||
/// 28px, Bold, 1.2 line height
|
||||
static TextStyle get display => GoogleFonts.inter(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.2,
|
||||
color: AppColors.textPrimary,
|
||||
|
|
@ -23,19 +23,19 @@ class AppTypography {
|
|||
);
|
||||
|
||||
/// H1 - Main page titles
|
||||
/// 24px, Bold, 1.3 line height
|
||||
static TextStyle get h1 => GoogleFonts.plusJakartaSans(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
/// 22px, Semi-bold, 1.3 line height
|
||||
static TextStyle get h1 => GoogleFonts.inter(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.3,
|
||||
color: AppColors.textPrimary,
|
||||
letterSpacing: -0.3,
|
||||
);
|
||||
|
||||
/// H2 - Section titles
|
||||
/// 20px, Semi-bold, 1.4 line height
|
||||
static TextStyle get h2 => GoogleFonts.plusJakartaSans(
|
||||
fontSize: 20,
|
||||
/// 18px, Semi-bold, 1.4 line height
|
||||
static TextStyle get h2 => GoogleFonts.inter(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.4,
|
||||
color: AppColors.textPrimary,
|
||||
|
|
@ -43,9 +43,9 @@ class AppTypography {
|
|||
);
|
||||
|
||||
/// H3 - Card titles, subsections
|
||||
/// 18px, Semi-bold, 1.4 line height
|
||||
static TextStyle get h3 => GoogleFonts.plusJakartaSans(
|
||||
fontSize: 18,
|
||||
/// 16px, Semi-bold, 1.4 line height
|
||||
static TextStyle get h3 => GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.4,
|
||||
color: AppColors.textPrimary,
|
||||
|
|
@ -55,28 +55,28 @@ class AppTypography {
|
|||
// BODY TEXT
|
||||
// ============================================
|
||||
|
||||
/// Body Large - Prominent body text
|
||||
/// Body Large - Important body text
|
||||
/// 16px, Regular, 1.5 line height
|
||||
static TextStyle get bodyLarge => GoogleFonts.plusJakartaSans(
|
||||
static TextStyle get bodyLarge => GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.5,
|
||||
color: AppColors.textPrimary,
|
||||
);
|
||||
|
||||
/// Body - Standard body text
|
||||
/// 14px, Regular, 1.5 line height
|
||||
static TextStyle get body => GoogleFonts.plusJakartaSans(
|
||||
fontSize: 14,
|
||||
/// Body - Default body text
|
||||
/// 15px, Regular, 1.5 line height
|
||||
static TextStyle get body => GoogleFonts.inter(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.5,
|
||||
color: AppColors.textPrimary,
|
||||
);
|
||||
|
||||
/// Body Small - Secondary body text
|
||||
/// 12px, Regular, 1.4 line height
|
||||
static TextStyle get bodySmall => GoogleFonts.plusJakartaSans(
|
||||
fontSize: 12,
|
||||
/// 14px, Regular, 1.4 line height
|
||||
static TextStyle get bodySmall => GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.4,
|
||||
color: AppColors.textPrimary,
|
||||
|
|
@ -86,19 +86,19 @@ class AppTypography {
|
|||
// LABELS & UI TEXT
|
||||
// ============================================
|
||||
|
||||
/// Label - Input labels, button text
|
||||
/// 14px, Medium, 1.4 line height
|
||||
static TextStyle get label => GoogleFonts.plusJakartaSans(
|
||||
fontSize: 14,
|
||||
/// Label - Input labels, list item titles
|
||||
/// 15px, Medium, 1.4 line height
|
||||
static TextStyle get label => GoogleFonts.inter(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.4,
|
||||
color: AppColors.textPrimary,
|
||||
);
|
||||
|
||||
/// Label Small - Small labels, badges
|
||||
/// 12px, Medium, 1.3 line height
|
||||
static TextStyle get labelSmall => GoogleFonts.plusJakartaSans(
|
||||
fontSize: 12,
|
||||
/// Label Small - Small labels, nav items
|
||||
/// 13px, Medium, 1.3 line height
|
||||
static TextStyle get labelSmall => GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.3,
|
||||
color: AppColors.textPrimary,
|
||||
|
|
@ -110,17 +110,17 @@ class AppTypography {
|
|||
|
||||
/// Caption - Timestamps, helper text
|
||||
/// 12px, Regular, 1.3 line height, Secondary color
|
||||
static TextStyle get caption => GoogleFonts.plusJakartaSans(
|
||||
static TextStyle get caption => GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.3,
|
||||
color: AppColors.textSecondary,
|
||||
);
|
||||
|
||||
/// Overline - Section headers, category labels
|
||||
/// 12px, Semi-bold, 0.5 letter spacing, uppercase
|
||||
static TextStyle get overline => GoogleFonts.plusJakartaSans(
|
||||
fontSize: 12,
|
||||
/// Overline - Section labels, category headers
|
||||
/// 11px, Semi-bold, 0.5 letter spacing
|
||||
static TextStyle get overline => GoogleFonts.inter(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.3,
|
||||
letterSpacing: 0.5,
|
||||
|
|
@ -131,9 +131,9 @@ class AppTypography {
|
|||
// NUMBERS & DATA
|
||||
// ============================================
|
||||
|
||||
/// Number Large - KPI values, large amounts
|
||||
/// Number Large - Dashboard KPIs, large metrics
|
||||
/// 28px, Bold, Tabular figures
|
||||
static TextStyle get numberLarge => GoogleFonts.plusJakartaSans(
|
||||
static TextStyle get numberLarge => GoogleFonts.inter(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.2,
|
||||
|
|
@ -141,19 +141,19 @@ class AppTypography {
|
|||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
);
|
||||
|
||||
/// Number - Standard numeric values
|
||||
/// 16px, Semi-bold, Tabular figures
|
||||
static TextStyle get number => GoogleFonts.plusJakartaSans(
|
||||
fontSize: 16,
|
||||
/// Number Medium - Card metrics
|
||||
/// 20px, Semi-bold, Tabular figures
|
||||
static TextStyle get numberMedium => GoogleFonts.inter(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.3,
|
||||
color: AppColors.textPrimary,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
);
|
||||
|
||||
/// Number Small - Small numeric values, percentages
|
||||
/// Number Small - Inline numbers
|
||||
/// 14px, Medium, Tabular figures
|
||||
static TextStyle get numberSmall => GoogleFonts.plusJakartaSans(
|
||||
static TextStyle get numberSmall => GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.3,
|
||||
|
|
@ -166,9 +166,9 @@ class AppTypography {
|
|||
// ============================================
|
||||
|
||||
/// Button Large - Primary action buttons
|
||||
/// 16px, Semi-bold
|
||||
static TextStyle get buttonLarge => GoogleFonts.plusJakartaSans(
|
||||
fontSize: 16,
|
||||
/// 15px, Semi-bold
|
||||
static TextStyle get buttonLarge => GoogleFonts.inter(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.0,
|
||||
color: Colors.white,
|
||||
|
|
@ -176,7 +176,7 @@ class AppTypography {
|
|||
|
||||
/// Button - Standard buttons
|
||||
/// 14px, Semi-bold
|
||||
static TextStyle get button => GoogleFonts.plusJakartaSans(
|
||||
static TextStyle get button => GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.0,
|
||||
|
|
@ -184,14 +184,36 @@ class AppTypography {
|
|||
);
|
||||
|
||||
/// Button Small - Compact buttons
|
||||
/// 12px, Semi-bold
|
||||
static TextStyle get buttonSmall => GoogleFonts.plusJakartaSans(
|
||||
fontSize: 12,
|
||||
/// 13px, Semi-bold
|
||||
static TextStyle get buttonSmall => GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.0,
|
||||
color: Colors.white,
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// LINK TEXT
|
||||
// ============================================
|
||||
|
||||
/// Link - Clickable text links
|
||||
/// 14px, Medium, Primary color
|
||||
static TextStyle get link => GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.4,
|
||||
color: AppColors.primary600,
|
||||
);
|
||||
|
||||
/// Link Small - Small clickable links (e.g., "View all")
|
||||
/// 13px, Medium, Primary color
|
||||
static TextStyle get linkSmall => GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.3,
|
||||
color: AppColors.primary600,
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// HELPER METHODS
|
||||
// ============================================
|
||||
|
|
|
|||
|
|
@ -2,17 +2,14 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'core/theme/theme.dart';
|
||||
import 'providers/settings_provider.dart';
|
||||
import 'routes/app_router.dart';
|
||||
import 'services/auth_service.dart';
|
||||
import 'services/settings_service.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Initialize services
|
||||
await AuthService().initialize();
|
||||
await SettingsService().initialize();
|
||||
|
||||
// Set system UI overlay style
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
|
|
@ -38,48 +35,28 @@ void main() async {
|
|||
}
|
||||
|
||||
/// BottleCRM - Main Application Widget
|
||||
class BottleCRMApp extends ConsumerStatefulWidget {
|
||||
class BottleCRMApp extends ConsumerWidget {
|
||||
const BottleCRMApp({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<BottleCRMApp> createState() => _BottleCRMAppState();
|
||||
}
|
||||
|
||||
class _BottleCRMAppState extends ConsumerState<BottleCRMApp> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize settings provider from persisted values
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(settingsProvider.notifier).initialize();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final router = ref.watch(appRouterProvider);
|
||||
final fontScale = ref.watch(fontScaleProvider);
|
||||
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaler: TextScaler.linear(fontScale),
|
||||
),
|
||||
child: MaterialApp.router(
|
||||
title: 'BottleCRM',
|
||||
debugShowCheckedModeBanner: false,
|
||||
return MaterialApp.router(
|
||||
title: 'BottleCRM',
|
||||
debugShowCheckedModeBanner: false,
|
||||
|
||||
// Theme
|
||||
theme: AppTheme.light,
|
||||
darkTheme: AppTheme.dark,
|
||||
themeMode: ThemeMode.light,
|
||||
// Theme
|
||||
theme: AppTheme.light,
|
||||
darkTheme: AppTheme.dark,
|
||||
themeMode: ThemeMode.light,
|
||||
|
||||
// Router
|
||||
routerConfig: router,
|
||||
// Router
|
||||
routerConfig: router,
|
||||
|
||||
// Scroll behavior for smooth scrolling
|
||||
scrollBehavior: const MaterialScrollBehavior().copyWith(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
),
|
||||
// Scroll behavior for smooth scrolling
|
||||
scrollBehavior: const MaterialScrollBehavior().copyWith(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../services/settings_service.dart';
|
||||
|
||||
/// Re-export FontSizeOption for convenience
|
||||
export '../services/settings_service.dart' show FontSizeOption;
|
||||
|
||||
/// Settings state for the app
|
||||
class SettingsState {
|
||||
final FontSizeOption fontSize;
|
||||
|
||||
const SettingsState({
|
||||
this.fontSize = FontSizeOption.defaultSize,
|
||||
});
|
||||
|
||||
/// Initial state
|
||||
const SettingsState.initial() : fontSize = FontSizeOption.defaultSize;
|
||||
|
||||
/// Get font scale factor
|
||||
double get fontScale => fontSize.scale;
|
||||
|
||||
/// Create a copy with updated fields
|
||||
SettingsState copyWith({
|
||||
FontSizeOption? fontSize,
|
||||
}) {
|
||||
return SettingsState(
|
||||
fontSize: fontSize ?? this.fontSize,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'SettingsState(fontSize: ${fontSize.label})';
|
||||
}
|
||||
|
||||
/// Notifier for settings state changes
|
||||
class SettingsNotifier extends StateNotifier<SettingsState> {
|
||||
SettingsNotifier() : super(const SettingsState.initial());
|
||||
|
||||
final SettingsService _settingsService = SettingsService();
|
||||
|
||||
/// Initialize settings from storage (call on app launch)
|
||||
Future<void> initialize() async {
|
||||
debugPrint('SettingsNotifier: Initializing...');
|
||||
|
||||
// SettingsService.initialize() should have been called before this
|
||||
state = state.copyWith(
|
||||
fontSize: _settingsService.fontSize,
|
||||
);
|
||||
|
||||
debugPrint('SettingsNotifier: Initialized with font size: ${state.fontSize.label}');
|
||||
}
|
||||
|
||||
/// Set font size preference
|
||||
Future<void> setFontSize(FontSizeOption size) async {
|
||||
debugPrint('SettingsNotifier: Setting font size to ${size.label}...');
|
||||
|
||||
// Update state immediately for live preview
|
||||
state = state.copyWith(fontSize: size);
|
||||
|
||||
// Persist to storage
|
||||
await _settingsService.setFontSize(size);
|
||||
|
||||
debugPrint('SettingsNotifier: Font size set to ${size.label}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for settings state
|
||||
final settingsProvider = StateNotifierProvider<SettingsNotifier, SettingsState>((ref) {
|
||||
return SettingsNotifier();
|
||||
});
|
||||
|
||||
/// Convenience provider for font scale
|
||||
final fontScaleProvider = Provider<double>((ref) {
|
||||
return ref.watch(settingsProvider).fontScale;
|
||||
});
|
||||
|
||||
/// Convenience provider for current font size option
|
||||
final fontSizeOptionProvider = Provider<FontSizeOption>((ref) {
|
||||
return ref.watch(settingsProvider).fontSize;
|
||||
});
|
||||
|
|
@ -8,7 +8,6 @@ import '../providers/auth_provider.dart';
|
|||
import '../screens/auth/splash_screen.dart';
|
||||
import '../screens/auth/login_screen.dart';
|
||||
import '../screens/auth/forgot_password_screen.dart';
|
||||
import '../screens/auth/onboarding_screen.dart';
|
||||
import '../screens/auth/org_selection_screen.dart';
|
||||
|
||||
// Main Screens
|
||||
|
|
@ -36,7 +35,6 @@ class AppRoutes {
|
|||
static const String splash = '/splash';
|
||||
static const String login = '/login';
|
||||
static const String forgotPassword = '/forgot-password';
|
||||
static const String onboarding = '/onboarding';
|
||||
static const String orgSelection = '/org-selection';
|
||||
|
||||
// Main routes
|
||||
|
|
@ -66,7 +64,6 @@ const _publicRoutes = [
|
|||
AppRoutes.splash,
|
||||
AppRoutes.login,
|
||||
AppRoutes.forgotPassword,
|
||||
AppRoutes.onboarding,
|
||||
];
|
||||
|
||||
/// Routes that require authentication but not org selection
|
||||
|
|
@ -140,11 +137,6 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
|||
name: 'forgotPassword',
|
||||
builder: (context, state) => const ForgotPasswordScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: AppRoutes.onboarding,
|
||||
name: 'onboarding',
|
||||
builder: (context, state) => const OnboardingScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: AppRoutes.orgSelection,
|
||||
name: 'orgSelection',
|
||||
|
|
|
|||
|
|
@ -1,325 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../core/theme/theme.dart';
|
||||
import '../../routes/app_router.dart';
|
||||
import '../../widgets/common/common.dart';
|
||||
|
||||
/// Onboarding Screen
|
||||
/// 3-slide feature introduction carousel
|
||||
class OnboardingScreen extends StatefulWidget {
|
||||
const OnboardingScreen({super.key});
|
||||
|
||||
@override
|
||||
State<OnboardingScreen> createState() => _OnboardingScreenState();
|
||||
}
|
||||
|
||||
class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
final PageController _pageController = PageController();
|
||||
int _currentPage = 0;
|
||||
|
||||
final List<_OnboardingSlide> _slides = [
|
||||
_OnboardingSlide(
|
||||
icon: LucideIcons.users,
|
||||
iconColor: AppColors.primary600,
|
||||
backgroundColor: AppColors.primary100,
|
||||
title: 'Track Your Leads',
|
||||
description:
|
||||
'Manage and organize all your leads in one place. Never miss a follow-up again.',
|
||||
),
|
||||
_OnboardingSlide(
|
||||
icon: LucideIcons.trendingUp,
|
||||
iconColor: AppColors.success600,
|
||||
backgroundColor: AppColors.success100,
|
||||
title: 'Close More Deals',
|
||||
description:
|
||||
'Visualize your sales pipeline and move deals through stages effortlessly.',
|
||||
),
|
||||
_OnboardingSlide(
|
||||
icon: LucideIcons.calendar,
|
||||
iconColor: AppColors.warning600,
|
||||
backgroundColor: AppColors.warning100,
|
||||
title: 'Stay Organized',
|
||||
description:
|
||||
'Keep track of tasks, meetings, and deadlines. Boost your productivity.',
|
||||
),
|
||||
];
|
||||
|
||||
void _goToNextPage() {
|
||||
if (_currentPage < _slides.length - 1) {
|
||||
_pageController.nextPage(
|
||||
duration: AppDurations.slow,
|
||||
curve: AppCurves.defaultCurve,
|
||||
);
|
||||
} else {
|
||||
_completeOnboarding();
|
||||
}
|
||||
}
|
||||
|
||||
void _completeOnboarding() {
|
||||
context.go(AppRoutes.login);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surface,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Skip Button
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: GestureDetector(
|
||||
onTap: _completeOnboarding,
|
||||
child: Text(
|
||||
'Skip',
|
||||
style: AppTypography.label.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Page View
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
onPageChanged: (index) {
|
||||
setState(() => _currentPage = index);
|
||||
},
|
||||
itemCount: _slides.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildSlide(_slides[index], index);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom Section
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
// Dot Indicators
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
_slides.length,
|
||||
(index) => _buildDot(index),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Action Button
|
||||
PrimaryButton(
|
||||
label: _currentPage == _slides.length - 1
|
||||
? 'Get Started'
|
||||
: 'Next',
|
||||
onPressed: _goToNextPage,
|
||||
icon: _currentPage == _slides.length - 1
|
||||
? LucideIcons.arrowRight
|
||||
: null,
|
||||
iconRight: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSlide(_OnboardingSlide slide, int index) {
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
duration: AppDurations.slow,
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 20 * (1 - value)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Illustration
|
||||
_buildIllustration(slide),
|
||||
|
||||
const SizedBox(height: 48),
|
||||
|
||||
// Title
|
||||
Text(
|
||||
slide.title,
|
||||
style: AppTypography.h1.copyWith(
|
||||
fontSize: 28,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Description
|
||||
Text(
|
||||
slide.description,
|
||||
style: AppTypography.body.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
height: 1.6,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIllustration(_OnboardingSlide slide) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Background decorative circles
|
||||
Container(
|
||||
width: 280,
|
||||
height: 280,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: slide.backgroundColor.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 220,
|
||||
height: 220,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: slide.backgroundColor.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
// Main circle with icon
|
||||
Container(
|
||||
width: 160,
|
||||
height: 160,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: slide.backgroundColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: slide.iconColor.withValues(alpha: 0.3),
|
||||
blurRadius: 30,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
slide.icon,
|
||||
size: 72,
|
||||
color: slide.iconColor,
|
||||
),
|
||||
),
|
||||
// Floating accent elements
|
||||
Positioned(
|
||||
top: 40,
|
||||
right: 50,
|
||||
child: _buildFloatingElement(
|
||||
size: 20,
|
||||
color: slide.iconColor.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 60,
|
||||
left: 40,
|
||||
child: _buildFloatingElement(
|
||||
size: 16,
|
||||
color: slide.iconColor.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 80,
|
||||
left: 60,
|
||||
child: _buildFloatingElement(
|
||||
size: 12,
|
||||
color: slide.iconColor.withValues(alpha: 0.5),
|
||||
isSquare: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFloatingElement({
|
||||
required double size,
|
||||
required Color color,
|
||||
bool isSquare = false,
|
||||
}) {
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
curve: Curves.elasticOut,
|
||||
builder: (context, value, child) {
|
||||
return Transform.scale(
|
||||
scale: value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius:
|
||||
isSquare ? BorderRadius.circular(4) : BorderRadius.circular(size),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDot(int index) {
|
||||
final isActive = index == _currentPage;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_pageController.animateToPage(
|
||||
index,
|
||||
duration: AppDurations.slow,
|
||||
curve: AppCurves.defaultCurve,
|
||||
);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: AppDurations.normal,
|
||||
curve: AppCurves.defaultCurve,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
height: 8,
|
||||
width: isActive ? 24 : 8,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? AppColors.primary600 : AppColors.gray300,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OnboardingSlide {
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final Color backgroundColor;
|
||||
final String title;
|
||||
final String description;
|
||||
|
||||
const _OnboardingSlide({
|
||||
required this.icon,
|
||||
required this.iconColor,
|
||||
required this.backgroundColor,
|
||||
required this.title,
|
||||
required this.description,
|
||||
});
|
||||
}
|
||||
|
|
@ -82,8 +82,8 @@ class _SplashScreenState extends ConsumerState<SplashScreen>
|
|||
context.go(AppRoutes.dashboard);
|
||||
}
|
||||
} else {
|
||||
// User is not logged in, go to onboarding/login
|
||||
context.go(AppRoutes.onboarding);
|
||||
// User is not logged in, go to login
|
||||
context.go(AppRoutes.login);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import '../../core/theme/theme.dart';
|
|||
import '../../data/models/models.dart';
|
||||
import '../../data/mock/mock_data.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../providers/settings_provider.dart';
|
||||
import '../../routes/app_router.dart';
|
||||
import '../../widgets/common/common.dart';
|
||||
|
||||
|
|
@ -76,10 +75,6 @@ class _MoreScreenState extends ConsumerState<MoreScreen> {
|
|||
);
|
||||
},
|
||||
),
|
||||
_FontSizeMenuItem(
|
||||
currentSize: ref.watch(fontSizeOptionProvider),
|
||||
onTap: () => _showFontSizePicker(),
|
||||
),
|
||||
_MenuItem(
|
||||
icon: LucideIcons.globe,
|
||||
label: 'Language',
|
||||
|
|
@ -310,47 +305,6 @@ class _MoreScreenState extends ConsumerState<MoreScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
void _showFontSizePicker() {
|
||||
final currentSize = ref.read(fontSizeOptionProvider);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: AppColors.surface,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.gray300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text('Font Size', style: AppTypography.h3),
|
||||
),
|
||||
...FontSizeOption.values.map((option) => _FontSizeOption(
|
||||
option: option,
|
||||
isSelected: currentSize == option,
|
||||
onTap: () {
|
||||
ref.read(settingsProvider.notifier).setFontSize(option);
|
||||
Navigator.pop(sheetContext);
|
||||
},
|
||||
)),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSignOut() {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
|
@ -578,126 +532,3 @@ class _LanguageOption extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// Font Size Menu Item Widget
|
||||
class _FontSizeMenuItem extends StatelessWidget {
|
||||
final FontSizeOption currentSize;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _FontSizeMenuItem({
|
||||
required this.currentSize,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: AppColors.gray100),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icon
|
||||
Icon(
|
||||
LucideIcons.type,
|
||||
size: 22,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
|
||||
const SizedBox(width: 14),
|
||||
|
||||
// Label
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Font Size',
|
||||
style: AppTypography.body.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Current Value
|
||||
Text(
|
||||
currentSize.label,
|
||||
style: AppTypography.body.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Chevron
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
size: 20,
|
||||
color: AppColors.textTertiary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Font Size Option Widget
|
||||
class _FontSizeOption extends StatelessWidget {
|
||||
final FontSizeOption option;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _FontSizeOption({
|
||||
required this.option,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
option.label,
|
||||
style: AppTypography.body.copyWith(
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: isSelected
|
||||
? AppColors.primary600
|
||||
: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Preview text
|
||||
Text(
|
||||
'Aa',
|
||||
style: TextStyle(
|
||||
fontSize: 14 * option.scale,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
Icon(
|
||||
LucideIcons.check,
|
||||
size: 20,
|
||||
color: AppColors.primary600,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Font size options for the app
|
||||
enum FontSizeOption {
|
||||
small('Small', 1.0),
|
||||
defaultSize('Default', 1.25),
|
||||
large('Large', 1.5);
|
||||
|
||||
const FontSizeOption(this.label, this.scale);
|
||||
final String label;
|
||||
final double scale;
|
||||
|
||||
/// Get FontSizeOption from stored string value
|
||||
static FontSizeOption fromString(String? value) {
|
||||
switch (value) {
|
||||
case 'small':
|
||||
return FontSizeOption.small;
|
||||
case 'large':
|
||||
return FontSizeOption.large;
|
||||
default:
|
||||
return FontSizeOption.defaultSize;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get string value for storage
|
||||
String toStorageString() {
|
||||
switch (this) {
|
||||
case FontSizeOption.small:
|
||||
return 'small';
|
||||
case FontSizeOption.large:
|
||||
return 'large';
|
||||
case FontSizeOption.defaultSize:
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings service for persisting app preferences
|
||||
class SettingsService {
|
||||
static final SettingsService _instance = SettingsService._internal();
|
||||
factory SettingsService() => _instance;
|
||||
SettingsService._internal();
|
||||
|
||||
// Storage keys
|
||||
static const String _fontSizeKey = 'font_size_preference';
|
||||
|
||||
// State
|
||||
FontSizeOption _fontSize = FontSizeOption.defaultSize;
|
||||
|
||||
// Getters
|
||||
FontSizeOption get fontSize => _fontSize;
|
||||
double get fontScale => _fontSize.scale;
|
||||
|
||||
/// Initialize the settings service - call on app startup
|
||||
Future<void> initialize() async {
|
||||
debugPrint('SettingsService: Initializing...');
|
||||
await _loadFromStorage();
|
||||
debugPrint('SettingsService: Initialized with font size: ${_fontSize.label}');
|
||||
}
|
||||
|
||||
/// Set font size preference
|
||||
Future<void> setFontSize(FontSizeOption size) async {
|
||||
_fontSize = size;
|
||||
await _saveToStorage();
|
||||
debugPrint('SettingsService: Font size set to ${size.label}');
|
||||
}
|
||||
|
||||
/// Load settings from SharedPreferences
|
||||
Future<void> _loadFromStorage() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final fontSizeValue = prefs.getString(_fontSizeKey);
|
||||
_fontSize = FontSizeOption.fromString(fontSizeValue);
|
||||
} catch (e) {
|
||||
debugPrint('SettingsService: Error loading settings: $e');
|
||||
_fontSize = FontSizeOption.defaultSize;
|
||||
}
|
||||
}
|
||||
|
||||
/// Save settings to SharedPreferences
|
||||
Future<void> _saveToStorage() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_fontSizeKey, _fontSize.toStorageString());
|
||||
} catch (e) {
|
||||
debugPrint('SettingsService: Error saving settings: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue