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,207 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnSidebarItem {
|
|
6
|
+
final String label;
|
|
7
|
+
final IconData icon;
|
|
8
|
+
final IconData? activeIcon;
|
|
9
|
+
final VoidCallback? onTap;
|
|
10
|
+
final bool isActive;
|
|
11
|
+
final List<CnSidebarItem>? children;
|
|
12
|
+
|
|
13
|
+
const CnSidebarItem({
|
|
14
|
+
required this.label,
|
|
15
|
+
required this.icon,
|
|
16
|
+
this.activeIcon,
|
|
17
|
+
this.onTap,
|
|
18
|
+
this.isActive = false,
|
|
19
|
+
this.children,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class CnSidebar extends StatefulWidget {
|
|
24
|
+
final List<CnSidebarItem> items;
|
|
25
|
+
final Widget? header;
|
|
26
|
+
final Widget? footer;
|
|
27
|
+
final double width;
|
|
28
|
+
final Color? backgroundColor;
|
|
29
|
+
final bool collapsible;
|
|
30
|
+
final VoidCallback? onCollapseToggle;
|
|
31
|
+
final bool isCollapsed;
|
|
32
|
+
|
|
33
|
+
const CnSidebar({
|
|
34
|
+
super.key,
|
|
35
|
+
required this.items,
|
|
36
|
+
this.header,
|
|
37
|
+
this.footer,
|
|
38
|
+
this.width = 240,
|
|
39
|
+
this.backgroundColor,
|
|
40
|
+
this.collapsible = false,
|
|
41
|
+
this.onCollapseToggle,
|
|
42
|
+
this.isCollapsed = false,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
State<CnSidebar> createState() => _CnSidebarState();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class _CnSidebarState extends State<CnSidebar> {
|
|
50
|
+
late bool _isCollapsed;
|
|
51
|
+
final Map<int, bool> _expandedItems = {};
|
|
52
|
+
|
|
53
|
+
@override
|
|
54
|
+
void initState() {
|
|
55
|
+
super.initState();
|
|
56
|
+
_isCollapsed = widget.isCollapsed;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
void _toggleCollapse() {
|
|
60
|
+
setState(() => _isCollapsed = !_isCollapsed);
|
|
61
|
+
widget.onCollapseToggle?.call();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@override
|
|
65
|
+
Widget build(BuildContext context) {
|
|
66
|
+
return AnimatedContainer(
|
|
67
|
+
duration: const Duration(milliseconds: 200),
|
|
68
|
+
width: _isCollapsed ? 64 : widget.width,
|
|
69
|
+
decoration: BoxDecoration(
|
|
70
|
+
color: widget.backgroundColor ?? CronixColors.surface,
|
|
71
|
+
border: const Border(
|
|
72
|
+
right: BorderSide(color: CronixColors.border),
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
child: Column(
|
|
76
|
+
children: [
|
|
77
|
+
if (widget.header != null)
|
|
78
|
+
Container(
|
|
79
|
+
padding: const EdgeInsets.all(16),
|
|
80
|
+
decoration: const BoxDecoration(
|
|
81
|
+
border: Border(
|
|
82
|
+
bottom: BorderSide(color: CronixColors.border),
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
child: _isCollapsed ? null : widget.header,
|
|
86
|
+
),
|
|
87
|
+
if (widget.collapsible)
|
|
88
|
+
IconButton(
|
|
89
|
+
icon: Icon(
|
|
90
|
+
_isCollapsed ? Icons.chevron_right : Icons.chevron_left,
|
|
91
|
+
color: CronixColors.textSecondary,
|
|
92
|
+
),
|
|
93
|
+
onPressed: _toggleCollapse,
|
|
94
|
+
),
|
|
95
|
+
Expanded(
|
|
96
|
+
child: SingleChildScrollView(
|
|
97
|
+
child: Column(
|
|
98
|
+
children: widget.items.asMap().entries.map((entry) {
|
|
99
|
+
final index = entry.key;
|
|
100
|
+
final item = entry.value;
|
|
101
|
+
return _buildItem(item, index);
|
|
102
|
+
}).toList(),
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
if (widget.footer != null)
|
|
107
|
+
Container(
|
|
108
|
+
padding: const EdgeInsets.all(16),
|
|
109
|
+
decoration: const BoxDecoration(
|
|
110
|
+
border: Border(
|
|
111
|
+
top: BorderSide(color: CronixColors.border),
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
child: _isCollapsed ? null : widget.footer,
|
|
115
|
+
),
|
|
116
|
+
],
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Widget _buildItem(CnSidebarItem item, int index) {
|
|
122
|
+
final hasChildren = item.children != null && item.children!.isNotEmpty;
|
|
123
|
+
final isExpanded = _expandedItems[index] ?? false;
|
|
124
|
+
|
|
125
|
+
return Column(
|
|
126
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
127
|
+
children: [
|
|
128
|
+
InkWell(
|
|
129
|
+
onTap: hasChildren
|
|
130
|
+
? () => setState(() => _expandedItems[index] = !isExpanded)
|
|
131
|
+
: item.onTap,
|
|
132
|
+
child: Container(
|
|
133
|
+
padding: EdgeInsets.symmetric(
|
|
134
|
+
horizontal: _isCollapsed ? 20 : 16,
|
|
135
|
+
vertical: 12,
|
|
136
|
+
),
|
|
137
|
+
decoration: BoxDecoration(
|
|
138
|
+
color: item.isActive
|
|
139
|
+
? CronixColors.accent.withOpacity(0.15)
|
|
140
|
+
: null,
|
|
141
|
+
border: item.isActive
|
|
142
|
+
? const Border(
|
|
143
|
+
left: BorderSide(color: CronixColors.accent, width: 3),
|
|
144
|
+
)
|
|
145
|
+
: null,
|
|
146
|
+
),
|
|
147
|
+
child: Row(
|
|
148
|
+
children: [
|
|
149
|
+
Icon(
|
|
150
|
+
item.isActive && item.activeIcon != null
|
|
151
|
+
? item.activeIcon!
|
|
152
|
+
: item.icon,
|
|
153
|
+
size: 20,
|
|
154
|
+
color: item.isActive ? CronixColors.accent : CronixColors.textSecondary,
|
|
155
|
+
),
|
|
156
|
+
if (!_isCollapsed) ...[
|
|
157
|
+
const SizedBox(width: 12),
|
|
158
|
+
Expanded(
|
|
159
|
+
child: Text(
|
|
160
|
+
item.label,
|
|
161
|
+
style: TextStyle(
|
|
162
|
+
color: item.isActive ? CronixColors.accent : CronixColors.text,
|
|
163
|
+
fontWeight: item.isActive ? FontWeight.w500 : FontWeight.w400,
|
|
164
|
+
),
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
if (hasChildren)
|
|
168
|
+
Icon(
|
|
169
|
+
isExpanded ? Icons.expand_less : Icons.expand_more,
|
|
170
|
+
size: 18,
|
|
171
|
+
color: CronixColors.textSecondary,
|
|
172
|
+
),
|
|
173
|
+
],
|
|
174
|
+
],
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
),
|
|
178
|
+
if (hasChildren && !_isCollapsed && isExpanded)
|
|
179
|
+
...item.children!.map((child) => Container(
|
|
180
|
+
margin: const EdgeInsets.only(left: 32),
|
|
181
|
+
child: _buildChildItem(child),
|
|
182
|
+
)),
|
|
183
|
+
],
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
Widget _buildChildItem(CnSidebarItem item) {
|
|
188
|
+
return InkWell(
|
|
189
|
+
onTap: item.onTap,
|
|
190
|
+
child: Container(
|
|
191
|
+
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
192
|
+
decoration: BoxDecoration(
|
|
193
|
+
color: item.isActive
|
|
194
|
+
? CronixColors.accent.withOpacity(0.1)
|
|
195
|
+
: null,
|
|
196
|
+
),
|
|
197
|
+
child: Text(
|
|
198
|
+
item.label,
|
|
199
|
+
style: TextStyle(
|
|
200
|
+
color: item.isActive ? CronixColors.accent : CronixColors.textSecondary,
|
|
201
|
+
fontSize: 13,
|
|
202
|
+
),
|
|
203
|
+
),
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
enum CnSkeletonVariant { text, circular, rectangular }
|
|
6
|
+
|
|
7
|
+
class CnSkeleton extends StatefulWidget {
|
|
8
|
+
final double? width;
|
|
9
|
+
final double? height;
|
|
10
|
+
final CnSkeletonVariant variant;
|
|
11
|
+
final BorderRadius? borderRadius;
|
|
12
|
+
|
|
13
|
+
const CnSkeleton({
|
|
14
|
+
super.key,
|
|
15
|
+
this.width,
|
|
16
|
+
this.height,
|
|
17
|
+
this.variant = CnSkeletonVariant.rectangular,
|
|
18
|
+
this.borderRadius,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const CnSkeleton.text({
|
|
22
|
+
super.key,
|
|
23
|
+
this.width,
|
|
24
|
+
this.height = 14,
|
|
25
|
+
this.borderRadius,
|
|
26
|
+
}) : variant = CnSkeletonVariant.text;
|
|
27
|
+
|
|
28
|
+
const CnSkeleton.circular({
|
|
29
|
+
super.key,
|
|
30
|
+
double? size,
|
|
31
|
+
}) : width = size,
|
|
32
|
+
height = size,
|
|
33
|
+
variant = CnSkeletonVariant.circular,
|
|
34
|
+
borderRadius = null;
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
State<CnSkeleton> createState() => _CnSkeletonState();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class _CnSkeletonState extends State<CnSkeleton>
|
|
41
|
+
with SingleTickerProviderStateMixin {
|
|
42
|
+
late AnimationController _controller;
|
|
43
|
+
late Animation<double> _animation;
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
void initState() {
|
|
47
|
+
super.initState();
|
|
48
|
+
_controller = AnimationController(
|
|
49
|
+
vsync: this,
|
|
50
|
+
duration: const Duration(milliseconds: 1500),
|
|
51
|
+
)..repeat();
|
|
52
|
+
_animation = Tween<double>(begin: -1, end: 2).animate(
|
|
53
|
+
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
void dispose() {
|
|
59
|
+
_controller.dispose();
|
|
60
|
+
super.dispose();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
Widget build(BuildContext context) {
|
|
65
|
+
return AnimatedBuilder(
|
|
66
|
+
animation: _animation,
|
|
67
|
+
builder: (context, child) {
|
|
68
|
+
return Container(
|
|
69
|
+
width: widget.width,
|
|
70
|
+
height: widget.height,
|
|
71
|
+
decoration: BoxDecoration(
|
|
72
|
+
color: CronixColors.shimmerBase,
|
|
73
|
+
borderRadius: _getBorderRadius(),
|
|
74
|
+
),
|
|
75
|
+
child: ClipRRect(
|
|
76
|
+
borderRadius: _getBorderRadius(),
|
|
77
|
+
child: LinearGradient(
|
|
78
|
+
begin: Alignment.topLeft,
|
|
79
|
+
end: Alignment.bottomRight,
|
|
80
|
+
colors: const [
|
|
81
|
+
CronixColors.shimmerBase,
|
|
82
|
+
CronixColors.shimmerHighlight,
|
|
83
|
+
CronixColors.shimmerBase,
|
|
84
|
+
],
|
|
85
|
+
stops: [
|
|
86
|
+
_animation.value - 0.3,
|
|
87
|
+
_animation.value,
|
|
88
|
+
_animation.value + 0.3,
|
|
89
|
+
].map((e) => e.clamp(0.0, 1.0)).toList(),
|
|
90
|
+
).createShader(
|
|
91
|
+
Rect.fromLTWH(0, 0, widget.width ?? 100, widget.height ?? 14),
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
BorderRadius _getBorderRadius() {
|
|
100
|
+
if (widget.borderRadius != null) return widget.borderRadius!;
|
|
101
|
+
switch (widget.variant) {
|
|
102
|
+
case CnSkeletonVariant.text:
|
|
103
|
+
return CronixRadius.radiusSM;
|
|
104
|
+
case CnSkeletonVariant.circular:
|
|
105
|
+
return BorderRadius.circular((widget.width ?? 40) / 2);
|
|
106
|
+
case CnSkeletonVariant.rectangular:
|
|
107
|
+
return CronixRadius.radiusMD;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class CnSkeletonList extends StatelessWidget {
|
|
113
|
+
final int itemCount;
|
|
114
|
+
final double itemHeight;
|
|
115
|
+
final double spacing;
|
|
116
|
+
|
|
117
|
+
const CnSkeletonList({
|
|
118
|
+
super.key,
|
|
119
|
+
this.itemCount = 5,
|
|
120
|
+
this.itemHeight = 60,
|
|
121
|
+
this.spacing = 8,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
@override
|
|
125
|
+
Widget build(BuildContext context) {
|
|
126
|
+
return Column(
|
|
127
|
+
children: List.generate(
|
|
128
|
+
itemCount,
|
|
129
|
+
(index) => Padding(
|
|
130
|
+
padding: EdgeInsets.only(bottom: index < itemCount - 1 ? spacing : 0),
|
|
131
|
+
child: const CnSkeleton(height: 60),
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnSlider extends StatefulWidget {
|
|
6
|
+
final double min;
|
|
7
|
+
final double max;
|
|
8
|
+
final double value;
|
|
9
|
+
final ValueChanged<double>? onChanged;
|
|
10
|
+
final int divisions;
|
|
11
|
+
final String? label;
|
|
12
|
+
final bool enabled;
|
|
13
|
+
final String Function(double)? valueFormatter;
|
|
14
|
+
|
|
15
|
+
const CnSlider({
|
|
16
|
+
super.key,
|
|
17
|
+
this.min = 0,
|
|
18
|
+
this.max = 100,
|
|
19
|
+
required this.value,
|
|
20
|
+
this.onChanged,
|
|
21
|
+
this.divisions = 100,
|
|
22
|
+
this.label,
|
|
23
|
+
this.enabled = true,
|
|
24
|
+
this.valueFormatter,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
State<CnSlider> createState() => _CnSliderState();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class _CnSliderState extends State<CnSlider> {
|
|
32
|
+
late double _value;
|
|
33
|
+
|
|
34
|
+
@override
|
|
35
|
+
void initState() {
|
|
36
|
+
super.initState();
|
|
37
|
+
_value = widget.value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@override
|
|
41
|
+
void didUpdateWidget(CnSlider oldWidget) {
|
|
42
|
+
super.didUpdateWidget(oldWidget);
|
|
43
|
+
if (oldWidget.value != widget.value) {
|
|
44
|
+
_value = widget.value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
String _formatValue(double value) {
|
|
49
|
+
if (widget.valueFormatter != null) {
|
|
50
|
+
return widget.valueFormatter!(value);
|
|
51
|
+
}
|
|
52
|
+
return value.toStringAsFixed(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
Widget build(BuildContext context) {
|
|
57
|
+
final percent = (_value - widget.min) / (widget.max - widget.min);
|
|
58
|
+
|
|
59
|
+
return Column(
|
|
60
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
61
|
+
mainAxisSize: MainAxisSize.min,
|
|
62
|
+
children: [
|
|
63
|
+
if (widget.label != null) ...[
|
|
64
|
+
Row(
|
|
65
|
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
66
|
+
children: [
|
|
67
|
+
Text(
|
|
68
|
+
widget.label!,
|
|
69
|
+
style: const TextStyle(
|
|
70
|
+
color: CronixColors.text,
|
|
71
|
+
fontSize: 14,
|
|
72
|
+
fontWeight: FontWeight.w500,
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
Text(
|
|
76
|
+
_formatValue(_value),
|
|
77
|
+
style: const TextStyle(
|
|
78
|
+
color: CronixColors.accent,
|
|
79
|
+
fontSize: 14,
|
|
80
|
+
fontWeight: FontWeight.w600,
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
],
|
|
84
|
+
),
|
|
85
|
+
const SizedBox(height: 12),
|
|
86
|
+
],
|
|
87
|
+
Stack(
|
|
88
|
+
clipBehavior: Clip.none,
|
|
89
|
+
children: [
|
|
90
|
+
Container(
|
|
91
|
+
height: 4,
|
|
92
|
+
decoration: BoxDecoration(
|
|
93
|
+
color: CronixColors.border,
|
|
94
|
+
borderRadius: CronixRadius.radiusFull,
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
FractionallySizedBox(
|
|
98
|
+
widthFactor: percent,
|
|
99
|
+
child: Container(
|
|
100
|
+
height: 4,
|
|
101
|
+
decoration: BoxDecoration(
|
|
102
|
+
color: widget.enabled ? CronixColors.accent : CronixColors.textMuted,
|
|
103
|
+
borderRadius: CronixRadius.radiusFull,
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
Positioned(
|
|
108
|
+
left: MediaQuery.of(context).size.width > 0
|
|
109
|
+
? 0
|
|
110
|
+
: 0,
|
|
111
|
+
child: LayoutBuilder(
|
|
112
|
+
builder: (context, constraints) {
|
|
113
|
+
return GestureDetector(
|
|
114
|
+
behavior: HitTestBehavior.opaque,
|
|
115
|
+
onHorizontalDragUpdate: widget.enabled
|
|
116
|
+
? (details) {
|
|
117
|
+
final box = context.findRenderObject() as RenderBox;
|
|
118
|
+
final localPosition = box.globalToLocal(details.globalPosition);
|
|
119
|
+
final sliderWidth = box.size.width;
|
|
120
|
+
final newValue = widget.min +
|
|
121
|
+
(localPosition.dx / sliderWidth) * (widget.max - widget.min);
|
|
122
|
+
final clampedValue = newValue.clamp(widget.min, widget.max);
|
|
123
|
+
setState(() => _value = clampedValue);
|
|
124
|
+
widget.onChanged?.call(clampedValue);
|
|
125
|
+
}
|
|
126
|
+
: null,
|
|
127
|
+
child: Container(
|
|
128
|
+
width: double.infinity,
|
|
129
|
+
height: 24,
|
|
130
|
+
color: Colors.transparent,
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
],
|
|
137
|
+
),
|
|
138
|
+
],
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
enum CnSpinnerSize { sm, md, lg }
|
|
6
|
+
|
|
7
|
+
class CnSpinner extends StatelessWidget {
|
|
8
|
+
final CnSpinnerSize size;
|
|
9
|
+
final Color? color;
|
|
10
|
+
final double? strokeWidth;
|
|
11
|
+
|
|
12
|
+
const CnSpinner({
|
|
13
|
+
super.key,
|
|
14
|
+
this.size = CnSpinnerSize.md,
|
|
15
|
+
this.color,
|
|
16
|
+
this.strokeWidth,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
double get _size {
|
|
20
|
+
switch (size) {
|
|
21
|
+
case CnSpinnerSize.sm:
|
|
22
|
+
return 16;
|
|
23
|
+
case CnSpinnerSize.md:
|
|
24
|
+
return 24;
|
|
25
|
+
case CnSpinnerSize.lg:
|
|
26
|
+
return 40;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
double get _strokeWidth {
|
|
31
|
+
return strokeWidth ?? _size / 8;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@override
|
|
35
|
+
Widget build(BuildContext context) {
|
|
36
|
+
return SizedBox(
|
|
37
|
+
width: _size,
|
|
38
|
+
height: _size,
|
|
39
|
+
child: CircularProgressIndicator(
|
|
40
|
+
strokeWidth: _strokeWidth,
|
|
41
|
+
valueColor: AlwaysStoppedAnimation<Color>(
|
|
42
|
+
color ?? CronixColors.accent,
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class CnSpinnerOverlay extends StatelessWidget {
|
|
50
|
+
final String? message;
|
|
51
|
+
final bool visible;
|
|
52
|
+
|
|
53
|
+
const CnSpinnerOverlay({
|
|
54
|
+
super.key,
|
|
55
|
+
this.message,
|
|
56
|
+
this.visible = true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
Widget build(BuildContext context) {
|
|
61
|
+
if (!visible) return const SizedBox.shrink();
|
|
62
|
+
|
|
63
|
+
return Container(
|
|
64
|
+
color: Colors.black54,
|
|
65
|
+
child: Center(
|
|
66
|
+
child: Column(
|
|
67
|
+
mainAxisSize: MainAxisSize.min,
|
|
68
|
+
children: [
|
|
69
|
+
const CnSpinner(size: CnSpinnerSize.lg),
|
|
70
|
+
if (message != null) ...[
|
|
71
|
+
const SizedBox(height: 16),
|
|
72
|
+
Text(
|
|
73
|
+
message!,
|
|
74
|
+
style: const TextStyle(
|
|
75
|
+
color: CronixColors.text,
|
|
76
|
+
fontSize: 14,
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
],
|
|
80
|
+
],
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnStat extends StatelessWidget {
|
|
6
|
+
final String label;
|
|
7
|
+
final String value;
|
|
8
|
+
final String? change;
|
|
9
|
+
final bool changePositive;
|
|
10
|
+
final IconData? icon;
|
|
11
|
+
final Color? accentColor;
|
|
12
|
+
final Widget? trailing;
|
|
13
|
+
|
|
14
|
+
const CnStat({
|
|
15
|
+
super.key,
|
|
16
|
+
required this.label,
|
|
17
|
+
required this.value,
|
|
18
|
+
this.change,
|
|
19
|
+
this.changePositive = true,
|
|
20
|
+
this.icon,
|
|
21
|
+
this.accentColor,
|
|
22
|
+
this.trailing,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
@override
|
|
26
|
+
Widget build(BuildContext context) {
|
|
27
|
+
return Container(
|
|
28
|
+
padding: const EdgeInsets.all(16),
|
|
29
|
+
decoration: BoxDecoration(
|
|
30
|
+
color: CronixColors.surface,
|
|
31
|
+
borderRadius: CronixRadius.radiusMD,
|
|
32
|
+
border: Border.all(color: CronixColors.border),
|
|
33
|
+
),
|
|
34
|
+
child: Row(
|
|
35
|
+
children: [
|
|
36
|
+
if (icon != null) ...[
|
|
37
|
+
Container(
|
|
38
|
+
padding: const EdgeInsets.all(12),
|
|
39
|
+
decoration: BoxDecoration(
|
|
40
|
+
color: (accentColor ?? CronixColors.accent).withOpacity(0.15),
|
|
41
|
+
borderRadius: CronixRadius.radiusMD,
|
|
42
|
+
),
|
|
43
|
+
child: Icon(
|
|
44
|
+
icon,
|
|
45
|
+
size: 24,
|
|
46
|
+
color: accentColor ?? CronixColors.accent,
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
const SizedBox(width: 16),
|
|
50
|
+
],
|
|
51
|
+
Expanded(
|
|
52
|
+
child: Column(
|
|
53
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
54
|
+
children: [
|
|
55
|
+
Text(
|
|
56
|
+
label,
|
|
57
|
+
style: const TextStyle(
|
|
58
|
+
color: CronixColors.textSecondary,
|
|
59
|
+
fontSize: 13,
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
const SizedBox(height: 4),
|
|
63
|
+
Text(
|
|
64
|
+
value,
|
|
65
|
+
style: const TextStyle(
|
|
66
|
+
color: CronixColors.text,
|
|
67
|
+
fontSize: 28,
|
|
68
|
+
fontWeight: FontWeight.w600,
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
if (change != null) ...[
|
|
72
|
+
const SizedBox(height: 4),
|
|
73
|
+
Row(
|
|
74
|
+
children: [
|
|
75
|
+
Icon(
|
|
76
|
+
changePositive
|
|
77
|
+
? Icons.arrow_upward
|
|
78
|
+
: Icons.arrow_downward,
|
|
79
|
+
size: 14,
|
|
80
|
+
color: changePositive
|
|
81
|
+
? CronixColors.success
|
|
82
|
+
: CronixColors.error,
|
|
83
|
+
),
|
|
84
|
+
const SizedBox(width: 4),
|
|
85
|
+
Text(
|
|
86
|
+
change!,
|
|
87
|
+
style: TextStyle(
|
|
88
|
+
color: changePositive
|
|
89
|
+
? CronixColors.success
|
|
90
|
+
: CronixColors.error,
|
|
91
|
+
fontSize: 12,
|
|
92
|
+
fontWeight: FontWeight.w500,
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
],
|
|
96
|
+
),
|
|
97
|
+
],
|
|
98
|
+
],
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
if (trailing != null) trailing!,
|
|
102
|
+
],
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class CnStatGrid extends StatelessWidget {
|
|
109
|
+
final List<CnStat> stats;
|
|
110
|
+
final int crossAxisCount;
|
|
111
|
+
final double spacing;
|
|
112
|
+
|
|
113
|
+
const CnStatGrid({
|
|
114
|
+
super.key,
|
|
115
|
+
required this.stats,
|
|
116
|
+
this.crossAxisCount = 4,
|
|
117
|
+
this.spacing = 16,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
@override
|
|
121
|
+
Widget build(BuildContext context) {
|
|
122
|
+
return GridView.builder(
|
|
123
|
+
shrinkWrap: true,
|
|
124
|
+
physics: const NeverScrollableScrollPhysics(),
|
|
125
|
+
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
126
|
+
crossAxisCount: crossAxisCount,
|
|
127
|
+
crossAxisSpacing: spacing,
|
|
128
|
+
mainAxisSpacing: spacing,
|
|
129
|
+
childAspectRatio: 1.5,
|
|
130
|
+
),
|
|
131
|
+
itemCount: stats.length,
|
|
132
|
+
itemBuilder: (context, index) => stats[index],
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|