cronixui 1.1.0 → 1.1.1
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/README.md +12 -27
- package/package.json +2 -1
- package/packages/flutter/lib/cronixui.dart +41 -0
- package/packages/flutter/lib/src/tokens/colors.dart +34 -0
- package/packages/flutter/lib/src/tokens/spacing.dart +54 -0
- package/packages/flutter/lib/src/tokens/theme.dart +174 -0
- package/packages/flutter/lib/src/widgets/cn_accordion.dart +254 -0
- package/packages/flutter/lib/src/widgets/cn_alert.dart +137 -0
- package/packages/flutter/lib/src/widgets/cn_avatar.dart +98 -0
- package/packages/flutter/lib/src/widgets/cn_badge.dart +80 -0
- package/packages/flutter/lib/src/widgets/cn_breadcrumb.dart +88 -0
- package/packages/flutter/lib/src/widgets/cn_button.dart +137 -0
- package/packages/flutter/lib/src/widgets/cn_card.dart +99 -0
- package/packages/flutter/lib/src/widgets/cn_checkbox.dart +77 -0
- package/packages/flutter/lib/src/widgets/cn_command_palette.dart +299 -0
- package/packages/flutter/lib/src/widgets/cn_container.dart +131 -0
- package/packages/flutter/lib/src/widgets/cn_dropdown.dart +149 -0
- package/packages/flutter/lib/src/widgets/cn_file_input.dart +113 -0
- package/packages/flutter/lib/src/widgets/cn_footer.dart +108 -0
- package/packages/flutter/lib/src/widgets/cn_header.dart +173 -0
- package/packages/flutter/lib/src/widgets/cn_input.dart +142 -0
- package/packages/flutter/lib/src/widgets/cn_list.dart +150 -0
- package/packages/flutter/lib/src/widgets/cn_modal.dart +213 -0
- package/packages/flutter/lib/src/widgets/cn_nav.dart +157 -0
- package/packages/flutter/lib/src/widgets/cn_pagination.dart +193 -0
- package/packages/flutter/lib/src/widgets/cn_progress.dart +146 -0
- package/packages/flutter/lib/src/widgets/cn_radio.dart +133 -0
- package/packages/flutter/lib/src/widgets/cn_search.dart +183 -0
- package/packages/flutter/lib/src/widgets/cn_select.dart +244 -0
- package/packages/flutter/lib/src/widgets/cn_sidebar.dart +207 -0
- package/packages/flutter/lib/src/widgets/cn_skeleton.dart +136 -0
- package/packages/flutter/lib/src/widgets/cn_slider.dart +141 -0
- package/packages/flutter/lib/src/widgets/cn_spinner.dart +85 -0
- package/packages/flutter/lib/src/widgets/cn_stat.dart +135 -0
- package/packages/flutter/lib/src/widgets/cn_table.dart +136 -0
- package/packages/flutter/lib/src/widgets/cn_tabs.dart +229 -0
- package/packages/flutter/lib/src/widgets/cn_tag.dart +185 -0
- package/packages/flutter/lib/src/widgets/cn_textarea.dart +143 -0
- package/packages/flutter/lib/src/widgets/cn_toast.dart +121 -0
- package/packages/flutter/lib/src/widgets/cn_toggle.dart +78 -0
- package/packages/flutter/lib/src/widgets/cn_tooltip.dart +118 -0
- package/packages/flutter/pubspec.yaml +20 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnModal extends StatelessWidget {
|
|
6
|
+
final String? title;
|
|
7
|
+
final Widget? content;
|
|
8
|
+
final List<Widget>? actions;
|
|
9
|
+
final bool fullWidth;
|
|
10
|
+
final double? width;
|
|
11
|
+
final double? maxHeight;
|
|
12
|
+
final bool showCloseButton;
|
|
13
|
+
final VoidCallback? onClose;
|
|
14
|
+
final IconData? icon;
|
|
15
|
+
final Color? iconColor;
|
|
16
|
+
|
|
17
|
+
const CnModal({
|
|
18
|
+
super.key,
|
|
19
|
+
this.title,
|
|
20
|
+
this.content,
|
|
21
|
+
this.actions,
|
|
22
|
+
this.fullWidth = false,
|
|
23
|
+
this.width,
|
|
24
|
+
this.maxHeight,
|
|
25
|
+
this.showCloseButton = true,
|
|
26
|
+
this.onClose,
|
|
27
|
+
this.icon,
|
|
28
|
+
this.iconColor,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
Widget build(BuildContext context) {
|
|
33
|
+
return Dialog(
|
|
34
|
+
backgroundColor: Colors.transparent,
|
|
35
|
+
child: Container(
|
|
36
|
+
width: fullWidth ? double.infinity : (width ?? 400),
|
|
37
|
+
constraints: BoxConstraints(
|
|
38
|
+
maxWidth: fullWidth ? double.infinity : 500,
|
|
39
|
+
maxHeight: maxHeight ?? 600,
|
|
40
|
+
),
|
|
41
|
+
decoration: BoxDecoration(
|
|
42
|
+
color: CronixColors.surface,
|
|
43
|
+
borderRadius: CronixRadius.radiusLG,
|
|
44
|
+
border: Border.all(color: CronixColors.border),
|
|
45
|
+
),
|
|
46
|
+
child: Column(
|
|
47
|
+
mainAxisSize: MainAxisSize.min,
|
|
48
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
49
|
+
children: [
|
|
50
|
+
if (title != null || showCloseButton || icon != null)
|
|
51
|
+
Container(
|
|
52
|
+
padding: const EdgeInsets.all(16),
|
|
53
|
+
decoration: const BoxDecoration(
|
|
54
|
+
border: Border(
|
|
55
|
+
bottom: BorderSide(color: CronixColors.border),
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
child: Row(
|
|
59
|
+
children: [
|
|
60
|
+
if (icon != null) ...[
|
|
61
|
+
Icon(icon, color: iconColor ?? CronixColors.accent, size: 24),
|
|
62
|
+
const SizedBox(width: 12),
|
|
63
|
+
],
|
|
64
|
+
Expanded(
|
|
65
|
+
child: Text(
|
|
66
|
+
title ?? '',
|
|
67
|
+
style: const TextStyle(
|
|
68
|
+
color: CronixColors.text,
|
|
69
|
+
fontSize: 18,
|
|
70
|
+
fontWeight: FontWeight.w600,
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
if (showCloseButton)
|
|
75
|
+
GestureDetector(
|
|
76
|
+
onTap: onClose ?? () => Navigator.of(context).pop(),
|
|
77
|
+
child: const Icon(
|
|
78
|
+
Icons.close,
|
|
79
|
+
color: CronixColors.textSecondary,
|
|
80
|
+
size: 20,
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
],
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
if (content != null)
|
|
87
|
+
Flexible(
|
|
88
|
+
child: SingleChildScrollView(
|
|
89
|
+
padding: const EdgeInsets.all(16),
|
|
90
|
+
child: content,
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
if (actions != null && actions!.isNotEmpty)
|
|
94
|
+
Container(
|
|
95
|
+
padding: const EdgeInsets.all(16),
|
|
96
|
+
decoration: const BoxDecoration(
|
|
97
|
+
border: Border(
|
|
98
|
+
top: BorderSide(color: CronixColors.border),
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
child: Row(
|
|
102
|
+
mainAxisAlignment: MainAxisAlignment.end,
|
|
103
|
+
children: [
|
|
104
|
+
for (int i = 0; i < actions!.length; i++) ...[
|
|
105
|
+
if (i > 0) const SizedBox(width: 8),
|
|
106
|
+
actions![i],
|
|
107
|
+
],
|
|
108
|
+
],
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
],
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static Future<T?> show<T>({
|
|
118
|
+
required BuildContext context,
|
|
119
|
+
String? title,
|
|
120
|
+
Widget? content,
|
|
121
|
+
List<Widget>? actions,
|
|
122
|
+
bool fullWidth = false,
|
|
123
|
+
bool barrierDismissible = true,
|
|
124
|
+
bool showCloseButton = true,
|
|
125
|
+
}) {
|
|
126
|
+
return showDialog<T>(
|
|
127
|
+
context: context,
|
|
128
|
+
barrierDismissible: barrierDismissible,
|
|
129
|
+
builder: (context) => CnModal(
|
|
130
|
+
title: title,
|
|
131
|
+
content: content,
|
|
132
|
+
actions: actions,
|
|
133
|
+
fullWidth: fullWidth,
|
|
134
|
+
showCloseButton: showCloseButton,
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class CnConfirmModal extends StatelessWidget {
|
|
141
|
+
final String title;
|
|
142
|
+
final String message;
|
|
143
|
+
final String confirmText;
|
|
144
|
+
final String cancelText;
|
|
145
|
+
final Color? confirmColor;
|
|
146
|
+
final VoidCallback? onConfirm;
|
|
147
|
+
final VoidCallback? onCancel;
|
|
148
|
+
final IconData? icon;
|
|
149
|
+
final Color? iconColor;
|
|
150
|
+
|
|
151
|
+
const CnConfirmModal({
|
|
152
|
+
super.key,
|
|
153
|
+
required this.title,
|
|
154
|
+
required this.message,
|
|
155
|
+
this.confirmText = 'Confirm',
|
|
156
|
+
this.cancelText = 'Cancel',
|
|
157
|
+
this.confirmColor,
|
|
158
|
+
this.onConfirm,
|
|
159
|
+
this.onCancel,
|
|
160
|
+
this.icon,
|
|
161
|
+
this.iconColor,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
@override
|
|
165
|
+
Widget build(BuildContext context) {
|
|
166
|
+
return CnModal(
|
|
167
|
+
title: title,
|
|
168
|
+
icon: icon ?? Icons.help_outline,
|
|
169
|
+
iconColor: iconColor,
|
|
170
|
+
content: Text(
|
|
171
|
+
message,
|
|
172
|
+
style: const TextStyle(color: CronixColors.text),
|
|
173
|
+
),
|
|
174
|
+
actions: [
|
|
175
|
+
TextButton(
|
|
176
|
+
onPressed: onCancel ?? () => Navigator.of(context).pop(false),
|
|
177
|
+
child: Text(cancelText, style: const TextStyle(color: CronixColors.textSecondary)),
|
|
178
|
+
),
|
|
179
|
+
ElevatedButton(
|
|
180
|
+
onPressed: onConfirm ?? () => Navigator.of(context).pop(true),
|
|
181
|
+
style: ElevatedButton.styleFrom(
|
|
182
|
+
backgroundColor: confirmColor ?? CronixColors.accent,
|
|
183
|
+
),
|
|
184
|
+
child: Text(confirmText, style: const TextStyle(color: CronixColors.text)),
|
|
185
|
+
),
|
|
186
|
+
],
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
static Future<bool?> show({
|
|
191
|
+
required BuildContext context,
|
|
192
|
+
required String title,
|
|
193
|
+
required String message,
|
|
194
|
+
String confirmText = 'Confirm',
|
|
195
|
+
String cancelText = 'Cancel',
|
|
196
|
+
Color? confirmColor,
|
|
197
|
+
IconData? icon,
|
|
198
|
+
Color? iconColor,
|
|
199
|
+
}) {
|
|
200
|
+
return showDialog<bool>(
|
|
201
|
+
context: context,
|
|
202
|
+
builder: (context) => CnConfirmModal(
|
|
203
|
+
title: title,
|
|
204
|
+
message: message,
|
|
205
|
+
confirmText: confirmText,
|
|
206
|
+
cancelText: cancelText,
|
|
207
|
+
confirmColor: confirmColor,
|
|
208
|
+
icon: icon,
|
|
209
|
+
iconColor: iconColor,
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnNavItem {
|
|
6
|
+
final String label;
|
|
7
|
+
final IconData icon;
|
|
8
|
+
final IconData? activeIcon;
|
|
9
|
+
final VoidCallback? onTap;
|
|
10
|
+
final bool isActive;
|
|
11
|
+
|
|
12
|
+
const CnNavItem({
|
|
13
|
+
required this.label,
|
|
14
|
+
required this.icon,
|
|
15
|
+
this.activeIcon,
|
|
16
|
+
this.onTap,
|
|
17
|
+
this.isActive = false,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class CnNav extends StatelessWidget {
|
|
22
|
+
final List<CnNavItem> items;
|
|
23
|
+
final int? selectedIndex;
|
|
24
|
+
final ValueChanged<int>? onIndexChanged;
|
|
25
|
+
final Color? backgroundColor;
|
|
26
|
+
final bool showLabels;
|
|
27
|
+
final double height;
|
|
28
|
+
|
|
29
|
+
const CnNav({
|
|
30
|
+
super.key,
|
|
31
|
+
required this.items,
|
|
32
|
+
this.selectedIndex,
|
|
33
|
+
this.onIndexChanged,
|
|
34
|
+
this.backgroundColor,
|
|
35
|
+
this.showLabels = true,
|
|
36
|
+
this.height = 56,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
@override
|
|
40
|
+
Widget build(BuildContext context) {
|
|
41
|
+
return Container(
|
|
42
|
+
height: height,
|
|
43
|
+
color: backgroundColor ?? CronixColors.surface,
|
|
44
|
+
child: Row(
|
|
45
|
+
children: items.asMap().entries.map((entry) {
|
|
46
|
+
final index = entry.key;
|
|
47
|
+
final item = entry.value;
|
|
48
|
+
final isSelected = selectedIndex == index || item.isActive;
|
|
49
|
+
|
|
50
|
+
return Expanded(
|
|
51
|
+
child: InkWell(
|
|
52
|
+
onTap: item.onTap ?? (onIndexChanged != null ? () => onIndexChanged!(index) : null),
|
|
53
|
+
child: Container(
|
|
54
|
+
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
55
|
+
child: Column(
|
|
56
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
57
|
+
children: [
|
|
58
|
+
Icon(
|
|
59
|
+
isSelected && item.activeIcon != null
|
|
60
|
+
? item.activeIcon!
|
|
61
|
+
: item.icon,
|
|
62
|
+
size: 24,
|
|
63
|
+
color: isSelected ? CronixColors.accent : CronixColors.textSecondary,
|
|
64
|
+
),
|
|
65
|
+
if (showLabels) ...[
|
|
66
|
+
const SizedBox(height: 4),
|
|
67
|
+
Text(
|
|
68
|
+
item.label,
|
|
69
|
+
style: TextStyle(
|
|
70
|
+
color: isSelected ? CronixColors.accent : CronixColors.textSecondary,
|
|
71
|
+
fontSize: 12,
|
|
72
|
+
fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400,
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
],
|
|
76
|
+
],
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
}).toList(),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class CnNavRail extends StatelessWidget {
|
|
88
|
+
final List<CnNavItem> items;
|
|
89
|
+
final int? selectedIndex;
|
|
90
|
+
final ValueChanged<int>? onIndexChanged;
|
|
91
|
+
final Color? backgroundColor;
|
|
92
|
+
final bool showLabels;
|
|
93
|
+
final double width;
|
|
94
|
+
|
|
95
|
+
const CnNavRail({
|
|
96
|
+
super.key,
|
|
97
|
+
required this.items,
|
|
98
|
+
this.selectedIndex,
|
|
99
|
+
this.onIndexChanged,
|
|
100
|
+
this.backgroundColor,
|
|
101
|
+
this.showLabels = true,
|
|
102
|
+
this.width = 72,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
@override
|
|
106
|
+
Widget build(BuildContext context) {
|
|
107
|
+
return Container(
|
|
108
|
+
width: width,
|
|
109
|
+
color: backgroundColor ?? CronixColors.surface,
|
|
110
|
+
child: Column(
|
|
111
|
+
children: items.asMap().entries.map((entry) {
|
|
112
|
+
final index = entry.key;
|
|
113
|
+
final item = entry.value;
|
|
114
|
+
final isSelected = selectedIndex == index || item.isActive;
|
|
115
|
+
|
|
116
|
+
return InkWell(
|
|
117
|
+
onTap: item.onTap ?? (onIndexChanged != null ? () => onIndexChanged!(index) : null),
|
|
118
|
+
child: Container(
|
|
119
|
+
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
120
|
+
child: Column(
|
|
121
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
122
|
+
children: [
|
|
123
|
+
Container(
|
|
124
|
+
width: 48,
|
|
125
|
+
height: 32,
|
|
126
|
+
decoration: BoxDecoration(
|
|
127
|
+
color: isSelected ? CronixColors.accent.withOpacity(0.2) : null,
|
|
128
|
+
borderRadius: CronixRadius.radiusMD,
|
|
129
|
+
),
|
|
130
|
+
child: Icon(
|
|
131
|
+
isSelected && item.activeIcon != null
|
|
132
|
+
? item.activeIcon!
|
|
133
|
+
: item.icon,
|
|
134
|
+
size: 22,
|
|
135
|
+
color: isSelected ? CronixColors.accent : CronixColors.textSecondary,
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
if (showLabels) ...[
|
|
139
|
+
const SizedBox(height: 4),
|
|
140
|
+
Text(
|
|
141
|
+
item.label,
|
|
142
|
+
style: TextStyle(
|
|
143
|
+
color: isSelected ? CronixColors.accent : CronixColors.textSecondary,
|
|
144
|
+
fontSize: 11,
|
|
145
|
+
fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400,
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
],
|
|
149
|
+
],
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}).toList(),
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnPagination extends StatelessWidget {
|
|
6
|
+
final int currentPage;
|
|
7
|
+
final int totalPages;
|
|
8
|
+
final ValueChanged<int>? onPageChanged;
|
|
9
|
+
final bool showPageNumbers;
|
|
10
|
+
final int maxVisiblePages;
|
|
11
|
+
final String? previousLabel;
|
|
12
|
+
final String? nextLabel;
|
|
13
|
+
|
|
14
|
+
const CnPagination({
|
|
15
|
+
super.key,
|
|
16
|
+
required this.currentPage,
|
|
17
|
+
required this.totalPages,
|
|
18
|
+
this.onPageChanged,
|
|
19
|
+
this.showPageNumbers = true,
|
|
20
|
+
this.maxVisiblePages = 5,
|
|
21
|
+
this.previousLabel,
|
|
22
|
+
this.nextLabel,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
List<int> get _visiblePages {
|
|
26
|
+
if (totalPages <= maxVisiblePages) {
|
|
27
|
+
return List.generate(totalPages, (i) => i + 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
int start = currentPage - (maxVisiblePages ~/ 2);
|
|
31
|
+
int end = currentPage + (maxVisiblePages ~/ 2);
|
|
32
|
+
|
|
33
|
+
if (start < 1) {
|
|
34
|
+
end += (1 - start);
|
|
35
|
+
start = 1;
|
|
36
|
+
}
|
|
37
|
+
if (end > totalPages) {
|
|
38
|
+
start -= (end - totalPages);
|
|
39
|
+
end = totalPages;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return List.generate(end - start + 1, (i) => start + i);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
Widget build(BuildContext context) {
|
|
47
|
+
return Row(
|
|
48
|
+
mainAxisSize: MainAxisSize.min,
|
|
49
|
+
children: [
|
|
50
|
+
_PageButton(
|
|
51
|
+
icon: Icons.chevron_left,
|
|
52
|
+
label: previousLabel,
|
|
53
|
+
enabled: currentPage > 1,
|
|
54
|
+
onPressed: () => onPageChanged?.call(currentPage - 1),
|
|
55
|
+
),
|
|
56
|
+
if (showPageNumbers) ...[
|
|
57
|
+
if (_visiblePages.first > 1) ...[
|
|
58
|
+
_PageNumber(
|
|
59
|
+
page: 1,
|
|
60
|
+
isSelected: currentPage == 1,
|
|
61
|
+
onPressed: () => onPageChanged?.call(1),
|
|
62
|
+
),
|
|
63
|
+
if (_visiblePages.first > 2)
|
|
64
|
+
const Padding(
|
|
65
|
+
padding: EdgeInsets.symmetric(horizontal: 4),
|
|
66
|
+
child: Text('...', style: TextStyle(color: CronixColors.textMuted)),
|
|
67
|
+
),
|
|
68
|
+
],
|
|
69
|
+
..._visiblePages.map((page) => Padding(
|
|
70
|
+
padding: const EdgeInsets.symmetric(horizontal: 2),
|
|
71
|
+
child: _PageNumber(
|
|
72
|
+
page: page,
|
|
73
|
+
isSelected: currentPage == page,
|
|
74
|
+
onPressed: () => onPageChanged?.call(page),
|
|
75
|
+
),
|
|
76
|
+
)),
|
|
77
|
+
if (_visiblePages.last < totalPages) ...[
|
|
78
|
+
if (_visiblePages.last < totalPages - 1)
|
|
79
|
+
const Padding(
|
|
80
|
+
padding: EdgeInsets.symmetric(horizontal: 4),
|
|
81
|
+
child: Text('...', style: TextStyle(color: CronixColors.textMuted)),
|
|
82
|
+
),
|
|
83
|
+
_PageNumber(
|
|
84
|
+
page: totalPages,
|
|
85
|
+
isSelected: currentPage == totalPages,
|
|
86
|
+
onPressed: () => onPageChanged?.call(totalPages),
|
|
87
|
+
),
|
|
88
|
+
],
|
|
89
|
+
],
|
|
90
|
+
_PageButton(
|
|
91
|
+
icon: Icons.chevron_right,
|
|
92
|
+
label: nextLabel,
|
|
93
|
+
enabled: currentPage < totalPages,
|
|
94
|
+
onPressed: () => onPageChanged?.call(currentPage + 1),
|
|
95
|
+
),
|
|
96
|
+
],
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class _PageButton extends StatelessWidget {
|
|
102
|
+
final IconData icon;
|
|
103
|
+
final String? label;
|
|
104
|
+
final bool enabled;
|
|
105
|
+
final VoidCallback? onPressed;
|
|
106
|
+
|
|
107
|
+
const _PageButton({
|
|
108
|
+
required this.icon,
|
|
109
|
+
this.label,
|
|
110
|
+
required this.enabled,
|
|
111
|
+
this.onPressed,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
@override
|
|
115
|
+
Widget build(BuildContext context) {
|
|
116
|
+
return Material(
|
|
117
|
+
color: Colors.transparent,
|
|
118
|
+
child: InkWell(
|
|
119
|
+
onTap: enabled ? onPressed : null,
|
|
120
|
+
borderRadius: CronixRadius.radiusSM,
|
|
121
|
+
child: Container(
|
|
122
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
123
|
+
decoration: BoxDecoration(
|
|
124
|
+
color: CronixColors.surface,
|
|
125
|
+
borderRadius: CronixRadius.radiusSM,
|
|
126
|
+
border: Border.all(color: CronixColors.border),
|
|
127
|
+
),
|
|
128
|
+
child: Row(
|
|
129
|
+
children: [
|
|
130
|
+
Icon(
|
|
131
|
+
icon,
|
|
132
|
+
size: 16,
|
|
133
|
+
color: enabled ? CronixColors.text : CronixColors.textMuted,
|
|
134
|
+
),
|
|
135
|
+
if (label != null) ...[
|
|
136
|
+
const SizedBox(width: 4),
|
|
137
|
+
Text(
|
|
138
|
+
label!,
|
|
139
|
+
style: TextStyle(
|
|
140
|
+
color: enabled ? CronixColors.text : CronixColors.textMuted,
|
|
141
|
+
fontSize: 13,
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
],
|
|
145
|
+
],
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
class _PageNumber extends StatelessWidget {
|
|
154
|
+
final int page;
|
|
155
|
+
final bool isSelected;
|
|
156
|
+
final VoidCallback? onPressed;
|
|
157
|
+
|
|
158
|
+
const _PageNumber({
|
|
159
|
+
required this.page,
|
|
160
|
+
required this.isSelected,
|
|
161
|
+
this.onPressed,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
@override
|
|
165
|
+
Widget build(BuildContext context) {
|
|
166
|
+
return Material(
|
|
167
|
+
color: Colors.transparent,
|
|
168
|
+
child: InkWell(
|
|
169
|
+
onTap: onPressed,
|
|
170
|
+
borderRadius: CronixRadius.radiusSM,
|
|
171
|
+
child: Container(
|
|
172
|
+
width: 36,
|
|
173
|
+
height: 36,
|
|
174
|
+
decoration: BoxDecoration(
|
|
175
|
+
color: isSelected ? CronixColors.accent : CronixColors.surface,
|
|
176
|
+
borderRadius: CronixRadius.radiusSM,
|
|
177
|
+
border: isSelected ? null : Border.all(color: CronixColors.border),
|
|
178
|
+
),
|
|
179
|
+
child: Center(
|
|
180
|
+
child: Text(
|
|
181
|
+
page.toString(),
|
|
182
|
+
style: TextStyle(
|
|
183
|
+
color: isSelected ? CronixColors.text : CronixColors.text,
|
|
184
|
+
fontSize: 13,
|
|
185
|
+
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
|
186
|
+
),
|
|
187
|
+
),
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnProgress extends StatelessWidget {
|
|
6
|
+
final double value;
|
|
7
|
+
final double max;
|
|
8
|
+
final double height;
|
|
9
|
+
final Color? backgroundColor;
|
|
10
|
+
final Color? valueColor;
|
|
11
|
+
final bool showLabel;
|
|
12
|
+
final String? label;
|
|
13
|
+
|
|
14
|
+
const CnProgress({
|
|
15
|
+
super.key,
|
|
16
|
+
required this.value,
|
|
17
|
+
this.max = 100,
|
|
18
|
+
this.height = 8,
|
|
19
|
+
this.backgroundColor,
|
|
20
|
+
this.valueColor,
|
|
21
|
+
this.showLabel = false,
|
|
22
|
+
this.label,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
double get _percentage => (value / max).clamp(0.0, 1.0);
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
Widget build(BuildContext context) {
|
|
29
|
+
return Column(
|
|
30
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
31
|
+
mainAxisSize: MainAxisSize.min,
|
|
32
|
+
children: [
|
|
33
|
+
if (showLabel || label != null) ...[
|
|
34
|
+
Row(
|
|
35
|
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
36
|
+
children: [
|
|
37
|
+
Text(
|
|
38
|
+
label ?? 'Progress',
|
|
39
|
+
style: const TextStyle(
|
|
40
|
+
color: CronixColors.text,
|
|
41
|
+
fontSize: 12,
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
Text(
|
|
45
|
+
'${(_percentage * 100).toStringAsFixed(0)}%',
|
|
46
|
+
style: const TextStyle(
|
|
47
|
+
color: CronixColors.textSecondary,
|
|
48
|
+
fontSize: 12,
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
],
|
|
52
|
+
),
|
|
53
|
+
const SizedBox(height: 8),
|
|
54
|
+
],
|
|
55
|
+
Container(
|
|
56
|
+
height: height,
|
|
57
|
+
decoration: BoxDecoration(
|
|
58
|
+
color: backgroundColor ?? CronixColors.border,
|
|
59
|
+
borderRadius: CronixRadius.radiusFull,
|
|
60
|
+
),
|
|
61
|
+
child: FractionallySizedBox(
|
|
62
|
+
alignment: Alignment.centerLeft,
|
|
63
|
+
widthFactor: _percentage,
|
|
64
|
+
child: Container(
|
|
65
|
+
decoration: BoxDecoration(
|
|
66
|
+
color: valueColor ?? CronixColors.accent,
|
|
67
|
+
borderRadius: CronixRadius.radiusFull,
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
],
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class CnProgressIndeterminate extends StatefulWidget {
|
|
78
|
+
final double height;
|
|
79
|
+
final Color? backgroundColor;
|
|
80
|
+
final Color? valueColor;
|
|
81
|
+
|
|
82
|
+
const CnProgressIndeterminate({
|
|
83
|
+
super.key,
|
|
84
|
+
this.height = 8,
|
|
85
|
+
this.backgroundColor,
|
|
86
|
+
this.valueColor,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
@override
|
|
90
|
+
State<CnProgressIndeterminate> createState() => _CnProgressIndeterminateState();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class _CnProgressIndeterminateState extends State<CnProgressIndeterminate>
|
|
94
|
+
with SingleTickerProviderStateMixin {
|
|
95
|
+
late AnimationController _controller;
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
void initState() {
|
|
99
|
+
super.initState();
|
|
100
|
+
_controller = AnimationController(
|
|
101
|
+
vsync: this,
|
|
102
|
+
duration: const Duration(milliseconds: 1500),
|
|
103
|
+
)..repeat();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@override
|
|
107
|
+
void dispose() {
|
|
108
|
+
_controller.dispose();
|
|
109
|
+
super.dispose();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@override
|
|
113
|
+
Widget build(BuildContext context) {
|
|
114
|
+
return AnimatedBuilder(
|
|
115
|
+
animation: _controller,
|
|
116
|
+
builder: (context, child) {
|
|
117
|
+
return Container(
|
|
118
|
+
height: widget.height,
|
|
119
|
+
decoration: BoxDecoration(
|
|
120
|
+
color: widget.backgroundColor ?? CronixColors.border,
|
|
121
|
+
borderRadius: CronixRadius.radiusFull,
|
|
122
|
+
),
|
|
123
|
+
child: LayoutBuilder(
|
|
124
|
+
builder: (context, constraints) {
|
|
125
|
+
return Stack(
|
|
126
|
+
children: [
|
|
127
|
+
Positioned(
|
|
128
|
+
left: constraints.maxWidth * _controller.value * 0.8,
|
|
129
|
+
child: Container(
|
|
130
|
+
width: constraints.maxWidth * 0.3,
|
|
131
|
+
height: widget.height,
|
|
132
|
+
decoration: BoxDecoration(
|
|
133
|
+
color: widget.valueColor ?? CronixColors.accent,
|
|
134
|
+
borderRadius: CronixRadius.radiusFull,
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
],
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|