Skip to content

Commit 6f533bc

Browse files
Facelift changes and model loading UX fix (#1)
* chore: add google_fonts dependency Required for the Manrope + Inter dual-font system specified in the Koshika design spec. * refactor: expand AppColors with semantic, text, and category tokens Add success/warning/info semantic colors, textPrimary/Secondary/Muted aliases, surfaceContainerHighest, onErrorContainer, onTertiaryContainer, and 10 health category colors with a categoryColor() lookup helper. * feat: create koshika_design_system with typography, spacing, radius, and elevation tokens Adds KoshikaTypography (Manrope headlines + Inter body with 15 Material styles and 6 custom health styles), KoshikaSpacing (8dp grid), KoshikaRadius (sm through pill), KoshikaElevation (tinted BoxShadow presets), and pre-built card decorations and button styles. * refactor: remove dark theme, replace ColorScheme.fromSeed with explicit scheme, wire fonts Switch to light-only theme (ThemeMode.light), replace the seed-generated ColorScheme with an explicit one mapped to AppColors tokens, wire Manrope + Inter via KoshikaTypography.textTheme, set card elevation to 0, and add pill/outlined button theme defaults. * feat: add KoshikaCard, StatusBadge, and IconContainer widgets KoshikaCard provides the standard card container with 24px radius, asymmetric editorial padding, and no-border design. StatusBadge maps BiomarkerFlag to semantic pill badges with icons. IconContainer wraps icons in colored rounded-rect backgrounds for list items. * feat: add shimmer loading widgets Adds ShimmerScope (shared AnimationController), ShimmerBox, ShimmerLine, and ShimmerCircle for skeleton loading states. Uses a horizontal gradient sweep on a 1500ms linear repeat cycle. * feat: add trend line sparkline painter and haptic feedback service SimpleTrendLinePainter is a lightweight CustomPainter for compact sparklines in dashboard category cards. Haptics wraps HapticFeedback with light, selection, and heavy static methods. * refactor: migrate dashboard to design system tokens and no-line rule Replace hardcoded colors, typography, spacing, and radii with KoshikaTypography, KoshikaSpacing, KoshikaRadius, and KoshikaDecorations. Hero card uses gradient decoration and heroMetric typography. Category trend cards now use health category colors. Insights card uses tertiaryContainer (insight) decoration. Section headers use sectionHeader style. Border.all removed from insights card (no-line rule). * refactor: migrate dashboard_summary_card and flag_badge to design system Replace hardcoded Colors.green/amber/orange/red/grey with AppColors semantic tokens. Remove Dividers and Border.all (no-line rule). Use KoshikaTypography, KoshikaDecorations, KoshikaRadius, and KoshikaSpacing. * refactor: migrate biomarker detail screen to design system Replace CircularProgressIndicator with shimmer skeleton loading. Use heroMetric typography for the main biomarker value and StatusBadge pill for flag display. Add LATEST RESULT and REFERENCE RANGE metric labels. History list uses alternating row backgrounds instead of Divider separators. All spacing, radii, and colors use design system tokens. * refactor: migrate biomarker_trend_chart and reference_range_gauge to design system Replace hardcoded Colors.green/red/orange with AppColors.success/error/warning. Use AppColors.secondary for chart line color. Replace Card with Container using KoshikaDecorations.card. Remove dead isDark branch in gauge painter. Zone labels use ALL-CAPS with design system-aligned styling. * refactor: redesign chat UI with glassmorphic input bar and accent-strip bubbles AI bubbles get 4px left accent strip and KOSHIKA INTELLIGENCE label. Chat input bar uses BackdropFilter for glassmorphic effect with privacy label. All state views (download, loading, ready, error, empty) migrated to design system tokens. Removed borders per no-line rule. * refactor: redesign reports, report detail, and settings screens Reports list uses KoshikaCard containers instead of Card widgets. Report detail uses metricLabel for category headers, removes Dividers. Settings replaces ListTiles with custom SettingsRow using IconContainer, model tiles use KoshikaDecorations.card with switch expressions, and Dividers replaced with spacing per no-line rule. * refactor: redesign onboarding with gradient CTA and design system tokens Onboarding pages use design system typography and spacing. Action button gets gradient pill decoration with medium elevation shadow. Page indicator dots use AppColors tokens. Icon circles use primary at 15% opacity for a lighter, editorial feel. * feat: redesign bottom navigation with glassmorphic bar and animated icons Custom bottom nav replaces NavigationBar with glassmorphic backdrop blur (white @ 85%), animated icon transitions between outlined and filled states, and primaryContainer pill indicator behind selected item. Uses extendBody for content-behind-nav effect. * fix: resolve model SDK re-registration and clean up error messages GemmaService and EmbeddingService now call installModel/installEmbedder in initialize() when model files already exist on disk. This registers the model as "active" with the flutter_gemma SDK so that getActiveModel/ getActiveEmbedder succeeds without requiring the user to re-download. Previously, the services only checked isModelInstalled but never called install(), causing "No active model set" errors on app re-open. Chat screen now auto-loads the model on entry when status is ready, removing the manual "Load Model" tap requirement on every launch. Session restore on initState removed — chat always starts fresh. Raw exception strings replaced with user-friendly messages across SplashScreen and SettingsScreen; raw errors go to debugPrint only. * fix: correct layout overflow and navigation edge cases Remove extendBody from HomeScreen — glassmorphic nav bar was obscuring scrollable content on screens that need full height. Set centerTitle: false in AppBarTheme so titles align left per the editorial design spec. Replace Row with Wrap in dashboard hero card stats so the three stat items wrap to a second line on narrow screens instead of overflowing. * fix: auto-load downloaded models in background at startup After services initialize, kick off loadModel() for both GemmaService and EmbeddingService if their status is ready (i.e. already downloaded). Calls are unawaited so navigation proceeds immediately while models warm up in the background, eliminating the manual Load tap requirement. * style(lint): add curly braces to if statements - Wrap single-line if bodies in curly braces per lint rules - Resolves curly_braces_in_flow_control_structures warnings in dashboard_screen.dart and app_colors.dart * fix(ui): resolve PR review issues from CodeRabbit - wrap shimmer skeleton in ListView to prevent overflow on landscape/small screens - add secondary name sort to make out-of-range list order deterministic - align unknown flag tooltip color with dot color (both now green) - replace GestureDetector with Material+InkWell in settings rows for ripple and a11y - use left BorderSide on chat bubble instead of separate accent strip widget
1 parent 65627f0 commit 6f533bc

26 files changed

Lines changed: 2481 additions & 1406 deletions

lib/main.dart

Lines changed: 178 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:ui';
2+
13
import 'package:flutter/material.dart';
24
import 'package:flutter_gemma/flutter_gemma.dart';
35

@@ -12,6 +14,8 @@ import 'services/biomarker_dictionary.dart';
1214
import 'services/embedding_service.dart';
1315
import 'services/gemma_service.dart';
1416
import 'services/vector_store_service.dart';
17+
import 'theme/app_colors.dart';
18+
import 'theme/koshika_design_system.dart';
1519

1620
/// Global references — initialized in SplashScreen before navigation.
1721
late ObjectBoxStore objectbox;
@@ -34,9 +38,8 @@ class KoshikaApp extends StatelessWidget {
3438
return MaterialApp(
3539
title: 'Koshika',
3640
debugShowCheckedModeBanner: false,
37-
theme: _buildTheme(Brightness.light),
38-
darkTheme: _buildTheme(Brightness.dark),
39-
themeMode: ThemeMode.system,
41+
theme: _buildTheme(),
42+
themeMode: ThemeMode.light,
4043
home: const SplashScreen(),
4144
routes: {
4245
'/home': (_) => const HomeScreen(),
@@ -45,43 +48,66 @@ class KoshikaApp extends StatelessWidget {
4548
);
4649
}
4750

48-
ThemeData _buildTheme(Brightness brightness) {
49-
final isDark = brightness == Brightness.dark;
50-
51-
// Health-themed teal/blue palette
52-
const seed = Color(0xFF0D9488); // Teal-600
53-
54-
final colorScheme = ColorScheme.fromSeed(
55-
seedColor: seed,
56-
brightness: brightness,
51+
ThemeData _buildTheme() {
52+
const colorScheme = ColorScheme(
53+
brightness: Brightness.light,
54+
primary: AppColors.primary,
55+
onPrimary: Colors.white,
56+
primaryContainer: AppColors.primaryContainer,
57+
onPrimaryContainer: AppColors.onPrimaryContainer,
58+
secondary: AppColors.secondary,
59+
onSecondary: Colors.white,
60+
secondaryContainer: AppColors.info,
61+
onSecondaryContainer: Colors.white,
62+
tertiary: AppColors.tertiary,
63+
onTertiary: Colors.white,
64+
tertiaryContainer: AppColors.tertiaryContainer,
65+
onTertiaryContainer: AppColors.onTertiaryContainer,
66+
error: AppColors.error,
67+
onError: Colors.white,
68+
errorContainer: AppColors.errorContainer,
69+
onErrorContainer: AppColors.onErrorContainer,
70+
surface: AppColors.surface,
71+
onSurface: AppColors.onSurface,
72+
onSurfaceVariant: AppColors.onSurfaceVariant,
73+
outline: AppColors.outlineVariant,
74+
outlineVariant: AppColors.outlineVariant,
75+
surfaceContainerLowest: AppColors.surfaceContainerLowest,
76+
surfaceContainerLow: AppColors.surfaceContainerLow,
77+
surfaceContainerHigh: AppColors.surfaceContainerHigh,
78+
surfaceContainerHighest: AppColors.surfaceContainerHighest,
5779
);
5880

5981
return ThemeData(
6082
useMaterial3: true,
6183
colorScheme: colorScheme,
62-
brightness: brightness,
63-
fontFamily: 'Roboto',
64-
appBarTheme: AppBarTheme(
65-
centerTitle: true,
84+
brightness: Brightness.light,
85+
textTheme: KoshikaTypography.textTheme,
86+
appBarTheme: const AppBarTheme(
87+
centerTitle: false,
6688
elevation: 0,
67-
backgroundColor: isDark ? colorScheme.surface : colorScheme.primary,
68-
foregroundColor: isDark ? colorScheme.onSurface : colorScheme.onPrimary,
89+
backgroundColor: AppColors.primary,
90+
foregroundColor: Colors.white,
6991
),
7092
cardTheme: CardThemeData(
71-
elevation: isDark ? 1 : 2,
72-
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
93+
elevation: 0,
94+
color: AppColors.surfaceContainerLowest,
95+
shape: RoundedRectangleBorder(borderRadius: KoshikaRadius.xxl),
7396
),
7497
navigationBarTheme: NavigationBarThemeData(
7598
elevation: 0,
76-
indicatorColor: colorScheme.primaryContainer,
99+
indicatorColor: AppColors.primaryContainer,
77100
labelTextStyle: WidgetStatePropertyAll(
78-
TextStyle(
79-
fontSize: 12,
80-
fontWeight: FontWeight.w500,
81-
color: colorScheme.onSurface,
101+
KoshikaTypography.textTheme.labelSmall!.copyWith(
102+
color: AppColors.onSurface,
82103
),
83104
),
84105
),
106+
filledButtonTheme: FilledButtonThemeData(style: KoshikaButtonStyles.pill),
107+
outlinedButtonTheme: OutlinedButtonThemeData(
108+
style: KoshikaButtonStyles.outlinedPill,
109+
),
110+
scaffoldBackgroundColor: AppColors.surface,
85111
);
86112
}
87113
}
@@ -116,33 +142,134 @@ class _HomeScreenState extends State<HomeScreen> {
116142
Widget build(BuildContext context) {
117143
return Scaffold(
118144
body: _buildCurrentScreen(),
119-
bottomNavigationBar: NavigationBar(
120-
selectedIndex: _currentIndex,
121-
onDestinationSelected: (index) {
122-
setState(() => _currentIndex = index);
123-
},
124-
destinations: const [
125-
NavigationDestination(
126-
icon: Icon(Icons.dashboard_outlined),
127-
selectedIcon: Icon(Icons.dashboard),
128-
label: 'Dashboard',
145+
bottomNavigationBar: ClipRect(
146+
child: BackdropFilter(
147+
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
148+
child: Container(
149+
decoration: BoxDecoration(
150+
color: Colors.white.withValues(alpha: 0.85),
151+
),
152+
child: SafeArea(
153+
top: false,
154+
child: Padding(
155+
padding: const EdgeInsets.symmetric(
156+
horizontal: KoshikaSpacing.sm,
157+
vertical: KoshikaSpacing.xs,
158+
),
159+
child: Row(
160+
mainAxisAlignment: MainAxisAlignment.spaceAround,
161+
children: [
162+
_NavItem(
163+
index: 0,
164+
currentIndex: _currentIndex,
165+
icon: Icons.dashboard_outlined,
166+
activeIcon: Icons.dashboard,
167+
label: 'Dashboard',
168+
onTap: () => setState(() => _currentIndex = 0),
169+
),
170+
_NavItem(
171+
index: 1,
172+
currentIndex: _currentIndex,
173+
icon: Icons.description_outlined,
174+
activeIcon: Icons.description,
175+
label: 'Reports',
176+
onTap: () => setState(() => _currentIndex = 1),
177+
),
178+
_NavItem(
179+
index: 2,
180+
currentIndex: _currentIndex,
181+
icon: Icons.chat_outlined,
182+
activeIcon: Icons.chat,
183+
label: 'Chat',
184+
onTap: () => setState(() => _currentIndex = 2),
185+
),
186+
_NavItem(
187+
index: 3,
188+
currentIndex: _currentIndex,
189+
icon: Icons.settings_outlined,
190+
activeIcon: Icons.settings,
191+
label: 'Settings',
192+
onTap: () => setState(() => _currentIndex = 3),
193+
),
194+
],
195+
),
196+
),
197+
),
129198
),
130-
NavigationDestination(
131-
icon: Icon(Icons.description_outlined),
132-
selectedIcon: Icon(Icons.description),
133-
label: 'Reports',
134-
),
135-
NavigationDestination(
136-
icon: Icon(Icons.chat_outlined),
137-
selectedIcon: Icon(Icons.chat),
138-
label: 'Chat',
139-
),
140-
NavigationDestination(
141-
icon: Icon(Icons.settings_outlined),
142-
selectedIcon: Icon(Icons.settings),
143-
label: 'Settings',
144-
),
145-
],
199+
),
200+
),
201+
);
202+
}
203+
}
204+
205+
class _NavItem extends StatelessWidget {
206+
final int index;
207+
final int currentIndex;
208+
final IconData icon;
209+
final IconData activeIcon;
210+
final String label;
211+
final VoidCallback onTap;
212+
213+
const _NavItem({
214+
required this.index,
215+
required this.currentIndex,
216+
required this.icon,
217+
required this.activeIcon,
218+
required this.label,
219+
required this.onTap,
220+
});
221+
222+
@override
223+
Widget build(BuildContext context) {
224+
final isSelected = index == currentIndex;
225+
226+
return GestureDetector(
227+
onTap: onTap,
228+
behavior: HitTestBehavior.opaque,
229+
child: SizedBox(
230+
width: 72,
231+
child: Column(
232+
mainAxisSize: MainAxisSize.min,
233+
children: [
234+
AnimatedContainer(
235+
duration: const Duration(milliseconds: 300),
236+
curve: Curves.easeInOut,
237+
padding: const EdgeInsets.symmetric(
238+
horizontal: KoshikaSpacing.base,
239+
vertical: KoshikaSpacing.xs,
240+
),
241+
decoration: BoxDecoration(
242+
color: isSelected
243+
? AppColors.primaryContainer.withValues(alpha: 0.3)
244+
: Colors.transparent,
245+
borderRadius: KoshikaRadius.pill,
246+
),
247+
child: AnimatedSwitcher(
248+
duration: const Duration(milliseconds: 300),
249+
child: Icon(
250+
isSelected ? activeIcon : icon,
251+
key: ValueKey(isSelected),
252+
size: 24,
253+
color: isSelected
254+
? AppColors.primary
255+
: AppColors.onSurfaceVariant,
256+
),
257+
),
258+
),
259+
const SizedBox(height: 2),
260+
AnimatedDefaultTextStyle(
261+
duration: const Duration(milliseconds: 200),
262+
style: TextStyle(
263+
fontSize: 11,
264+
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
265+
color: isSelected
266+
? AppColors.primary
267+
: AppColors.onSurfaceVariant,
268+
),
269+
child: Text(label),
270+
),
271+
],
272+
),
146273
),
147274
);
148275
}

0 commit comments

Comments
 (0)