@marcos_feitoza/personal-finance-frontend-core-ui 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +28 -0
- package/lib/personal_finance_frontend_core_ui.dart +2 -0
- package/lib/utils/app_dialogs.dart +4 -4
- package/lib/utils/app_responsive.dart +87 -0
- package/lib/widgets/app_async_state_view.dart +147 -0
- package/lib/widgets/historical_data_table.dart +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [1.2.2](https://github.com/MarcosOps/personal-finance-frontend-core-ui/compare/v1.2.1...v1.2.2) (2026-01-30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Padronizar mensagens de erro e estados de loading ([66ea1d0](https://github.com/MarcosOps/personal-finance-frontend-core-ui/commit/66ea1d074dffa12bcd068265a6920e5997c20f2b))
|
|
7
|
+
|
|
8
|
+
## [1.2.1](https://github.com/MarcosOps/personal-finance-frontend-core-ui/compare/v1.2.0...v1.2.1) (2026-01-30)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* Web responsivo (mobile/tablet/resize) ([dba28a4](https://github.com/MarcosOps/personal-finance-frontend-core-ui/commit/dba28a4c370aac6f02a0e304e25c6eebaca4b961))
|
|
14
|
+
|
|
1
15
|
# [1.2.0](https://github.com/MarcosOps/personal-finance-frontend-core-ui/compare/v1.1.0...v1.2.0) (2026-01-29)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -75,6 +75,34 @@ showErrorSnackBar(context, 'Something went wrong');
|
|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
|
78
|
+
## Loading / Empty / Error / Success (Fase 3)
|
|
79
|
+
|
|
80
|
+
Para evitar telas “silenciosas” e condicionais repetidos em cada screen (`if (isLoading) ... else if (error) ...`), use o widget centralizado:
|
|
81
|
+
|
|
82
|
+
- `lib/widgets/app_async_state_view.dart`
|
|
83
|
+
|
|
84
|
+
Ele padroniza os estados:
|
|
85
|
+
|
|
86
|
+
- **loading**: `CircularProgressIndicator`
|
|
87
|
+
- **error**: mensagem + botão “Try again” (opcional)
|
|
88
|
+
- **empty**: mensagem + ação (opcional)
|
|
89
|
+
- **success**: renderiza o `child`
|
|
90
|
+
|
|
91
|
+
Exemplo:
|
|
92
|
+
|
|
93
|
+
```dart
|
|
94
|
+
return AppAsyncStateView(
|
|
95
|
+
isLoading: viewModel.isLoading,
|
|
96
|
+
errorMessage: viewModel.errorMessage,
|
|
97
|
+
isEmpty: viewModel.items.isEmpty,
|
|
98
|
+
emptyMessage: 'No items found.',
|
|
99
|
+
onRetry: viewModel.fetch,
|
|
100
|
+
child: ListView(...),
|
|
101
|
+
);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
78
106
|
## Como Usar (Instalação como Dependência)
|
|
79
107
|
|
|
80
108
|
Este pacote é uma dependência local para a aplicação principal (`personal-finance-frontend`) e outras features packages do frontend.
|
|
@@ -10,17 +10,17 @@ class AppDialogs {
|
|
|
10
10
|
context: context,
|
|
11
11
|
builder: (BuildContext context) {
|
|
12
12
|
return AlertDialog(
|
|
13
|
-
title: const Text('
|
|
13
|
+
title: const Text('Confirm Deletion'),
|
|
14
14
|
content: Text(
|
|
15
|
-
'
|
|
15
|
+
'Are you sure you want to delete "$itemName"? This action cannot be undone.'),
|
|
16
16
|
actions: <Widget>[
|
|
17
17
|
TextButton(
|
|
18
18
|
onPressed: () => Navigator.of(context).pop(false),
|
|
19
|
-
child: const Text('
|
|
19
|
+
child: const Text('Cancel'),
|
|
20
20
|
),
|
|
21
21
|
TextButton(
|
|
22
22
|
onPressed: () => Navigator.of(context).pop(true),
|
|
23
|
-
child: const Text('
|
|
23
|
+
child: const Text('Delete'),
|
|
24
24
|
),
|
|
25
25
|
],
|
|
26
26
|
);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import 'package:flutter/widgets.dart';
|
|
2
|
+
|
|
3
|
+
/// Simple responsive helpers and breakpoints.
|
|
4
|
+
///
|
|
5
|
+
/// Why this exists:
|
|
6
|
+
/// - Most of the app was built desktop-first (many wide `Row`s and `DataTable`s).
|
|
7
|
+
/// - On mobile/tablet or when resizing the browser, layouts overflow.
|
|
8
|
+
/// - These helpers give us one place to define breakpoints and common patterns.
|
|
9
|
+
///
|
|
10
|
+
/// Breakpoints (can be adjusted later):
|
|
11
|
+
/// - mobile: < 600
|
|
12
|
+
/// - tablet: 600 .. < 1024
|
|
13
|
+
/// - desktop: >= 1024
|
|
14
|
+
class AppBreakpoints {
|
|
15
|
+
AppBreakpoints._();
|
|
16
|
+
|
|
17
|
+
static const double mobile = 600;
|
|
18
|
+
static const double tablet = 1024;
|
|
19
|
+
|
|
20
|
+
static bool isMobileWidth(double width) => width < mobile;
|
|
21
|
+
static bool isTabletWidth(double width) => width >= mobile && width < tablet;
|
|
22
|
+
static bool isDesktopWidth(double width) => width >= tablet;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Builds different widgets depending on the current available width.
|
|
26
|
+
class ResponsiveBuilder extends StatelessWidget {
|
|
27
|
+
final Widget Function(BuildContext context) mobile;
|
|
28
|
+
final Widget Function(BuildContext context)? tablet;
|
|
29
|
+
final Widget Function(BuildContext context) desktop;
|
|
30
|
+
|
|
31
|
+
const ResponsiveBuilder({
|
|
32
|
+
super.key,
|
|
33
|
+
required this.mobile,
|
|
34
|
+
this.tablet,
|
|
35
|
+
required this.desktop,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
@override
|
|
39
|
+
Widget build(BuildContext context) {
|
|
40
|
+
return LayoutBuilder(
|
|
41
|
+
builder: (context, constraints) {
|
|
42
|
+
final width = constraints.maxWidth;
|
|
43
|
+
|
|
44
|
+
if (AppBreakpoints.isMobileWidth(width)) {
|
|
45
|
+
return mobile(context);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (AppBreakpoints.isTabletWidth(width)) {
|
|
49
|
+
return (tablet ?? desktop)(context);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return desktop(context);
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Wraps a wide widget (like DataTable) with horizontal scroll when needed.
|
|
59
|
+
///
|
|
60
|
+
/// On desktop the widget is rendered normally.
|
|
61
|
+
/// On mobile/tablet we enable horizontal scrolling to avoid overflow.
|
|
62
|
+
class ResponsiveHorizontalScroll extends StatelessWidget {
|
|
63
|
+
final Widget child;
|
|
64
|
+
|
|
65
|
+
const ResponsiveHorizontalScroll({super.key, required this.child});
|
|
66
|
+
|
|
67
|
+
@override
|
|
68
|
+
Widget build(BuildContext context) {
|
|
69
|
+
return LayoutBuilder(
|
|
70
|
+
builder: (context, constraints) {
|
|
71
|
+
final width = constraints.maxWidth;
|
|
72
|
+
final needsScroll = !AppBreakpoints.isDesktopWidth(width);
|
|
73
|
+
|
|
74
|
+
if (!needsScroll) return child;
|
|
75
|
+
|
|
76
|
+
return SingleChildScrollView(
|
|
77
|
+
scrollDirection: Axis.horizontal,
|
|
78
|
+
child: ConstrainedBox(
|
|
79
|
+
// Make sure the child has at least the current width.
|
|
80
|
+
constraints: BoxConstraints(minWidth: width),
|
|
81
|
+
child: child,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
|
|
3
|
+
/// A centralized widget to standardize async UI states.
|
|
4
|
+
///
|
|
5
|
+
/// Goal: avoid duplicated `if (isLoading) ... else if (error) ...` blocks
|
|
6
|
+
/// across screens and keep messaging consistent.
|
|
7
|
+
///
|
|
8
|
+
/// Supported states:
|
|
9
|
+
/// - loading: shows a centered progress indicator
|
|
10
|
+
/// - error: shows a message + optional retry button
|
|
11
|
+
/// - empty: shows a message + optional action
|
|
12
|
+
/// - success: renders the provided [child]
|
|
13
|
+
///
|
|
14
|
+
/// Usage:
|
|
15
|
+
/// ```dart
|
|
16
|
+
/// return AppAsyncStateView(
|
|
17
|
+
/// isLoading: viewModel.isLoading,
|
|
18
|
+
/// errorMessage: viewModel.errorMessage,
|
|
19
|
+
/// isEmpty: viewModel.items.isEmpty,
|
|
20
|
+
/// emptyMessage: 'No items found.',
|
|
21
|
+
/// onRetry: viewModel.fetch,
|
|
22
|
+
/// child: ListView(...),
|
|
23
|
+
/// );
|
|
24
|
+
/// ```
|
|
25
|
+
class AppAsyncStateView extends StatelessWidget {
|
|
26
|
+
final bool isLoading;
|
|
27
|
+
|
|
28
|
+
/// Any non-empty string will be treated as an error state.
|
|
29
|
+
final String? errorMessage;
|
|
30
|
+
|
|
31
|
+
/// The screen decides what "empty" means (list empty, object null, etc.).
|
|
32
|
+
final bool isEmpty;
|
|
33
|
+
final String emptyMessage;
|
|
34
|
+
|
|
35
|
+
/// Optional retry action, commonly used for network errors.
|
|
36
|
+
final VoidCallback? onRetry;
|
|
37
|
+
|
|
38
|
+
/// Optional action shown in the empty state.
|
|
39
|
+
final String? emptyActionLabel;
|
|
40
|
+
final VoidCallback? onEmptyAction;
|
|
41
|
+
|
|
42
|
+
/// The content when success.
|
|
43
|
+
final Widget child;
|
|
44
|
+
|
|
45
|
+
/// You can override the default loading indicator.
|
|
46
|
+
final Widget? loadingWidget;
|
|
47
|
+
|
|
48
|
+
const AppAsyncStateView({
|
|
49
|
+
super.key,
|
|
50
|
+
required this.isLoading,
|
|
51
|
+
required this.child,
|
|
52
|
+
this.errorMessage,
|
|
53
|
+
this.isEmpty = false,
|
|
54
|
+
this.emptyMessage = 'No data found.',
|
|
55
|
+
this.onRetry,
|
|
56
|
+
this.emptyActionLabel,
|
|
57
|
+
this.onEmptyAction,
|
|
58
|
+
this.loadingWidget,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
bool get _hasError => (errorMessage ?? '').trim().isNotEmpty;
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
Widget build(BuildContext context) {
|
|
65
|
+
if (isLoading) {
|
|
66
|
+
return loadingWidget ?? const Center(child: CircularProgressIndicator());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (_hasError) {
|
|
70
|
+
return _CenteredMessage(
|
|
71
|
+
title: 'Something went wrong',
|
|
72
|
+
message: errorMessage!.trim(),
|
|
73
|
+
icon: Icons.error_outline,
|
|
74
|
+
actionLabel: onRetry == null ? null : 'Try again',
|
|
75
|
+
onAction: onRetry,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isEmpty) {
|
|
80
|
+
return _CenteredMessage(
|
|
81
|
+
title: 'Nothing here yet',
|
|
82
|
+
message: emptyMessage,
|
|
83
|
+
icon: Icons.inbox_outlined,
|
|
84
|
+
actionLabel: emptyActionLabel,
|
|
85
|
+
onAction: onEmptyAction,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return child;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class _CenteredMessage extends StatelessWidget {
|
|
94
|
+
final String title;
|
|
95
|
+
final String message;
|
|
96
|
+
final IconData icon;
|
|
97
|
+
final String? actionLabel;
|
|
98
|
+
final VoidCallback? onAction;
|
|
99
|
+
|
|
100
|
+
const _CenteredMessage({
|
|
101
|
+
required this.title,
|
|
102
|
+
required this.message,
|
|
103
|
+
required this.icon,
|
|
104
|
+
this.actionLabel,
|
|
105
|
+
this.onAction,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
@override
|
|
109
|
+
Widget build(BuildContext context) {
|
|
110
|
+
final theme = Theme.of(context);
|
|
111
|
+
return Center(
|
|
112
|
+
child: Padding(
|
|
113
|
+
padding: const EdgeInsets.all(24.0),
|
|
114
|
+
child: ConstrainedBox(
|
|
115
|
+
constraints: const BoxConstraints(maxWidth: 480),
|
|
116
|
+
child: Column(
|
|
117
|
+
mainAxisSize: MainAxisSize.min,
|
|
118
|
+
children: [
|
|
119
|
+
Icon(icon, size: 44, color: theme.colorScheme.onSurfaceVariant),
|
|
120
|
+
const SizedBox(height: 12),
|
|
121
|
+
Text(
|
|
122
|
+
title,
|
|
123
|
+
textAlign: TextAlign.center,
|
|
124
|
+
style: theme.textTheme.titleMedium,
|
|
125
|
+
),
|
|
126
|
+
const SizedBox(height: 8),
|
|
127
|
+
Text(
|
|
128
|
+
message,
|
|
129
|
+
textAlign: TextAlign.center,
|
|
130
|
+
style: theme.textTheme.bodyMedium?.copyWith(
|
|
131
|
+
color: theme.colorScheme.onSurfaceVariant,
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
if (actionLabel != null && onAction != null) ...[
|
|
135
|
+
const SizedBox(height: 16),
|
|
136
|
+
ElevatedButton(
|
|
137
|
+
onPressed: onAction,
|
|
138
|
+
child: Text(actionLabel!),
|
|
139
|
+
),
|
|
140
|
+
],
|
|
141
|
+
],
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:personal_finance_frontend_core_ui/utils/app_responsive.dart';
|
|
2
3
|
|
|
3
4
|
/// A reusable widget to display historical data in a standardized DataTable.
|
|
4
5
|
class HistoricalDataTable extends StatelessWidget {
|
|
@@ -24,8 +25,7 @@ class HistoricalDataTable extends StatelessWidget {
|
|
|
24
25
|
);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
return
|
|
28
|
-
scrollDirection: Axis.horizontal,
|
|
28
|
+
return ResponsiveHorizontalScroll(
|
|
29
29
|
child: DataTable(
|
|
30
30
|
columnSpacing: 24.0,
|
|
31
31
|
columns: columns,
|