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,108 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnFooter extends StatelessWidget {
|
|
6
|
+
final List<Widget>? leading;
|
|
7
|
+
final List<Widget>? trailing;
|
|
8
|
+
final String? copyright;
|
|
9
|
+
final Color? backgroundColor;
|
|
10
|
+
final double height;
|
|
11
|
+
final bool bordered;
|
|
12
|
+
|
|
13
|
+
const CnFooter({
|
|
14
|
+
super.key,
|
|
15
|
+
this.leading,
|
|
16
|
+
this.trailing,
|
|
17
|
+
this.copyright,
|
|
18
|
+
this.backgroundColor,
|
|
19
|
+
this.height = 56,
|
|
20
|
+
this.bordered = true,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
@override
|
|
24
|
+
Widget build(BuildContext context) {
|
|
25
|
+
return Container(
|
|
26
|
+
height: height,
|
|
27
|
+
decoration: BoxDecoration(
|
|
28
|
+
color: backgroundColor ?? CronixColors.surface,
|
|
29
|
+
border: bordered
|
|
30
|
+
? const Border(
|
|
31
|
+
top: BorderSide(color: CronixColors.border),
|
|
32
|
+
)
|
|
33
|
+
: null,
|
|
34
|
+
),
|
|
35
|
+
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
36
|
+
child: Row(
|
|
37
|
+
children: [
|
|
38
|
+
if (leading != null) ...leading!,
|
|
39
|
+
const Spacer(),
|
|
40
|
+
if (copyright != null)
|
|
41
|
+
Text(
|
|
42
|
+
copyright!,
|
|
43
|
+
style: const TextStyle(
|
|
44
|
+
color: CronixColors.textMuted,
|
|
45
|
+
fontSize: 12,
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
const Spacer(),
|
|
49
|
+
if (trailing != null) ...trailing!,
|
|
50
|
+
],
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class CnFooterLinks extends StatelessWidget {
|
|
57
|
+
final List<FooterLink> links;
|
|
58
|
+
final String? divider;
|
|
59
|
+
|
|
60
|
+
const CnFooterLinks({
|
|
61
|
+
super.key,
|
|
62
|
+
required this.links,
|
|
63
|
+
this.divider,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
@override
|
|
67
|
+
Widget build(BuildContext context) {
|
|
68
|
+
return Row(
|
|
69
|
+
children: links.asMap().entries.map((entry) {
|
|
70
|
+
final index = entry.key;
|
|
71
|
+
final link = entry.value;
|
|
72
|
+
return Row(
|
|
73
|
+
children: [
|
|
74
|
+
if (index > 0 && divider != null)
|
|
75
|
+
Padding(
|
|
76
|
+
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
77
|
+
child: Text(
|
|
78
|
+
divider!,
|
|
79
|
+
style: const TextStyle(color: CronixColors.textMuted),
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
InkWell(
|
|
83
|
+
onTap: link.onTap,
|
|
84
|
+
child: Text(
|
|
85
|
+
link.label,
|
|
86
|
+
style: TextStyle(
|
|
87
|
+
color: CronixColors.textSecondary,
|
|
88
|
+
fontSize: 12,
|
|
89
|
+
decoration: TextDecoration.underline,
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
],
|
|
94
|
+
);
|
|
95
|
+
}).toList(),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class FooterLink {
|
|
101
|
+
final String label;
|
|
102
|
+
final VoidCallback? onTap;
|
|
103
|
+
|
|
104
|
+
const FooterLink({
|
|
105
|
+
required this.label,
|
|
106
|
+
this.onTap,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnHeader extends StatelessWidget implements PreferredSizeWidget {
|
|
6
|
+
final String? title;
|
|
7
|
+
final List<Widget>? actions;
|
|
8
|
+
final Widget? leading;
|
|
9
|
+
final bool showBackButton;
|
|
10
|
+
final double height;
|
|
11
|
+
final Color? backgroundColor;
|
|
12
|
+
final bool borderless;
|
|
13
|
+
final Widget? flexibleSpace;
|
|
14
|
+
|
|
15
|
+
const CnHeader({
|
|
16
|
+
super.key,
|
|
17
|
+
this.title,
|
|
18
|
+
this.actions,
|
|
19
|
+
this.leading,
|
|
20
|
+
this.showBackButton = false,
|
|
21
|
+
this.height = 56,
|
|
22
|
+
this.backgroundColor,
|
|
23
|
+
this.borderless = false,
|
|
24
|
+
this.flexibleSpace,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
Size get preferredSize => Size.fromHeight(height);
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
Widget build(BuildContext context) {
|
|
32
|
+
return Container(
|
|
33
|
+
decoration: BoxDecoration(
|
|
34
|
+
color: backgroundColor ?? CronixColors.background,
|
|
35
|
+
border: borderless ? null : const Border(
|
|
36
|
+
bottom: BorderSide(color: CronixColors.border),
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
child: AppBar(
|
|
40
|
+
backgroundColor: Colors.transparent,
|
|
41
|
+
elevation: 0,
|
|
42
|
+
scrolledUnderElevation: 0,
|
|
43
|
+
automaticallyImplyLeading: false,
|
|
44
|
+
leading: showBackButton
|
|
45
|
+
? IconButton(
|
|
46
|
+
icon: const Icon(Icons.arrow_back, color: CronixColors.text),
|
|
47
|
+
onPressed: () => Navigator.of(context).pop(),
|
|
48
|
+
)
|
|
49
|
+
: leading,
|
|
50
|
+
title: title != null
|
|
51
|
+
? Text(
|
|
52
|
+
title!,
|
|
53
|
+
style: const TextStyle(
|
|
54
|
+
color: CronixColors.text,
|
|
55
|
+
fontSize: 18,
|
|
56
|
+
fontWeight: FontWeight.w600,
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
: null,
|
|
60
|
+
actions: actions?.map((action) => Padding(
|
|
61
|
+
padding: const EdgeInsets.only(right: 8),
|
|
62
|
+
child: action,
|
|
63
|
+
)).toList(),
|
|
64
|
+
flexibleSpace: flexibleSpace,
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class CnHeaderWithSearch extends StatefulWidget implements PreferredSizeWidget {
|
|
71
|
+
final String title;
|
|
72
|
+
final ValueChanged<String>? onSearch;
|
|
73
|
+
final String? searchPlaceholder;
|
|
74
|
+
final List<Widget>? actions;
|
|
75
|
+
final double height;
|
|
76
|
+
|
|
77
|
+
const CnHeaderWithSearch({
|
|
78
|
+
super.key,
|
|
79
|
+
required this.title,
|
|
80
|
+
this.onSearch,
|
|
81
|
+
this.searchPlaceholder,
|
|
82
|
+
this.actions,
|
|
83
|
+
this.height = 56,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
@override
|
|
87
|
+
Size get preferredSize => Size.fromHeight(height * 2);
|
|
88
|
+
|
|
89
|
+
@override
|
|
90
|
+
State<CnHeaderWithSearch> createState() => _CnHeaderWithSearchState();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class _CnHeaderWithSearchState extends State<CnHeaderWithSearch> {
|
|
94
|
+
bool _isSearching = false;
|
|
95
|
+
final TextEditingController _searchController = TextEditingController();
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
void dispose() {
|
|
99
|
+
_searchController.dispose();
|
|
100
|
+
super.dispose();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@override
|
|
104
|
+
Widget build(BuildContext context) {
|
|
105
|
+
return Container(
|
|
106
|
+
decoration: const BoxDecoration(
|
|
107
|
+
color: CronixColors.background,
|
|
108
|
+
border: Border(
|
|
109
|
+
bottom: BorderSide(color: CronixColors.border),
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
child: Column(
|
|
113
|
+
children: [
|
|
114
|
+
SizedBox(
|
|
115
|
+
height: widget.height,
|
|
116
|
+
child: Row(
|
|
117
|
+
children: [
|
|
118
|
+
Expanded(
|
|
119
|
+
child: _isSearching
|
|
120
|
+
? TextField(
|
|
121
|
+
controller: _searchController,
|
|
122
|
+
autofocus: true,
|
|
123
|
+
onChanged: widget.onSearch,
|
|
124
|
+
style: const TextStyle(color: CronixColors.text),
|
|
125
|
+
decoration: InputDecoration(
|
|
126
|
+
hintText: widget.searchPlaceholder ?? 'Search...',
|
|
127
|
+
hintStyle: const TextStyle(color: CronixColors.textMuted),
|
|
128
|
+
filled: true,
|
|
129
|
+
fillColor: CronixColors.surface,
|
|
130
|
+
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
|
131
|
+
border: OutlineInputBorder(
|
|
132
|
+
borderRadius: CronixRadius.radiusMD,
|
|
133
|
+
borderSide: BorderSide.none,
|
|
134
|
+
),
|
|
135
|
+
prefixIcon: const Icon(Icons.search, color: CronixColors.textSecondary),
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
: Padding(
|
|
139
|
+
padding: const EdgeInsets.only(left: 16),
|
|
140
|
+
child: Text(
|
|
141
|
+
widget.title,
|
|
142
|
+
style: const TextStyle(
|
|
143
|
+
color: CronixColors.text,
|
|
144
|
+
fontSize: 20,
|
|
145
|
+
fontWeight: FontWeight.w600,
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
),
|
|
150
|
+
IconButton(
|
|
151
|
+
icon: Icon(
|
|
152
|
+
_isSearching ? Icons.close : Icons.search,
|
|
153
|
+
color: CronixColors.text,
|
|
154
|
+
),
|
|
155
|
+
onPressed: () {
|
|
156
|
+
setState(() {
|
|
157
|
+
_isSearching = !_isSearching;
|
|
158
|
+
if (!_isSearching) {
|
|
159
|
+
_searchController.clear();
|
|
160
|
+
widget.onSearch?.call('');
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
),
|
|
165
|
+
if (widget.actions != null) ...widget.actions!,
|
|
166
|
+
],
|
|
167
|
+
),
|
|
168
|
+
),
|
|
169
|
+
],
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnInput extends StatefulWidget {
|
|
6
|
+
final String? label;
|
|
7
|
+
final String? placeholder;
|
|
8
|
+
final String? initialValue;
|
|
9
|
+
final TextEditingController? controller;
|
|
10
|
+
final bool obscureText;
|
|
11
|
+
final bool enabled;
|
|
12
|
+
final int maxLines;
|
|
13
|
+
final IconData? prefixIcon;
|
|
14
|
+
final IconData? suffixIcon;
|
|
15
|
+
final VoidCallback? onSuffixTap;
|
|
16
|
+
final ValueChanged<String>? onChanged;
|
|
17
|
+
final ValueChanged<String>? onSubmitted;
|
|
18
|
+
final String? Function(String?)? validator;
|
|
19
|
+
final TextInputType? keyboardType;
|
|
20
|
+
final FocusNode? focusNode;
|
|
21
|
+
|
|
22
|
+
const CnInput({
|
|
23
|
+
super.key,
|
|
24
|
+
this.label,
|
|
25
|
+
this.placeholder,
|
|
26
|
+
this.initialValue,
|
|
27
|
+
this.controller,
|
|
28
|
+
this.obscureText = false,
|
|
29
|
+
this.enabled = true,
|
|
30
|
+
this.maxLines = 1,
|
|
31
|
+
this.prefixIcon,
|
|
32
|
+
this.suffixIcon,
|
|
33
|
+
this.onSuffixTap,
|
|
34
|
+
this.onChanged,
|
|
35
|
+
this.onSubmitted,
|
|
36
|
+
this.validator,
|
|
37
|
+
this.keyboardType,
|
|
38
|
+
this.focusNode,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
@override
|
|
42
|
+
State<CnInput> createState() => _CnInputState();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class _CnInputState extends State<CnInput> {
|
|
46
|
+
late TextEditingController _controller;
|
|
47
|
+
String? _errorText;
|
|
48
|
+
|
|
49
|
+
@override
|
|
50
|
+
void initState() {
|
|
51
|
+
super.initState();
|
|
52
|
+
_controller = widget.controller ??
|
|
53
|
+
TextEditingController(text: widget.initialValue);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
void dispose() {
|
|
58
|
+
if (widget.controller == null) {
|
|
59
|
+
_controller.dispose();
|
|
60
|
+
}
|
|
61
|
+
super.dispose();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@override
|
|
65
|
+
Widget build(BuildContext context) {
|
|
66
|
+
return Column(
|
|
67
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
68
|
+
mainAxisSize: MainAxisSize.min,
|
|
69
|
+
children: [
|
|
70
|
+
if (widget.label != null) ...[
|
|
71
|
+
Text(
|
|
72
|
+
widget.label!,
|
|
73
|
+
style: const TextStyle(
|
|
74
|
+
color: CronixColors.text,
|
|
75
|
+
fontSize: 14,
|
|
76
|
+
fontWeight: FontWeight.w500,
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
const SizedBox(height: 8),
|
|
80
|
+
],
|
|
81
|
+
TextFormField(
|
|
82
|
+
controller: _controller,
|
|
83
|
+
obscureText: widget.obscureText,
|
|
84
|
+
enabled: widget.enabled,
|
|
85
|
+
maxLines: widget.maxLines,
|
|
86
|
+
keyboardType: widget.keyboardType,
|
|
87
|
+
focusNode: widget.focusNode,
|
|
88
|
+
onChanged: widget.onChanged,
|
|
89
|
+
onFieldSubmitted: widget.onSubmitted,
|
|
90
|
+
validator: (value) {
|
|
91
|
+
if (widget.validator != null) {
|
|
92
|
+
final error = widget.validator!(value);
|
|
93
|
+
setState(() => _errorText = error);
|
|
94
|
+
return error;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
},
|
|
98
|
+
style: const TextStyle(color: CronixColors.text),
|
|
99
|
+
decoration: InputDecoration(
|
|
100
|
+
hintText: widget.placeholder,
|
|
101
|
+
hintStyle: const TextStyle(color: CronixColors.textMuted),
|
|
102
|
+
filled: true,
|
|
103
|
+
fillColor: widget.enabled ? CronixColors.surface : CronixColors.surfaceLight,
|
|
104
|
+
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
|
105
|
+
border: OutlineInputBorder(
|
|
106
|
+
borderRadius: CronixRadius.radiusMD,
|
|
107
|
+
borderSide: const BorderSide(color: CronixColors.border),
|
|
108
|
+
),
|
|
109
|
+
enabledBorder: OutlineInputBorder(
|
|
110
|
+
borderRadius: CronixRadius.radiusMD,
|
|
111
|
+
borderSide: const BorderSide(color: CronixColors.border),
|
|
112
|
+
),
|
|
113
|
+
focusedBorder: OutlineInputBorder(
|
|
114
|
+
borderRadius: CronixRadius.radiusMD,
|
|
115
|
+
borderSide: const BorderSide(color: CronixColors.accent, width: 2),
|
|
116
|
+
),
|
|
117
|
+
errorBorder: OutlineInputBorder(
|
|
118
|
+
borderRadius: CronixRadius.radiusMD,
|
|
119
|
+
borderSide: const BorderSide(color: CronixColors.error),
|
|
120
|
+
),
|
|
121
|
+
prefixIcon: widget.prefixIcon != null
|
|
122
|
+
? Icon(widget.prefixIcon, color: CronixColors.textSecondary, size: 20)
|
|
123
|
+
: null,
|
|
124
|
+
suffixIcon: widget.suffixIcon != null
|
|
125
|
+
? IconButton(
|
|
126
|
+
icon: Icon(widget.suffixIcon, color: CronixColors.textSecondary, size: 20),
|
|
127
|
+
onPressed: widget.onSuffixTap,
|
|
128
|
+
)
|
|
129
|
+
: null,
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
if (_errorText != null) ...[
|
|
133
|
+
const SizedBox(height: 4),
|
|
134
|
+
Text(
|
|
135
|
+
_errorText!,
|
|
136
|
+
style: const TextStyle(color: CronixColors.error, fontSize: 12),
|
|
137
|
+
),
|
|
138
|
+
],
|
|
139
|
+
],
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnListItem {
|
|
6
|
+
final String title;
|
|
7
|
+
final String? subtitle;
|
|
8
|
+
final IconData? leadingIcon;
|
|
9
|
+
final IconData? trailingIcon;
|
|
10
|
+
final VoidCallback? onTap;
|
|
11
|
+
final Widget? trailing;
|
|
12
|
+
final Color? backgroundColor;
|
|
13
|
+
|
|
14
|
+
const CnListItem({
|
|
15
|
+
required this.title,
|
|
16
|
+
this.subtitle,
|
|
17
|
+
this.leadingIcon,
|
|
18
|
+
this.trailingIcon,
|
|
19
|
+
this.onTap,
|
|
20
|
+
this.trailing,
|
|
21
|
+
this.backgroundColor,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class CnList extends StatelessWidget {
|
|
26
|
+
final List<CnListItem> items;
|
|
27
|
+
final bool divided;
|
|
28
|
+
final bool bordered;
|
|
29
|
+
final double itemHeight;
|
|
30
|
+
|
|
31
|
+
const CnList({
|
|
32
|
+
super.key,
|
|
33
|
+
required this.items,
|
|
34
|
+
this.divided = true,
|
|
35
|
+
this.bordered = false,
|
|
36
|
+
this.itemHeight = 56,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
@override
|
|
40
|
+
Widget build(BuildContext context) {
|
|
41
|
+
return Container(
|
|
42
|
+
decoration: BoxDecoration(
|
|
43
|
+
color: CronixColors.surface,
|
|
44
|
+
borderRadius: CronixRadius.radiusMD,
|
|
45
|
+
border: bordered ? Border.all(color: CronixColors.border) : null,
|
|
46
|
+
),
|
|
47
|
+
child: ListView.separated(
|
|
48
|
+
shrinkWrap: true,
|
|
49
|
+
physics: const NeverScrollableScrollPhysics(),
|
|
50
|
+
itemCount: items.length,
|
|
51
|
+
separatorBuilder: (context, index) => divided
|
|
52
|
+
? const Divider(color: CronixColors.border, height: 1)
|
|
53
|
+
: const SizedBox.shrink(),
|
|
54
|
+
itemBuilder: (context, index) {
|
|
55
|
+
final item = items[index];
|
|
56
|
+
return _buildItem(item);
|
|
57
|
+
},
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Widget _buildItem(CnListItem item) {
|
|
63
|
+
return InkWell(
|
|
64
|
+
onTap: item.onTap,
|
|
65
|
+
child: Container(
|
|
66
|
+
height: itemHeight,
|
|
67
|
+
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
68
|
+
color: item.backgroundColor,
|
|
69
|
+
child: Row(
|
|
70
|
+
children: [
|
|
71
|
+
if (item.leadingIcon != null) ...[
|
|
72
|
+
Icon(
|
|
73
|
+
item.leadingIcon,
|
|
74
|
+
size: 20,
|
|
75
|
+
color: CronixColors.textSecondary,
|
|
76
|
+
),
|
|
77
|
+
const SizedBox(width: 12),
|
|
78
|
+
],
|
|
79
|
+
Expanded(
|
|
80
|
+
child: Column(
|
|
81
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
82
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
83
|
+
children: [
|
|
84
|
+
Text(
|
|
85
|
+
item.title,
|
|
86
|
+
style: const TextStyle(
|
|
87
|
+
color: CronixColors.text,
|
|
88
|
+
fontSize: 14,
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
if (item.subtitle != null) ...[
|
|
92
|
+
const SizedBox(height: 2),
|
|
93
|
+
Text(
|
|
94
|
+
item.subtitle!,
|
|
95
|
+
style: const TextStyle(
|
|
96
|
+
color: CronixColors.textSecondary,
|
|
97
|
+
fontSize: 12,
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
],
|
|
101
|
+
],
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
if (item.trailing != null) item.trailing!,
|
|
105
|
+
if (item.trailingIcon != null)
|
|
106
|
+
Icon(
|
|
107
|
+
item.trailingIcon,
|
|
108
|
+
size: 18,
|
|
109
|
+
color: CronixColors.textMuted,
|
|
110
|
+
),
|
|
111
|
+
],
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class CnListBuilder<T> extends StatelessWidget {
|
|
119
|
+
final List<T> items;
|
|
120
|
+
final Widget Function(BuildContext context, T item, int index) itemBuilder;
|
|
121
|
+
final bool divided;
|
|
122
|
+
final bool bordered;
|
|
123
|
+
|
|
124
|
+
const CnListBuilder({
|
|
125
|
+
super.key,
|
|
126
|
+
required this.items,
|
|
127
|
+
required this.itemBuilder,
|
|
128
|
+
this.divided = true,
|
|
129
|
+
this.bordered = false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
@override
|
|
133
|
+
Widget build(BuildContext context) {
|
|
134
|
+
return Container(
|
|
135
|
+
decoration: BoxDecoration(
|
|
136
|
+
borderRadius: CronixRadius.radiusMD,
|
|
137
|
+
border: bordered ? Border.all(color: CronixColors.border) : null,
|
|
138
|
+
),
|
|
139
|
+
child: ListView.separated(
|
|
140
|
+
shrinkWrap: true,
|
|
141
|
+
physics: const NeverScrollableScrollPhysics(),
|
|
142
|
+
itemCount: items.length,
|
|
143
|
+
separatorBuilder: (context, index) => divided
|
|
144
|
+
? const Divider(color: CronixColors.border, height: 1)
|
|
145
|
+
: const SizedBox.shrink(),
|
|
146
|
+
itemBuilder: (context, index) => itemBuilder(context, items[index], index),
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|