- created snackbar, step indicator, money text, text field and progress bar components.

- created wallet balance block, wallet item and kid line chart widgets.
- restructured onboarding, signup and device signup screens into layouts and main screens.
- updated signup and kid wallet screens to 17/11 design.
This commit is contained in:
2025-11-21 15:28:46 +01:00
parent 4225f7510b
commit 8201bff0a7
56 changed files with 1991 additions and 1491 deletions

View File

@@ -1,2 +1,7 @@
export 'src/theme/theme_port.dart';
export 'src/theme/theme_sf_adapter.dart';
export 'src/steps/step_indicator.dart';
export 'src/texts/money_text.dart';
export 'src/progress_bars/progress_bar.dart';
export 'src/inputs/textfields.dart';
export 'src/snackbars/snackbar.dart';

View File

@@ -0,0 +1,72 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CustomTextField extends ConsumerStatefulWidget{
bool? showPassword;
final bool numeric;
final String hint;
final String label;
final int? lines;
final ValueChanged<String>? onChanged;
final int? length;
CustomTextField({
super.key,
this.showPassword,
this.numeric = false,
this.hint = '',
this.label = '',
this.lines,
this.length,
this.onChanged,
});
@override
ConsumerState<CustomTextField> createState() => CustomTextFieldState();
}
class CustomTextFieldState extends ConsumerState<CustomTextField>{
@override
Widget build(BuildContext context) {
final theme = ref.watch(themePortProvider);
return TextFormField(
keyboardType: widget.numeric? TextInputType.number : TextInputType.text,
obscureText: !(widget.showPassword ?? true),
enableSuggestions: widget.showPassword ?? true,
autocorrect: !(widget.showPassword ?? false),
style: TextStyle(color: theme.getColorFor(ThemeCode.buttonSecondary)),
inputFormatters: widget.numeric? [
FilteringTextInputFormatter.digitsOnly
] : [],
decoration: InputDecoration(
counterText: "",
hintText: widget.hint,
labelText: widget.label,
floatingLabelBehavior: FloatingLabelBehavior.always,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: theme.getColorFor(ThemeCode.textPrimary)),
gapPadding: 16
),
suffixIcon: widget.showPassword!=null ? IconButton(
icon: Icon(widget.showPassword!
? Icons.visibility_off
: Icons.visibility),
onPressed: () {
setState(() {
widget.showPassword = !widget.showPassword!;
});
},
) : null,
),
minLines: widget.lines ?? 1,
maxLines: widget.lines ?? 1,
maxLength: widget.length,
onChanged: widget.onChanged ?? (_)=>{},
);
}
}

View File

@@ -0,0 +1,56 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ProgressBar extends ConsumerWidget{
final double max;
final double value;
final double height;
final double textSize;
final double textSecondarySize;
final Color backgroundColor;
final Color foregroundColor;
final Color textColor;
const ProgressBar(
this.max,
this.value,
this.height,
this.textSize,
this.textSecondarySize,
this.backgroundColor,
this.foregroundColor,
this.textColor,
);
@override
Widget build(BuildContext context, WidgetRef ref) {
return
Stack(
children: [
LinearProgressIndicator(
value: value / max,
minHeight: height,
borderRadius: BorderRadius.all(Radius.circular(24)),
color: foregroundColor,
backgroundColor: backgroundColor
),
FractionallySizedBox(
widthFactor: value / max,
child: SizedBox(
height: height,
child: Center(
child: MoneyText(
text: "$value",
size: textSize,
secondarySize: textSecondarySize,
color: textColor,
),
)
),
),
]
);
}
}

View File

@@ -0,0 +1,65 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
enum MessageType {
info,
error,
warning,
success
}
class CustomSnackBar extends ConsumerWidget{
final MessageType type;
final String message;
CustomSnackBar({
super.key,
this.type = MessageType.info,
required this.message,
});
@override
SnackBar build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
late final Color foregroundColor;
late final Color backgroundColor;
late final IconData icon;
switch (type){
case MessageType.info:
backgroundColor = Color(0xFFE3EFFD);
foregroundColor = Color(0xFF1F4ECF);
icon = Icons.info;
case MessageType.error:
backgroundColor = Color(0xFFFBEDE9);
foregroundColor = Color(0xFFD12D00);
icon = Icons.cancel;
case MessageType.warning:
backgroundColor = Color(0xFFFBF3E2);
foregroundColor = Color(0xFFE34B04);
icon = Icons.warning_outlined;
case MessageType.success:
backgroundColor = Color(0xFFE2F4E8);
foregroundColor = Color(0xFF00713D);
icon = Icons.check_circle;
}
return SnackBar(
behavior: SnackBarBehavior.floating,
backgroundColor: backgroundColor,
shape: RoundedRectangleBorder(
side: BorderSide(color: foregroundColor, width: 1),
borderRadius: BorderRadius.all(Radius.circular(10))
),
content: Row(
spacing: 8,
children: [
Icon(icon, color: foregroundColor),
Expanded(child: Text(message, style: TextStyle(color: theme.getColorFor(ThemeCode.textPrimary), fontSize: 14)))
],
),
);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
class StepIndicator extends StatelessWidget{
final int max;
final int current;
final Color color;
const StepIndicator({super.key, required this.max, required this.current, required this.color});
@override
Widget build(BuildContext context) {
return Row(
spacing: 12,
children: [
Spacer(),
...List<Widget>.generate(max, (int index){
return DecoratedBox(
decoration: ShapeDecoration(
shape: CircleBorder(side: BorderSide(color: color)),
color: (index < current)? color: Colors.transparent
),
child: SizedBox(width: 16, height: 16),
);
}),
Spacer()
]
);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
class MoneyText extends StatelessWidget {
final String text;
final double size;
final double? secondarySize;
final Color color;
final double height;
const MoneyText({
super.key,
required this.text,
required this.size,
this.secondarySize,
required this.color,
this.height = 1,
});
@override
Widget build(BuildContext context) {
final units = text.split(".")[0];
final cents = ",${text.split(".")[1]}";
return Text.rich(TextSpan(
text: units,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: size,
color: color,
height: height
),
children: [
TextSpan(
text: cents,
style: TextStyle(
fontWeight: FontWeight.normal,
fontSize: secondarySize ?? size,
)
)
]
));
}
}

View File

@@ -25,6 +25,7 @@ enum ThemeCode {
abstract class ThemePort {
late Map<ThemeCode, Color> theme;
late List<List<Color>> cardColors;
late List<Color> disabledCardColors;
Color getColorFor(ThemeCode code) {
Color? c = theme[code];
@@ -37,4 +38,8 @@ abstract class ThemePort {
List<Color> getCardColorFor(int index) {
return cardColors[index % cardColors.length];
}
List<Color> getDisabledCardColors() {
return disabledCardColors;
}
}

View File

@@ -25,4 +25,11 @@ class ThemeSfAdapter extends ThemePort {
@override
List<List<Color>> get cardColors => _cardColors;
final List<Color> _disabledCardColors = [
Color(0xFF989797), Color(0xFF797676), Color(0xFF5F5A5A)
];
@override
List<Color> get disabledCardColors => _disabledCardColors;
}

View File

@@ -13,6 +13,8 @@ dependencies:
sdk: flutter
flutter_riverpod: ^3.0.3
get_it: ^9.0.5
fonts:
path: ../../packages/fonts
dev_dependencies:
flutter_test:

View File

@@ -0,0 +1,20 @@
name: fonts
# resolution: workspace
description: "A new Flutter package project."
version: 0.0.1
environment:
sdk: ^3.9.2
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
fonts:
- family: Stolzl
fonts:
- asset: lib/fonts/stolzl_regular.otf
- asset: lib/fonts/stolzl_bold.otf
weight: 500

View File

@@ -4,3 +4,4 @@ export 'src/widgets/deposit_block.dart';
export 'src/screens/connection_error_screen.dart';
export 'src/screens/server_error_screen.dart';
export 'src/screens/no_plan_error_screen.dart';
export 'src/widgets/wallet_balance_block.dart';

View File

@@ -1,10 +1,11 @@
class Kid {
final String name;
final double balance;
final double savings;
const Kid({
required this.name,
required this.balance,
required this.savings,
});
}

View File

@@ -1,6 +1,5 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class DepositBlock extends ConsumerWidget {
@@ -20,40 +19,56 @@ class DepositBlock extends ConsumerWidget {
),
margin: EdgeInsets.only(top: 10),
child: Column(
spacing: 24,
children: [
Text(
"Ingresar dinero en el wallet",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
Align(
alignment: Alignment.centerLeft,
child: Text(
"Ingresar dinero en el wallet",
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
),
),
Row(
spacing: 10,
Column(
spacing: 16,
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
labelText: "Cantidad",
hintText: "0€",
border: OutlineInputBorder(),
Row(
spacing: 10,
children: [
Expanded(
child: CustomTextField(
label: "Cantidad",
hint: "0€",
numeric: true,
),
),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
),
FilledButton(
onPressed: () => {},
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll<Color>(
theme.getColorFor(ThemeCode.buttonPrimary),
),
shape: WidgetStatePropertyAll(RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(18))
))
),
child: SizedBox(
height: 60,
child: Center(child: Text("Ingresar", style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500)))),
),
],
),
FilledButton(
onPressed: () => {},
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll<Color>(
theme.getColorFor(ThemeCode.buttonPrimary),
),
),
child: Text("Ingresar"),
Align(
alignment: Alignment.topLeft,
child: Row(
spacing: 4,
children: [
Icon(Icons.info_outline, size: 16),
Text("Máximo que puedes añadir: $max"),
],
)
),
],
),
Align(
alignment: Alignment.topLeft,
child: Text("Máximo que puedes añadir: $max"),
),
)
],
),
);

