style: switch to Material 3 light sage theme and bottom NavigationBar
This commit is contained in:
parent
9df372ebb7
commit
644da80c4e
178
lib/app.dart
178
lib/app.dart
|
|
@ -42,114 +42,60 @@ class _YnabAppState extends ConsumerState<YnabApp> {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: kSurfaceBg,
|
backgroundColor: kSurfaceBg,
|
||||||
|
bottomNavigationBar: NavigationBar(
|
||||||
|
selectedIndex: selectedIndex,
|
||||||
|
onDestinationSelected: (int index) {
|
||||||
|
ref.read(_selectedIndexProvider.notifier).state = index;
|
||||||
|
},
|
||||||
|
destinations: _navItems.map((item) {
|
||||||
|
return NavigationDestination(
|
||||||
|
icon: Icon(item.icon),
|
||||||
|
selectedIcon: Icon(item.selectedIcon),
|
||||||
|
label: item.label,
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Row(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// ── Side Navigation ──
|
// Header
|
||||||
Container(
|
Container(
|
||||||
width: 72,
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: kSurfaceCard,
|
color: kSurfaceBg,
|
||||||
border: Border(
|
border: Border(
|
||||||
right: BorderSide(color: kDivider, width: 1),
|
bottom: BorderSide(color: kDivider, width: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 20),
|
|
||||||
// App logo / icon
|
|
||||||
Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: kGreenAccent.withAlpha(30),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(Icons.currency_bitcoin, color: kGreenAccent, size: 22),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
Text(
|
||||||
'YNAB\nLite',
|
_navItems[selectedIndex].label,
|
||||||
textAlign: TextAlign.center,
|
style: const TextStyle(
|
||||||
style: TextStyle(
|
color: kTextPrimary,
|
||||||
fontSize: 9,
|
fontSize: 18,
|
||||||
color: kGreenAccent,
|
fontWeight: FontWeight.w600,
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
height: 1.2,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 28),
|
|
||||||
// Navigation items
|
|
||||||
...List.generate(_navItems.length, (i) {
|
|
||||||
final item = _navItems[i];
|
|
||||||
final active = i == selectedIndex;
|
|
||||||
return _NavRailItem(
|
|
||||||
icon: active ? item.selectedIcon : item.icon,
|
|
||||||
label: item.label,
|
|
||||||
active: active,
|
|
||||||
onTap: () => ref.read(_selectedIndexProvider.notifier).state = i,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
// Settings at bottom
|
IconButton(
|
||||||
_NavRailItem(
|
icon: const Icon(Icons.add, color: kGreenAccent),
|
||||||
icon: Icons.settings_outlined,
|
onPressed: () {},
|
||||||
selectedIcon: Icons.settings,
|
tooltip: 'Ajouter une transaction',
|
||||||
label: 'Paramètres',
|
),
|
||||||
active: false,
|
IconButton(
|
||||||
onTap: () {},
|
icon: const Icon(Icons.search, color: kTextSecondary),
|
||||||
|
onPressed: () {},
|
||||||
|
tooltip: 'Rechercher',
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// ── Main Content ──
|
// Content
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: AnimatedSwitcher(
|
||||||
children: [
|
duration: const Duration(milliseconds: 200),
|
||||||
// Header
|
child: _screens[selectedIndex],
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: kSurfaceBg,
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(color: kDivider, width: 0.5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
_navItems[selectedIndex].label,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: kTextPrimary,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.add, color: kGreenAccent),
|
|
||||||
onPressed: () {},
|
|
||||||
tooltip: 'Ajouter une transaction',
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.search, color: kTextSecondary),
|
|
||||||
onPressed: () {},
|
|
||||||
tooltip: 'Rechercher',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Content
|
|
||||||
Expanded(
|
|
||||||
child: AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
child: _screens[selectedIndex],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -166,53 +112,3 @@ class _NavItem {
|
||||||
const _NavItem({required this.icon, required this.selectedIcon, required this.label});
|
const _NavItem({required this.icon, required this.selectedIcon, required this.label});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NavRailItem extends StatelessWidget {
|
|
||||||
final IconData icon;
|
|
||||||
final IconData? selectedIcon;
|
|
||||||
final String label;
|
|
||||||
final bool active;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
|
|
||||||
const _NavRailItem({
|
|
||||||
required this.icon,
|
|
||||||
this.selectedIcon,
|
|
||||||
required this.label,
|
|
||||||
required this.active,
|
|
||||||
required this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
||||||
child: Tooltip(
|
|
||||||
message: label,
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: onTap,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Container(
|
|
||||||
width: 56,
|
|
||||||
height: 48,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
color: active ? kGreenAccent.withAlpha(20) : null,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
size: 22,
|
|
||||||
color: active ? kGreenAccent : kTextSecondary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ part 'db.g.dart';
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase() : super(_openConnection());
|
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
|
||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
static QueryExecutor _openConnection() {
|
||||||
return driftDatabase(name: 'ynab_lite');
|
return driftDatabase(name: 'ynab_lite');
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ class YnabLiteApp extends StatelessWidget {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'YNAB Lite',
|
title: 'YNAB Lite',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ynabDarkTheme,
|
theme: ynabTheme,
|
||||||
|
darkTheme: ynabDarkTheme,
|
||||||
|
themeMode: ThemeMode.system,
|
||||||
home: const YnabApp(),
|
home: const YnabApp(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,23 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
// ── Couleurs custom ───────────────────────────────────────────────────────────
|
// ── Couleurs custom ───────────────────────────────────────────────────────────
|
||||||
// Fond très sombre mais pas noir, vert fluo pour accent, elevation uniquement.
|
// Couleurs custom pour le thème vert sage (Light Theme)
|
||||||
const kSurfaceBg = Color(0xFF12161C);
|
const kSurfaceBg = Color(0xFFEDF1E7); // Fond vert très clair (de l'image)
|
||||||
const kSurfaceCard = Color(0xFF1A1E26);
|
const kSurfaceCard = Color(0xFFF6F9F6); // Fond de carte off-white
|
||||||
const kSurfaceElevated = Color(0xFF232730);
|
const kSurfaceElevated = Color(0xFFCAD8C3); // Sage green pour les surfaces surélevées
|
||||||
const kGreenAccent = Color(0xFF00E676);
|
const kGreenAccent = Color(0xFF3B5238); // Vert sombre pour le texte et les accents
|
||||||
const kGreenAccentDim = Color(0xFF00B85C);
|
const kGreenAccentDim = Color(0xFF556B52);
|
||||||
const kRedAccent = Color(0xFFFF5252);
|
const kRedAccent = Color(0xFFBA1A1A); // Rouge d'erreur Material 3
|
||||||
const kTextPrimary = Color(0xFFE8EAED);
|
const kTextPrimary = Color(0xFF191D17); // Texte principal sombre
|
||||||
const kTextSecondary = Color(0xFF9AA0A6);
|
const kTextSecondary = Color(0xFF43493E); // Texte secondaire gris-vert
|
||||||
const kDivider = Color(0xFF2C3038);
|
const kDivider = Color(0xFFDDE4D7); // Séparateurs clairs
|
||||||
|
|
||||||
final ThemeData ynabDarkTheme = ThemeData(
|
final ThemeData ynabTheme = ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.light,
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
seedColor: kGreenAccent,
|
seedColor: const Color(0xFFCAD8C3),
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.light,
|
||||||
surface: kSurfaceBg,
|
surface: kSurfaceBg,
|
||||||
primary: kGreenAccent,
|
primary: kGreenAccent,
|
||||||
secondary: kGreenAccentDim,
|
secondary: kGreenAccentDim,
|
||||||
|
|
@ -30,27 +30,35 @@ final ThemeData ynabDarkTheme = ThemeData(
|
||||||
scaffoldBackgroundColor: kSurfaceBg,
|
scaffoldBackgroundColor: kSurfaceBg,
|
||||||
cardTheme: CardThemeData(
|
cardTheme: CardThemeData(
|
||||||
color: kSurfaceCard,
|
color: kSurfaceCard,
|
||||||
elevation: 2,
|
elevation: 0, // M3 utilise plutôt les tonalités et outline qu'une grosse élévation
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(color: kDivider, width: 1),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
),
|
),
|
||||||
navigationRailTheme: NavigationRailThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
backgroundColor: kSurfaceCard,
|
backgroundColor: kSurfaceCard,
|
||||||
indicatorColor: kGreenAccent.withAlpha(40),
|
indicatorColor: const Color(0xFFCAD8C3),
|
||||||
unselectedIconTheme: const IconThemeData(color: kTextSecondary),
|
iconTheme: WidgetStateProperty.resolveWith((states) {
|
||||||
selectedIconTheme: const IconThemeData(color: kGreenAccent),
|
if (states.contains(WidgetState.selected)) {
|
||||||
unselectedLabelTextStyle: GoogleFonts.inter(
|
return const IconThemeData(color: Color(0xFF191D17));
|
||||||
color: kTextSecondary,
|
}
|
||||||
fontSize: 11,
|
return const IconThemeData(color: Color(0xFF43493E));
|
||||||
),
|
}),
|
||||||
selectedLabelTextStyle: GoogleFonts.inter(
|
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||||
color: kGreenAccent,
|
if (states.contains(WidgetState.selected)) {
|
||||||
fontSize: 11,
|
return GoogleFonts.inter(
|
||||||
fontWeight: FontWeight.w600,
|
color: const Color(0xFF191D17),
|
||||||
),
|
fontSize: 12,
|
||||||
labelType: NavigationRailLabelType.none,
|
fontWeight: FontWeight.w600,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return GoogleFonts.inter(
|
||||||
|
color: const Color(0xFF43493E),
|
||||||
|
fontSize: 12,
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
backgroundColor: kSurfaceBg,
|
backgroundColor: kSurfaceBg,
|
||||||
|
|
@ -65,7 +73,7 @@ final ThemeData ynabDarkTheme = ThemeData(
|
||||||
iconTheme: const IconThemeData(color: kTextSecondary),
|
iconTheme: const IconThemeData(color: kTextSecondary),
|
||||||
),
|
),
|
||||||
textTheme: GoogleFonts.interTextTheme(
|
textTheme: GoogleFonts.interTextTheme(
|
||||||
ThemeData.dark().textTheme.copyWith(
|
ThemeData.light().textTheme.copyWith(
|
||||||
headlineLarge: const TextStyle(color: kTextPrimary),
|
headlineLarge: const TextStyle(color: kTextPrimary),
|
||||||
headlineMedium: const TextStyle(color: kTextPrimary),
|
headlineMedium: const TextStyle(color: kTextPrimary),
|
||||||
headlineSmall: const TextStyle(color: kTextPrimary),
|
headlineSmall: const TextStyle(color: kTextPrimary),
|
||||||
|
|
@ -82,17 +90,17 @@ final ThemeData ynabDarkTheme = ThemeData(
|
||||||
),
|
),
|
||||||
dividerTheme: const DividerThemeData(color: kDivider, thickness: 1),
|
dividerTheme: const DividerThemeData(color: kDivider, thickness: 1),
|
||||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||||
backgroundColor: kGreenAccent,
|
backgroundColor: const Color(0xFFCAD8C3),
|
||||||
foregroundColor: kSurfaceBg,
|
foregroundColor: kTextPrimary,
|
||||||
elevation: 4,
|
elevation: 2,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
),
|
),
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: kSurfaceElevated,
|
fillColor: kSurfaceCard,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide.none,
|
borderSide: const BorderSide(color: kDivider),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
|
@ -118,6 +126,15 @@ final ThemeData ynabDarkTheme = ThemeData(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final ThemeData ynabDarkTheme = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
seedColor: const Color(0xFFCAD8C3),
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
String formatCurrency(double amount) {
|
String formatCurrency(double amount) {
|
||||||
final sign = amount < 0 ? '-' : '';
|
final sign = amount < 0 ? '-' : '';
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,29 @@ import 'package:ynab_lite/app.dart';
|
||||||
import 'package:ynab_lite/theme/app_theme.dart';
|
import 'package:ynab_lite/theme/app_theme.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:ynab_lite/providers/db_provider.dart';
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:ynab_lite/data/db.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('App renders dashboard by default', (tester) async {
|
testWidgets('App renders dashboard by default', (tester) async {
|
||||||
|
final db = AppDatabase(NativeDatabase.memory());
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
const ProviderScope(
|
ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
dbProvider.overrideWithValue(db),
|
||||||
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
theme: ynabDarkTheme,
|
theme: ynabTheme,
|
||||||
home: YnabApp(),
|
home: const YnabApp(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('Tableau de bord'), findsOneWidget);
|
expect(find.text('Tableau de bord'), findsNWidgets(2));
|
||||||
expect(find.text('Solde total'), findsOneWidget);
|
expect(find.text('Solde total'), findsOneWidget);
|
||||||
|
await db.close();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue