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(
|
||||
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(
|
||||
child: Row(
|
||||
child: Column(
|
||||
children: [
|
||||
// ── Side Navigation ──
|
||||
// Header
|
||||
Container(
|
||||
width: 72,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: kSurfaceCard,
|
||||
color: kSurfaceBg,
|
||||
border: Border(
|
||||
right: BorderSide(color: kDivider, width: 1),
|
||||
bottom: BorderSide(color: kDivider, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
child: Row(
|
||||
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(
|
||||
'YNAB\nLite',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: kGreenAccent,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.2,
|
||||
_navItems[selectedIndex].label,
|
||||
style: const TextStyle(
|
||||
color: kTextPrimary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
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(),
|
||||
// Settings at bottom
|
||||
_NavRailItem(
|
||||
icon: Icons.settings_outlined,
|
||||
selectedIcon: Icons.settings,
|
||||
label: 'Paramètres',
|
||||
active: false,
|
||||
onTap: () {},
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, color: kGreenAccent),
|
||||
onPressed: () {},
|
||||
tooltip: 'Ajouter une transaction',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search, color: kTextSecondary),
|
||||
onPressed: () {},
|
||||
tooltip: 'Rechercher',
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
// ── Main Content ──
|
||||
// Content
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
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],
|
||||
),
|
||||
),
|
||||
],
|
||||
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});
|
||||
}
|
||||
|
||||
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 {
|
||||
AppDatabase() : super(_openConnection());
|
||||
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(name: 'ynab_lite');
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ class YnabLiteApp extends StatelessWidget {
|
|||
return MaterialApp(
|
||||
title: 'YNAB Lite',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ynabDarkTheme,
|
||||
theme: ynabTheme,
|
||||
darkTheme: ynabDarkTheme,
|
||||
themeMode: ThemeMode.system,
|
||||
home: const YnabApp(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,23 +2,23 @@ import 'package:flutter/material.dart';
|
|||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
// ── Couleurs custom ───────────────────────────────────────────────────────────
|
||||
// Fond très sombre mais pas noir, vert fluo pour accent, elevation uniquement.
|
||||
const kSurfaceBg = Color(0xFF12161C);
|
||||
const kSurfaceCard = Color(0xFF1A1E26);
|
||||
const kSurfaceElevated = Color(0xFF232730);
|
||||
const kGreenAccent = Color(0xFF00E676);
|
||||
const kGreenAccentDim = Color(0xFF00B85C);
|
||||
const kRedAccent = Color(0xFFFF5252);
|
||||
const kTextPrimary = Color(0xFFE8EAED);
|
||||
const kTextSecondary = Color(0xFF9AA0A6);
|
||||
const kDivider = Color(0xFF2C3038);
|
||||
// Couleurs custom pour le thème vert sage (Light Theme)
|
||||
const kSurfaceBg = Color(0xFFEDF1E7); // Fond vert très clair (de l'image)
|
||||
const kSurfaceCard = Color(0xFFF6F9F6); // Fond de carte off-white
|
||||
const kSurfaceElevated = Color(0xFFCAD8C3); // Sage green pour les surfaces surélevées
|
||||
const kGreenAccent = Color(0xFF3B5238); // Vert sombre pour le texte et les accents
|
||||
const kGreenAccentDim = Color(0xFF556B52);
|
||||
const kRedAccent = Color(0xFFBA1A1A); // Rouge d'erreur Material 3
|
||||
const kTextPrimary = Color(0xFF191D17); // Texte principal sombre
|
||||
const kTextSecondary = Color(0xFF43493E); // Texte secondaire gris-vert
|
||||
const kDivider = Color(0xFFDDE4D7); // Séparateurs clairs
|
||||
|
||||
final ThemeData ynabDarkTheme = ThemeData(
|
||||
final ThemeData ynabTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
brightness: Brightness.light,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: kGreenAccent,
|
||||
brightness: Brightness.dark,
|
||||
seedColor: const Color(0xFFCAD8C3),
|
||||
brightness: Brightness.light,
|
||||
surface: kSurfaceBg,
|
||||
primary: kGreenAccent,
|
||||
secondary: kGreenAccentDim,
|
||||
|
|
@ -30,27 +30,35 @@ final ThemeData ynabDarkTheme = ThemeData(
|
|||
scaffoldBackgroundColor: kSurfaceBg,
|
||||
cardTheme: CardThemeData(
|
||||
color: kSurfaceCard,
|
||||
elevation: 2,
|
||||
elevation: 0, // M3 utilise plutôt les tonalités et outline qu'une grosse élévation
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: kDivider, width: 1),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
),
|
||||
navigationRailTheme: NavigationRailThemeData(
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: kSurfaceCard,
|
||||
indicatorColor: kGreenAccent.withAlpha(40),
|
||||
unselectedIconTheme: const IconThemeData(color: kTextSecondary),
|
||||
selectedIconTheme: const IconThemeData(color: kGreenAccent),
|
||||
unselectedLabelTextStyle: GoogleFonts.inter(
|
||||
color: kTextSecondary,
|
||||
fontSize: 11,
|
||||
),
|
||||
selectedLabelTextStyle: GoogleFonts.inter(
|
||||
color: kGreenAccent,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
labelType: NavigationRailLabelType.none,
|
||||
indicatorColor: const Color(0xFFCAD8C3),
|
||||
iconTheme: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return const IconThemeData(color: Color(0xFF191D17));
|
||||
}
|
||||
return const IconThemeData(color: Color(0xFF43493E));
|
||||
}),
|
||||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return GoogleFonts.inter(
|
||||
color: const Color(0xFF191D17),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
}
|
||||
return GoogleFonts.inter(
|
||||
color: const Color(0xFF43493E),
|
||||
fontSize: 12,
|
||||
);
|
||||
}),
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: kSurfaceBg,
|
||||
|
|
@ -65,7 +73,7 @@ final ThemeData ynabDarkTheme = ThemeData(
|
|||
iconTheme: const IconThemeData(color: kTextSecondary),
|
||||
),
|
||||
textTheme: GoogleFonts.interTextTheme(
|
||||
ThemeData.dark().textTheme.copyWith(
|
||||
ThemeData.light().textTheme.copyWith(
|
||||
headlineLarge: const TextStyle(color: kTextPrimary),
|
||||
headlineMedium: const TextStyle(color: kTextPrimary),
|
||||
headlineSmall: const TextStyle(color: kTextPrimary),
|
||||
|
|
@ -82,17 +90,17 @@ final ThemeData ynabDarkTheme = ThemeData(
|
|||
),
|
||||
dividerTheme: const DividerThemeData(color: kDivider, thickness: 1),
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: kGreenAccent,
|
||||
foregroundColor: kSurfaceBg,
|
||||
elevation: 4,
|
||||
backgroundColor: const Color(0xFFCAD8C3),
|
||||
foregroundColor: kTextPrimary,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: kSurfaceElevated,
|
||||
fillColor: kSurfaceCard,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
borderSide: const BorderSide(color: kDivider),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
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
|
||||
String formatCurrency(double amount) {
|
||||
final sign = amount < 0 ? '-' : '';
|
||||
|
|
|
|||
|
|
@ -3,19 +3,29 @@ import 'package:ynab_lite/app.dart';
|
|||
import 'package:ynab_lite/theme/app_theme.dart';
|
||||
import 'package:flutter/material.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() {
|
||||
testWidgets('App renders dashboard by default', (tester) async {
|
||||
final db = AppDatabase(NativeDatabase.memory());
|
||||
await tester.pumpWidget(
|
||||
const ProviderScope(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
dbProvider.overrideWithValue(db),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: ynabDarkTheme,
|
||||
home: YnabApp(),
|
||||
theme: ynabTheme,
|
||||
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);
|
||||
await db.close();
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue