Small Flutter UI tweaks

This commit is contained in:
Juan José Mata
2026-02-16 04:23:00 +01:00
parent eb0d05a7fb
commit 2dcb4b4f67
8 changed files with 432 additions and 39 deletions

View File

@@ -65,6 +65,12 @@ class SureApp extends StatelessWidget {
title: 'Sure Finance',
debugShowCheckedModeBanner: false,
theme: ThemeData(
fontFamily: 'Geist',
fontFamilyFallback: const [
'Inter',
'Arial',
'sans-serif',
],
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6366F1),
brightness: Brightness.light,
@@ -96,6 +102,12 @@ class SureApp extends StatelessWidget {
),
),
darkTheme: ThemeData(
fontFamily: 'Geist',
fontFamilyFallback: const [
'Inter',
'Arial',
'sans-serif',
],
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6366F1),
brightness: Brightness.dark,

View File

@@ -110,12 +110,19 @@ class _ChatListScreenState extends State<ChatListScreen> {
return Scaffold(
appBar: AppBar(
title: const Text('AI Assistant'),
title: const Text('Chats'),
centerTitle: false,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _handleRefresh,
tooltip: 'Refresh',
Padding(
padding: const EdgeInsets.only(top: 12, right: 12),
child: InkWell(
onTap: _handleRefresh,
child: const SizedBox(
width: 36,
height: 36,
child: Icon(Icons.refresh),
),
),
),
],
),

View File

@@ -373,7 +373,6 @@ class DashboardScreenState extends State<DashboardScreen> {
return Scaffold(
appBar: AppBar(
title: const Text('Dashboard'),
actions: [
if (_showSyncSuccess)
Padding(

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'intro_screen_stub.dart' if (dart.library.html) 'intro_screen_web.dart';
class IntroScreen extends StatelessWidget {
const IntroScreen({super.key, this.onStartChat});
final VoidCallback? onStartChat;
@override
Widget build(BuildContext context) {
return IntroScreenPlatform(onStartChat: onStartChat);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class IntroScreenPlatform extends StatelessWidget {
const IntroScreenPlatform({super.key, this.onStartChat});
final VoidCallback? onStartChat;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: const Card(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
children: <Widget>[
Text(
'Intro experience coming soon',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
SizedBox(height: 12),
Text(
"We're building a richer onboarding journey to learn about your goals, milestones, and day-to-day needs. "
'For now, head over to the chat sidebar to start a conversation with Sure and let us know where you are in your financial journey.',
textAlign: TextAlign.center,
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,242 @@
import 'dart:html' as html;
import 'dart:ui_web' as ui;
import 'package:flutter/material.dart';
class IntroScreenPlatform extends StatefulWidget {
const IntroScreenPlatform({super.key, this.onStartChat});
final VoidCallback? onStartChat;
@override
State<IntroScreenPlatform> createState() => _IntroScreenPlatformState();
}
class _IntroScreenPlatformState extends State<IntroScreenPlatform> {
static int _nextViewId = 0;
late final String _viewType;
@override
void initState() {
super.initState();
final currentId = _nextViewId;
_nextViewId += 1;
_viewType = 'intro-screen-web-$currentId';
ui.platformViewRegistry.registerViewFactory(_viewType, (int viewId) {
final frame = html.IFrameElement()
..srcdoc = _introHtmlContent
..style.width = '100%'
..style.height = '100%'
..style.border = '0';
return frame;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SizedBox.expand(
child: HtmlElementView(viewType: _viewType),
),
);
}
}
const String _introHtmlContent = '''
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
:root {
color-scheme: light dark;
}
body {
margin: 0;
min-height: 100vh;
font-family: Geist, Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: #111827;
background: transparent;
}
.grow {
min-height: 100vh;
}
.overflow-y-auto {
overflow-y: auto;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.lg\:px-10 {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
.pt-0 {
padding-top: 0;
}
.pb-4 {
padding-bottom: 1rem;
}
.w-full {
width: 100%;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.max-w-5xl {
max-width: 64rem;
}
.max-w-3xl {
max-width: 48rem;
}
.space-y-2 > * + * {
margin-top: 0.5rem;
}
.text-2xl {
font-size: 1.5rem;
}
.text-xl {
font-size: 1.25rem;
}
.text-center {
text-align: center;
}
.font-semibold {
font-weight: 600;
}
.text-secondary {
color: #4b5563;
}
.text-primary {
color: #111827;
}
.space-y-4 > * + * {
margin-top: 1rem;
}
.bg-container {
background: #ffffff;
box-shadow: 0 1px 2px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.1);
}
.intro-card-shell {
width: min(95%, 48rem);
box-sizing: border-box;
padding-left: 1rem;
padding-right: 1rem;
}
.shadow-border-xs {
border: 1px solid #e5e7eb;
}
.rounded-2xl {
border-radius: 1rem;
}
.p-8 {
padding: 2rem;
}
.flex {
display: flex;
}
.justify-center {
justify-content: center;
}
.inline-flex {
display: inline-flex;
}
.items-center {
align-items: center;
}
.gap-2 {
gap: 0.5rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.bg-primary {
background: #2563eb;
}
.text-white {
color: #fff;
}
.font-medium {
font-weight: 500;
}
a {
color: #fff;
text-decoration: none;
}
.w-16 {
width: 4rem;
}
.h-16 {
height: 4rem;
}
.container {
padding-top: 0;
}
</style>
</head>
<body>
<main class="grow overflow-y-auto px-3 lg:px-10 pt-0 pb-4 w-full mx-auto max-w-5xl" data-app-layout-target="content">
<div class="mx-auto max-w-3xl intro-card-shell">
<div class="bg-container shadow-border-xs rounded-2xl p-8 text-center space-y-4">
<h2 class="text-xl font-semibold text-primary">Intro experience coming soon</h2>
<p class="text-secondary">
We're building a richer onboarding journey to learn about your goals, milestones, and day-to-day needs. For now, head over to the chat sidebar to start a conversation with Sure and let us know where you are in your financial journey.
</p>
</div>
</div>
</main>
</body>
</html>
''';

View File

@@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import 'dashboard_screen.dart';
import 'chat_list_screen.dart';
import 'dashboard_screen.dart';
import 'intro_screen.dart';
import 'more_screen.dart';
import 'settings_screen.dart';
@@ -17,13 +20,17 @@ class _MainNavigationScreenState extends State<MainNavigationScreen> {
int _currentIndex = 0;
final _dashboardKey = GlobalKey<DashboardScreenState>();
List<Widget> _buildScreens(bool introLayout) {
List<Widget> _buildScreens(bool introLayout, VoidCallback? onStartChat) {
final screens = <Widget>[];
if (!introLayout) {
screens.add(DashboardScreen(key: _dashboardKey));
}
if (introLayout) {
screens.add(IntroScreen(onStartChat: onStartChat));
}
screens.add(const ChatListScreen());
if (!introLayout) {
@@ -35,6 +42,36 @@ class _MainNavigationScreenState extends State<MainNavigationScreen> {
return screens;
}
Future<void> _handleDestinationSelected(
int index,
AuthProvider authProvider,
bool introLayout,
) async {
const chatIndex = 1;
if (index == chatIndex && !authProvider.aiEnabled) {
final enabled = await _showEnableAiPrompt();
if (!enabled) {
return;
}
}
if (mounted) {
setState(() {
_currentIndex = index;
});
if (!introLayout && index == 0) {
_dashboardKey.currentState?.reloadPreferences();
}
}
}
Future<void> _handleSelectSettings(AuthProvider authProvider, bool introLayout) async {
final settingsIndex = introLayout ? 2 : 3;
await _handleDestinationSelected(settingsIndex, authProvider, introLayout);
}
List<NavigationDestination> _buildDestinations(bool introLayout) {
final destinations = <NavigationDestination>[];
@@ -48,6 +85,16 @@ class _MainNavigationScreenState extends State<MainNavigationScreen> {
);
}
if (introLayout) {
destinations.add(
const NavigationDestination(
icon: Icon(Icons.auto_awesome_outlined),
selectedIcon: Icon(Icons.auto_awesome),
label: 'Intro',
),
);
}
destinations.add(
const NavigationDestination(
icon: Icon(Icons.chat_bubble_outline),
@@ -66,17 +113,48 @@ class _MainNavigationScreenState extends State<MainNavigationScreen> {
);
}
destinations.add(
const NavigationDestination(
icon: Icon(Icons.settings_outlined),
selectedIcon: Icon(Icons.settings),
label: 'Settings',
),
);
return destinations;
}
PreferredSizeWidget _buildTopBar(AuthProvider authProvider, bool introLayout) {
return AppBar(
automaticallyImplyLeading: false,
toolbarHeight: 60,
elevation: 0,
titleSpacing: 0,
centerTitle: false,
actionsPadding: EdgeInsets.zero,
title: Container(
width: 60,
height: 60,
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.only(top: 12, left: 12),
child: SvgPicture.asset(
'assets/images/logomark.svg',
width: 36,
height: 36,
),
),
),
actions: [
Padding(
padding: const EdgeInsets.only(top: 12, right: 12),
child: InkWell(
onTap: () {
_handleSelectSettings(authProvider, introLayout);
},
child: const SizedBox(
width: 36,
height: 36,
child: Icon(Icons.settings_outlined),
),
),
),
],
);
}
Future<bool> _showEnableAiPrompt() async {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
@@ -116,43 +194,49 @@ class _MainNavigationScreenState extends State<MainNavigationScreen> {
return enabled;
}
int _resolveBottomSelectedIndex(List<NavigationDestination> destinations) {
if (destinations.isEmpty) {
return 0;
}
if (_currentIndex < 0) {
return 0;
}
if (_currentIndex >= destinations.length) {
return destinations.length - 1;
}
return _currentIndex;
}
@override
Widget build(BuildContext context) {
return Consumer<AuthProvider>(
builder: (context, authProvider, _) {
final introLayout = authProvider.isIntroLayout;
final screens = _buildScreens(introLayout);
const chatIndex = 1;
final screens = _buildScreens(
introLayout,
() => _handleDestinationSelected(chatIndex, authProvider, introLayout),
);
final destinations = _buildDestinations(introLayout);
final bottomNavIndex = _resolveBottomSelectedIndex(destinations);
if (_currentIndex >= screens.length) {
_currentIndex = 0;
}
final chatIndex = introLayout ? 0 : 1;
final homeIndex = 0;
return Scaffold(
appBar: _buildTopBar(authProvider, introLayout),
body: IndexedStack(
index: _currentIndex,
children: screens,
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) async {
if (index == chatIndex && !authProvider.aiEnabled) {
final enabled = await _showEnableAiPrompt();
if (!enabled) {
return;
}
}
setState(() {
_currentIndex = index;
});
if (!introLayout && index == homeIndex) {
_dashboardKey.currentState?.reloadPreferences();
}
selectedIndex: bottomNavIndex,
onDestinationSelected: (index) {
_handleDestinationSelected(index, authProvider, introLayout);
},
destinations: destinations,
),

View File

@@ -10,9 +10,6 @@ class MoreScreen extends StatelessWidget {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('More'),
),
body: ListView(
children: [
_buildMenuItem(