This commit is contained in:
Ashwin 2026-01-19 17:25:18 +05:30
parent 3d7b269ff1
commit 12e6398d01
11 changed files with 825 additions and 775 deletions

696
mobile/DESIGN_SYSTEM.md Normal file
View 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.*

View file

@ -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

View file

@ -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),
),
),

View file

@ -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
// ============================================

View file

@ -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(),
),
);
}

View file

@ -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;
});

View file

@ -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',

View file

@ -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,
});
}

View file

@ -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);
}
}

View file

@ -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,
),
],
),
),
);
}
}

View file

@ -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');
}
}
}