View File

@@ -4,10 +4,7 @@ import 'package:fl_chart/fl_chart.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class LineGraph extends ConsumerStatefulWidget {
final lines = [
[0, 1, 0, 1, 0, 1, 0],
[1, 0, 1, 0, 1, 0, 1],
];
final lines = [[0,1,0,1,0,1,0],[1,0,1,0,1,0,1]];
LineGraph({super.key});
@@ -16,7 +13,8 @@ class LineGraph extends ConsumerStatefulWidget {
}
class LineGraphState extends ConsumerState<LineGraph> {
final weekDays = ["L", "M", "X", "J", "V", "S", "D"];
final weekDays = ["L", "M", "X", "J", "V", "S", "D"];
String? timeSpan;
late var days = weekDays;
@@ -31,148 +29,146 @@ class LineGraphState extends ConsumerState<LineGraph> {
final theme = ref.watch(themePortProvider);
return Container(
padding: EdgeInsets.all(15),
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
border: BoxBorder.fromLTRB(
left: BorderSide(color: Colors.cyan, width: 5),
),
borderRadius: BorderRadius.all(Radius.circular(20)),
color: theme.getColorFor(ThemeCode.backgroundPrimary),
border: BoxBorder.fromLTRB(left: BorderSide(color: Colors.cyan, width: 5)),
borderRadius: BorderRadius.all(Radius.circular(13)),
color: theme.getColorFor(ThemeCode.backgroundPrimary)
),
child: Column(
spacing: 10,
spacing: 32,
children: [
Row(
children: [
Text("Gastos", style: TextStyle(fontWeight: FontWeight.bold)),
Spacer(),
Container(
padding: EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10)),
color: theme.getColorFor(ThemeCode.backgroundSecondary),
),
child: DropdownButton(
underline: Container(),
value: timeSpan,
onChanged: (String? value) {
setState(() {
timeSpan = value;
});
Row(children: [
Text("Gastos", style: TextStyle(fontWeight: FontWeight.w500, fontSize: 18)),
Spacer(),
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 0),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(8)),
color: theme.getColorFor(ThemeCode.backgroundSecondary),
),
child: DropdownButton(
underline: Container(),
value: timeSpan,
onChanged: (String? value) {
setState(() {
timeSpan = value;
});
},
dropdownColor: theme.getColorFor(ThemeCode.backgroundPrimary),
items: [
DropdownMenuItem(value: "day", child: Text("Hoy", style: TextStyle(fontSize: 14, letterSpacing: 0))),
DropdownMenuItem(value: "week", child: Text("Esta semana", style: TextStyle(fontSize: 14, letterSpacing: 0))),
DropdownMenuItem(value: "month", child: Text("Este mes", style: TextStyle(fontSize: 14, letterSpacing: 0))),
]
),
)
]),
SizedBox(
height: 160,
child: LineChart(LineChartData(
gridData: FlGridData(
show: true,
drawHorizontalLine: false,
drawVerticalLine: true,
verticalInterval: 1,
getDrawingVerticalLine: (value)=>FlLine(strokeWidth: 43, color: theme.getColorFor(ThemeCode.backgroundSecondary))
),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
interval: 1,
showTitles: true,
reservedSize: 40,
getTitlesWidget: (double value, TitleMeta meta){
String text = weekDays[value.toInt()];
return SideTitleWidget(
space: 4,
meta: meta,
child: Expanded(child: Center(child: Text(text, style: TextStyle(fontSize: 12)))),
);
},
dropdownColor: theme.getColorFor(ThemeCode.backgroundPrimary),
items: [
DropdownMenuItem(value: "day", child: Text("Hoy")),
DropdownMenuItem(value: "week", child: Text("Esta semana")),
DropdownMenuItem(value: "month", child: Text("Este mes")),
],
),
),
],
),
Expanded(
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawHorizontalLine: false,
drawVerticalLine: true,
verticalInterval: 1,
leftTitles: AxisTitles(),
topTitles: AxisTitles(),
rightTitles: AxisTitles()
),
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
tooltipBorderRadius: BorderRadius.all(Radius.circular(7)),
getTooltipColor: (spot) => theme.getColorFor(ThemeCode.buttonSecondary),
getTooltipItems: (List<LineBarSpot> touchedSpots) {
return touchedSpots.map((LineBarSpot touchedSpot) {
return LineTooltipItem(
"${touchedSpot.y}",
TextStyle(fontWeight: FontWeight.w500, fontSize: 14, color: theme.getColorFor(ThemeCode.textSecondary)),
);
}).toList();
},
),
getTouchedSpotIndicator: (
_,
indicators,
) {
return indicators
.map((int index) => const TouchedSpotIndicatorData(
FlLine(color: Colors.transparent),
FlDotData(show: false),
))
.toList();
},
touchSpotThreshold: 25,
distanceCalculator:
(Offset touchPoint, Offset spotPixelCoordinates) =>
(touchPoint - spotPixelCoordinates).distance,
),
borderData: FlBorderData(
show: true,
border: Border(
bottom: BorderSide(
color: Colors.lightBlue.withValues(alpha: 0.2),
width: 4
),
titlesData: FlTitlesData(
//show: false,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: (double value, TitleMeta meta) =>
SideTitleWidget(
space: 4,
meta: meta,
/*fitInside: fitInsideBottomTitle
? SideTitleFitInsideData.fromTitleMeta(meta, distanceFromEdge: 0)
: SideTitleFitInsideData.disable(),*/
child: Text(weekDays[value.toInt()]),
),
),
),
leftTitles: AxisTitles(),
topTitles: AxisTitles(),
rightTitles: AxisTitles(),
),
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipColor: (touchedSpot) =>
theme.getColorFor(ThemeCode.buttonSecondary),
getTooltipItems: (List<LineBarSpot> touchedBarSpots) {
return touchedBarSpots.map((barSpot) {
return LineTooltipItem(
"${barSpot.y}",
TextStyle(
color: theme.getColorFor(ThemeCode.textSecondary),
),
);
}).toList();
},
),
),
borderData: FlBorderData(
show: true,
border: Border(
bottom: BorderSide(
color: Colors.lightBlue.withValues(alpha: 0.2),
width: 4,
),
left: const BorderSide(color: Colors.transparent),
right: const BorderSide(color: Colors.transparent),
top: const BorderSide(color: Colors.transparent),
),
),
lineBarsData: [
LineChartBarData(
isCurved: true,
color: Colors.pink,
barWidth: 5,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false),
spots: const [
FlSpot(0, 1),
FlSpot(1, 0),
FlSpot(2, 1),
FlSpot(3, 0),
FlSpot(4, 1),
FlSpot(5, 0),
FlSpot(6, 1),
],
),
LineChartBarData(
isCurved: true,
color: Colors.cyan,
barWidth: 5,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false),
spots: const [
FlSpot(0, 0),
FlSpot(1, 1),
FlSpot(2, 0),
FlSpot(3, 1),
FlSpot(4, 0),
FlSpot(5, 1),
FlSpot(6, 0),
],
),
],
minX: 0,
maxX: days.length - 1,
maxY: 1,
minY: 0,
left: const BorderSide(color: Colors.transparent),
right: const BorderSide(color: Colors.transparent),
top: const BorderSide(color: Colors.transparent),
),
),
),
lineBarsData: [
LineChartBarData(
isCurved: true,
color: Colors.pink,
barWidth: 5,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false),
spots: List<FlSpot>.generate(days.length, (int index){
return FlSpot(index.toDouble(), (index+1)%2);
})
),
LineChartBarData(
isCurved: true,
color: Colors.cyan,
barWidth: 5,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false),
spots: List<FlSpot>.generate(days.length, (int index){
return FlSpot(index.toDouble(), index%2);
})
),
],
minX: 0,
maxX: days.length-1,
maxY: 1,
minY: 0,
))
)
],
),
);
}
}
}

View File

@@ -0,0 +1,59 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class WalletBalanceBlock extends ConsumerWidget {
final double max;
final double value;
final double savings;
final double savingsPlan;
const WalletBalanceBlock({
super.key,
required this.max,
required this.value,
required this.savings,
this.savingsPlan = 30.0
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
return Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.all(Radius.circular(20)),
),
child: Column(
spacing: 16,
children: [
Row(
children: [
Text(
"Wallet",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
Spacer(),
MoneyText(
text: "$max€ total",
size: 26,
secondarySize: 16,
color: theme.getColorFor(ThemeCode.textPrimary),
),
],
),
Row(children: [
Text("Objetivos de ahorro"),
Spacer(),
Text("$savingsPlan")
]),
ProgressBar(savingsPlan, savings, 24, 16, 12, theme.getColorFor(ThemeCode.backgroundSecondary), theme.getColorFor(ThemeCode.backgroundTertiary), theme.getColorFor(ThemeCode.textPrimary)),
ProgressBar(max, value, 83, 40, 24, theme.getColorFor(ThemeCode.backgroundTertiary), theme.getColorFor(ThemeCode.buttonPrimary), theme.getColorFor(ThemeCode.textSecondary)),
Center(child: Text("Disponible")),
],
),
);
}
}