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,136 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnTableColumn<T> {
|
|
6
|
+
final String label;
|
|
7
|
+
final double? width;
|
|
8
|
+
final bool sortable;
|
|
9
|
+
final String? Function(T item)? valueBuilder;
|
|
10
|
+
final Widget Function(T item)? cellBuilder;
|
|
11
|
+
final TextAlign alignment;
|
|
12
|
+
|
|
13
|
+
const CnTableColumn({
|
|
14
|
+
required this.label,
|
|
15
|
+
this.width,
|
|
16
|
+
this.sortable = false,
|
|
17
|
+
this.valueBuilder,
|
|
18
|
+
this.cellBuilder,
|
|
19
|
+
this.alignment = TextAlign.left,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class CnTable<T> extends StatefulWidget {
|
|
24
|
+
final List<CnTableColumn<T>> columns;
|
|
25
|
+
final List<T> data;
|
|
26
|
+
final void Function(T item)? onRowTap;
|
|
27
|
+
final bool striped;
|
|
28
|
+
final bool bordered;
|
|
29
|
+
final String? emptyMessage;
|
|
30
|
+
final Widget? emptyWidget;
|
|
31
|
+
|
|
32
|
+
const CnTable({
|
|
33
|
+
super.key,
|
|
34
|
+
required this.columns,
|
|
35
|
+
required this.data,
|
|
36
|
+
this.onRowTap,
|
|
37
|
+
this.striped = false,
|
|
38
|
+
this.bordered = false,
|
|
39
|
+
this.emptyMessage,
|
|
40
|
+
this.emptyWidget,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
@override
|
|
44
|
+
State<CnTable<T>> createState() => _CnTableState<T>();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class _CnTableState<T> extends State<CnTable<T>> {
|
|
48
|
+
int? _sortColumnIndex;
|
|
49
|
+
bool _sortAscending = true;
|
|
50
|
+
|
|
51
|
+
void _sort(int columnIndex) {
|
|
52
|
+
setState(() {
|
|
53
|
+
if (_sortColumnIndex == columnIndex) {
|
|
54
|
+
_sortAscending = !_sortAscending;
|
|
55
|
+
} else {
|
|
56
|
+
_sortColumnIndex = columnIndex;
|
|
57
|
+
_sortAscending = true;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@override
|
|
63
|
+
Widget build(BuildContext context) {
|
|
64
|
+
if (widget.data.isEmpty) {
|
|
65
|
+
return widget.emptyWidget ??
|
|
66
|
+
Center(
|
|
67
|
+
child: Padding(
|
|
68
|
+
padding: const EdgeInsets.all(32),
|
|
69
|
+
child: Text(
|
|
70
|
+
widget.emptyMessage ?? 'No data available',
|
|
71
|
+
style: const TextStyle(color: CronixColors.textMuted),
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return Container(
|
|
78
|
+
decoration: BoxDecoration(
|
|
79
|
+
border: widget.bordered
|
|
80
|
+
? Border.all(color: CronixColors.border)
|
|
81
|
+
: null,
|
|
82
|
+
borderRadius: CronixRadius.radiusMD,
|
|
83
|
+
),
|
|
84
|
+
child: SingleChildScrollView(
|
|
85
|
+
scrollDirection: Axis.horizontal,
|
|
86
|
+
child: DataTable(
|
|
87
|
+
headingRowColor: WidgetStateProperty.all(CronixColors.surfaceLight),
|
|
88
|
+
dataRowColor: WidgetStateProperty.resolveWith((states) {
|
|
89
|
+
if (widget.striped) {
|
|
90
|
+
final index = states.contains(WidgetState.selected) ? -1 : 0;
|
|
91
|
+
return index % 2 == 0
|
|
92
|
+
? CronixColors.surface
|
|
93
|
+
: CronixColors.surfaceLight;
|
|
94
|
+
}
|
|
95
|
+
return Colors.transparent;
|
|
96
|
+
}),
|
|
97
|
+
dividerThickness: widget.bordered ? 1 : 0,
|
|
98
|
+
columns: widget.columns.map((col) {
|
|
99
|
+
return DataColumn(
|
|
100
|
+
label: Text(
|
|
101
|
+
col.label,
|
|
102
|
+
style: const TextStyle(
|
|
103
|
+
color: CronixColors.text,
|
|
104
|
+
fontWeight: FontWeight.w600,
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
onSort: col.sortable ? (_) => _sort(widget.columns.indexOf(col)) : null,
|
|
108
|
+
);
|
|
109
|
+
}).toList(),
|
|
110
|
+
rows: widget.data.map((item) {
|
|
111
|
+
return DataRow(
|
|
112
|
+
onSelectChanged: widget.onRowTap != null
|
|
113
|
+
? (_) => widget.onRowTap!(item)
|
|
114
|
+
: null,
|
|
115
|
+
cells: widget.columns.map((col) {
|
|
116
|
+
Widget cell;
|
|
117
|
+
if (col.cellBuilder != null) {
|
|
118
|
+
cell = col.cellBuilder!(item);
|
|
119
|
+
} else if (col.valueBuilder != null) {
|
|
120
|
+
cell = Text(
|
|
121
|
+
col.valueBuilder!(item) ?? '',
|
|
122
|
+
style: const TextStyle(color: CronixColors.text),
|
|
123
|
+
textAlign: col.alignment,
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
cell = const SizedBox.shrink();
|
|
127
|
+
}
|
|
128
|
+
return DataCell(cell);
|
|
129
|
+
}).toList(),
|
|
130
|
+
);
|
|
131
|
+
}).toList(),
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnTabs extends StatefulWidget {
|
|
6
|
+
final List<String> tabs;
|
|
7
|
+
final int initialIndex;
|
|
8
|
+
final ValueChanged<int>? onChanged;
|
|
9
|
+
final bool fullWidth;
|
|
10
|
+
final Color? indicatorColor;
|
|
11
|
+
final Color? backgroundColor;
|
|
12
|
+
|
|
13
|
+
const CnTabs({
|
|
14
|
+
super.key,
|
|
15
|
+
required this.tabs,
|
|
16
|
+
this.initialIndex = 0,
|
|
17
|
+
this.onChanged,
|
|
18
|
+
this.fullWidth = false,
|
|
19
|
+
this.indicatorColor,
|
|
20
|
+
this.backgroundColor,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
@override
|
|
24
|
+
State<CnTabs> createState() => _CnTabsState();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class _CnTabsState extends State<CnTabs> with SingleTickerProviderStateMixin {
|
|
28
|
+
late TabController _controller;
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
void initState() {
|
|
32
|
+
super.initState();
|
|
33
|
+
_controller = TabController(
|
|
34
|
+
length: widget.tabs.length,
|
|
35
|
+
vsync: this,
|
|
36
|
+
initialIndex: widget.initialIndex,
|
|
37
|
+
);
|
|
38
|
+
_controller.addListener(_handleTabChange);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
void _handleTabChange() {
|
|
42
|
+
if (!_controller.indexIsChanging) {
|
|
43
|
+
widget.onChanged?.call(_controller.index);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
void didUpdateWidget(CnTabs oldWidget) {
|
|
49
|
+
super.didUpdateWidget(oldWidget);
|
|
50
|
+
if (oldWidget.tabs.length != widget.tabs.length) {
|
|
51
|
+
_controller.removeListener(_handleTabChange);
|
|
52
|
+
_controller.dispose();
|
|
53
|
+
_controller = TabController(
|
|
54
|
+
length: widget.tabs.length,
|
|
55
|
+
vsync: this,
|
|
56
|
+
initialIndex: _controller.index.clamp(0, widget.tabs.length - 1),
|
|
57
|
+
);
|
|
58
|
+
_controller.addListener(_handleTabChange);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@override
|
|
63
|
+
void dispose() {
|
|
64
|
+
_controller.removeListener(_handleTabChange);
|
|
65
|
+
_controller.dispose();
|
|
66
|
+
super.dispose();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@override
|
|
70
|
+
Widget build(BuildContext context) {
|
|
71
|
+
return Container(
|
|
72
|
+
color: widget.backgroundColor ?? Colors.transparent,
|
|
73
|
+
child: TabBar(
|
|
74
|
+
controller: _controller,
|
|
75
|
+
isScrollable: !widget.fullWidth,
|
|
76
|
+
indicatorSize: TabBarIndicatorSize.label,
|
|
77
|
+
indicator: UnderlineTabIndicator(
|
|
78
|
+
borderSide: BorderSide(
|
|
79
|
+
color: widget.indicatorColor ?? CronixColors.accent,
|
|
80
|
+
width: 2,
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
indicatorWeight: 2,
|
|
84
|
+
labelColor: CronixColors.text,
|
|
85
|
+
unselectedLabelColor: CronixColors.textSecondary,
|
|
86
|
+
labelStyle: const TextStyle(
|
|
87
|
+
fontSize: 14,
|
|
88
|
+
fontWeight: FontWeight.w500,
|
|
89
|
+
),
|
|
90
|
+
unselectedLabelStyle: const TextStyle(
|
|
91
|
+
fontSize: 14,
|
|
92
|
+
fontWeight: FontWeight.w400,
|
|
93
|
+
),
|
|
94
|
+
tabs: widget.tabs.map((tab) => Tab(text: tab)).toList(),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class CnTabView extends StatefulWidget {
|
|
101
|
+
final List<Widget> children;
|
|
102
|
+
final int initialIndex;
|
|
103
|
+
|
|
104
|
+
const CnTabView({
|
|
105
|
+
super.key,
|
|
106
|
+
required this.children,
|
|
107
|
+
this.initialIndex = 0,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
@override
|
|
111
|
+
State<CnTabView> createState() => _CnTabViewState();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class _CnTabViewState extends State<CnTabView> with SingleTickerProviderStateMixin {
|
|
115
|
+
late TabController _controller;
|
|
116
|
+
|
|
117
|
+
@override
|
|
118
|
+
void initState() {
|
|
119
|
+
super.initState();
|
|
120
|
+
_controller = TabController(
|
|
121
|
+
length: widget.children.length,
|
|
122
|
+
vsync: this,
|
|
123
|
+
initialIndex: widget.initialIndex,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@override
|
|
128
|
+
void dispose() {
|
|
129
|
+
_controller.dispose();
|
|
130
|
+
super.dispose();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@override
|
|
134
|
+
Widget build(BuildContext context) {
|
|
135
|
+
return TabBarView(
|
|
136
|
+
controller: _controller,
|
|
137
|
+
children: widget.children,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
class CnTabsWithContent extends StatefulWidget {
|
|
143
|
+
final List<TabData> tabs;
|
|
144
|
+
final int initialIndex;
|
|
145
|
+
final ValueChanged<int>? onChanged;
|
|
146
|
+
|
|
147
|
+
const CnTabsWithContent({
|
|
148
|
+
super.key,
|
|
149
|
+
required this.tabs,
|
|
150
|
+
this.initialIndex = 0,
|
|
151
|
+
this.onChanged,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
@override
|
|
155
|
+
State<CnTabsWithContent> createState() => _CnTabsWithContentState();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
class _CnTabsWithContentState extends State<CnTabsWithContent>
|
|
159
|
+
with SingleTickerProviderStateMixin {
|
|
160
|
+
late TabController _controller;
|
|
161
|
+
|
|
162
|
+
@override
|
|
163
|
+
void initState() {
|
|
164
|
+
super.initState();
|
|
165
|
+
_controller = TabController(
|
|
166
|
+
length: widget.tabs.length,
|
|
167
|
+
vsync: this,
|
|
168
|
+
initialIndex: widget.initialIndex,
|
|
169
|
+
);
|
|
170
|
+
_controller.addListener(() {
|
|
171
|
+
if (!_controller.indexIsChanging) {
|
|
172
|
+
widget.onChanged?.call(_controller.index);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@override
|
|
178
|
+
void dispose() {
|
|
179
|
+
_controller.dispose();
|
|
180
|
+
super.dispose();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@override
|
|
184
|
+
Widget build(BuildContext context) {
|
|
185
|
+
return Column(
|
|
186
|
+
children: [
|
|
187
|
+
Container(
|
|
188
|
+
decoration: const BoxDecoration(
|
|
189
|
+
border: Border(
|
|
190
|
+
bottom: BorderSide(color: CronixColors.border),
|
|
191
|
+
),
|
|
192
|
+
),
|
|
193
|
+
child: TabBar(
|
|
194
|
+
controller: _controller,
|
|
195
|
+
indicator: UnderlineTabIndicator(
|
|
196
|
+
borderSide: BorderSide(
|
|
197
|
+
color: CronixColors.accent,
|
|
198
|
+
width: 2,
|
|
199
|
+
),
|
|
200
|
+
),
|
|
201
|
+
labelColor: CronixColors.text,
|
|
202
|
+
unselectedLabelColor: CronixColors.textSecondary,
|
|
203
|
+
labelStyle: const TextStyle(
|
|
204
|
+
fontSize: 14,
|
|
205
|
+
fontWeight: FontWeight.w500,
|
|
206
|
+
),
|
|
207
|
+
tabs: widget.tabs.map((tab) => Tab(text: tab.label)).toList(),
|
|
208
|
+
),
|
|
209
|
+
),
|
|
210
|
+
Expanded(
|
|
211
|
+
child: TabBarView(
|
|
212
|
+
controller: _controller,
|
|
213
|
+
children: widget.tabs.map((tab) => tab.content).toList(),
|
|
214
|
+
),
|
|
215
|
+
),
|
|
216
|
+
],
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
class TabData {
|
|
222
|
+
final String label;
|
|
223
|
+
final Widget content;
|
|
224
|
+
|
|
225
|
+
const TabData({
|
|
226
|
+
required this.label,
|
|
227
|
+
required this.content,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnTag extends StatelessWidget {
|
|
6
|
+
final String label;
|
|
7
|
+
final IconData? icon;
|
|
8
|
+
final VoidCallback? onRemove;
|
|
9
|
+
final VoidCallback? onTap;
|
|
10
|
+
final Color? backgroundColor;
|
|
11
|
+
final Color? textColor;
|
|
12
|
+
final double fontSize;
|
|
13
|
+
final bool outlined;
|
|
14
|
+
|
|
15
|
+
const CnTag({
|
|
16
|
+
super.key,
|
|
17
|
+
required this.label,
|
|
18
|
+
this.icon,
|
|
19
|
+
this.onRemove,
|
|
20
|
+
this.onTap,
|
|
21
|
+
this.backgroundColor,
|
|
22
|
+
this.textColor,
|
|
23
|
+
this.fontSize = 12,
|
|
24
|
+
this.outlined = false,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
Widget build(BuildContext context) {
|
|
29
|
+
Widget content = Container(
|
|
30
|
+
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
31
|
+
decoration: BoxDecoration(
|
|
32
|
+
color: outlined ? null : (backgroundColor ?? CronixColors.surfaceLight),
|
|
33
|
+
borderRadius: CronixRadius.radiusFull,
|
|
34
|
+
border: outlined
|
|
35
|
+
? Border.all(color: backgroundColor ?? CronixColors.border)
|
|
36
|
+
: null,
|
|
37
|
+
),
|
|
38
|
+
child: Row(
|
|
39
|
+
mainAxisSize: MainAxisSize.min,
|
|
40
|
+
children: [
|
|
41
|
+
if (icon != null) ...[
|
|
42
|
+
Icon(icon, size: fontSize, color: textColor ?? CronixColors.text),
|
|
43
|
+
const SizedBox(width: 4),
|
|
44
|
+
],
|
|
45
|
+
Text(
|
|
46
|
+
label,
|
|
47
|
+
style: TextStyle(
|
|
48
|
+
color: textColor ?? CronixColors.text,
|
|
49
|
+
fontSize: fontSize,
|
|
50
|
+
fontWeight: FontWeight.w500,
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
if (onRemove != null) ...[
|
|
54
|
+
const SizedBox(width: 6),
|
|
55
|
+
GestureDetector(
|
|
56
|
+
onTap: onRemove,
|
|
57
|
+
child: Icon(
|
|
58
|
+
Icons.close,
|
|
59
|
+
size: fontSize + 2,
|
|
60
|
+
color: textColor ?? CronixColors.textSecondary,
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
],
|
|
64
|
+
],
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (onTap != null) {
|
|
69
|
+
return InkWell(
|
|
70
|
+
onTap: onTap,
|
|
71
|
+
borderRadius: CronixRadius.radiusFull,
|
|
72
|
+
child: content,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return content;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class CnTagGroup extends StatelessWidget {
|
|
81
|
+
final List<CnTag> tags;
|
|
82
|
+
final WrapAlignment alignment;
|
|
83
|
+
final double spacing;
|
|
84
|
+
final double runSpacing;
|
|
85
|
+
|
|
86
|
+
const CnTagGroup({
|
|
87
|
+
super.key,
|
|
88
|
+
required this.tags,
|
|
89
|
+
this.alignment = WrapAlignment.start,
|
|
90
|
+
this.spacing = 8,
|
|
91
|
+
this.runSpacing = 8,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
@override
|
|
95
|
+
Widget build(BuildContext context) {
|
|
96
|
+
return Wrap(
|
|
97
|
+
alignment: alignment,
|
|
98
|
+
spacing: spacing,
|
|
99
|
+
runSpacing: runSpacing,
|
|
100
|
+
children: tags,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
class CnTagInput extends StatefulWidget {
|
|
106
|
+
final List<String> initialTags;
|
|
107
|
+
final ValueChanged<List<String>>? onChanged;
|
|
108
|
+
final String? placeholder;
|
|
109
|
+
final int? maxTags;
|
|
110
|
+
|
|
111
|
+
const CnTagInput({
|
|
112
|
+
super.key,
|
|
113
|
+
this.initialTags = const [],
|
|
114
|
+
this.onChanged,
|
|
115
|
+
this.placeholder,
|
|
116
|
+
this.maxTags,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
@override
|
|
120
|
+
State<CnTagInput> createState() => _CnTagInputState();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
class _CnTagInputState extends State<CnTagInput> {
|
|
124
|
+
late List<String> _tags;
|
|
125
|
+
final TextEditingController _controller = TextEditingController();
|
|
126
|
+
|
|
127
|
+
@override
|
|
128
|
+
void initState() {
|
|
129
|
+
super.initState();
|
|
130
|
+
_tags = List.from(widget.initialTags);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
void _addTag(String tag) {
|
|
134
|
+
if (tag.isEmpty || _tags.contains(tag)) return;
|
|
135
|
+
if (widget.maxTags != null && _tags.length >= widget.maxTags!) return;
|
|
136
|
+
|
|
137
|
+
setState(() => _tags.add(tag));
|
|
138
|
+
_controller.clear();
|
|
139
|
+
widget.onChanged?.call(_tags);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
void _removeTag(String tag) {
|
|
143
|
+
setState(() => _tags.remove(tag));
|
|
144
|
+
widget.onChanged?.call(_tags);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@override
|
|
148
|
+
Widget build(BuildContext context) {
|
|
149
|
+
return Container(
|
|
150
|
+
padding: const EdgeInsets.all(8),
|
|
151
|
+
decoration: BoxDecoration(
|
|
152
|
+
color: CronixColors.surface,
|
|
153
|
+
borderRadius: CronixRadius.radiusMD,
|
|
154
|
+
border: Border.all(color: CronixColors.border),
|
|
155
|
+
),
|
|
156
|
+
child: Wrap(
|
|
157
|
+
spacing: 8,
|
|
158
|
+
runSpacing: 8,
|
|
159
|
+
children: [
|
|
160
|
+
..._tags.map((tag) => CnTag(
|
|
161
|
+
label: tag,
|
|
162
|
+
onRemove: () => _removeTag(tag),
|
|
163
|
+
)),
|
|
164
|
+
if (widget.maxTags == null || _tags.length < widget.maxTags!)
|
|
165
|
+
SizedBox(
|
|
166
|
+
width: 120,
|
|
167
|
+
height: 28,
|
|
168
|
+
child: TextField(
|
|
169
|
+
controller: _controller,
|
|
170
|
+
onSubmitted: _addTag,
|
|
171
|
+
style: const TextStyle(color: CronixColors.text, fontSize: 12),
|
|
172
|
+
decoration: InputDecoration(
|
|
173
|
+
hintText: widget.placeholder ?? 'Add tag...',
|
|
174
|
+
hintStyle: const TextStyle(color: CronixColors.textMuted),
|
|
175
|
+
border: InputBorder.none,
|
|
176
|
+
contentPadding: EdgeInsets.zero,
|
|
177
|
+
isDense: true,
|
|
178
|
+
),
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
],
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../tokens/colors.dart';
|
|
3
|
+
import '../tokens/spacing.dart';
|
|
4
|
+
|
|
5
|
+
class CnTextarea extends StatefulWidget {
|
|
6
|
+
final String? label;
|
|
7
|
+
final String? placeholder;
|
|
8
|
+
final String? initialValue;
|
|
9
|
+
final TextEditingController? controller;
|
|
10
|
+
final bool enabled;
|
|
11
|
+
final int minLines;
|
|
12
|
+
final int maxLines;
|
|
13
|
+
final int? maxLength;
|
|
14
|
+
final ValueChanged<String>? onChanged;
|
|
15
|
+
final String? Function(String?)? validator;
|
|
16
|
+
|
|
17
|
+
const CnTextarea({
|
|
18
|
+
super.key,
|
|
19
|
+
this.label,
|
|
20
|
+
this.placeholder,
|
|
21
|
+
this.initialValue,
|
|
22
|
+
this.controller,
|
|
23
|
+
this.enabled = true,
|
|
24
|
+
this.minLines = 3,
|
|
25
|
+
this.maxLines = 5,
|
|
26
|
+
this.maxLength,
|
|
27
|
+
this.onChanged,
|
|
28
|
+
this.validator,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
State<CnTextarea> createState() => _CnTextareaState();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class _CnTextareaState extends State<CnTextarea> {
|
|
36
|
+
late TextEditingController _controller;
|
|
37
|
+
String? _errorText;
|
|
38
|
+
int _currentLength = 0;
|
|
39
|
+
|
|
40
|
+
@override
|
|
41
|
+
void initState() {
|
|
42
|
+
super.initState();
|
|
43
|
+
_controller = widget.controller ??
|
|
44
|
+
TextEditingController(text: widget.initialValue);
|
|
45
|
+
_currentLength = _controller.text.length;
|
|
46
|
+
_controller.addListener(_updateLength);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
void _updateLength() {
|
|
50
|
+
setState(() => _currentLength = _controller.text.length);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@override
|
|
54
|
+
void dispose() {
|
|
55
|
+
_controller.removeListener(_updateLength);
|
|
56
|
+
if (widget.controller == null) {
|
|
57
|
+
_controller.dispose();
|
|
58
|
+
}
|
|
59
|
+
super.dispose();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@override
|
|
63
|
+
Widget build(BuildContext context) {
|
|
64
|
+
return Column(
|
|
65
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
66
|
+
mainAxisSize: MainAxisSize.min,
|
|
67
|
+
children: [
|
|
68
|
+
if (widget.label != null) ...[
|
|
69
|
+
Text(
|
|
70
|
+
widget.label!,
|
|
71
|
+
style: const TextStyle(
|
|
72
|
+
color: CronixColors.text,
|
|
73
|
+
fontSize: 14,
|
|
74
|
+
fontWeight: FontWeight.w500,
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
const SizedBox(height: 8),
|
|
78
|
+
],
|
|
79
|
+
TextFormField(
|
|
80
|
+
controller: _controller,
|
|
81
|
+
enabled: widget.enabled,
|
|
82
|
+
minLines: widget.minLines,
|
|
83
|
+
maxLines: widget.maxLines,
|
|
84
|
+
maxLength: widget.maxLength,
|
|
85
|
+
onChanged: widget.onChanged,
|
|
86
|
+
validator: (value) {
|
|
87
|
+
if (widget.validator != null) {
|
|
88
|
+
final error = widget.validator!(value);
|
|
89
|
+
setState(() => _errorText = error);
|
|
90
|
+
return error;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
},
|
|
94
|
+
style: const TextStyle(color: CronixColors.text),
|
|
95
|
+
decoration: InputDecoration(
|
|
96
|
+
hintText: widget.placeholder,
|
|
97
|
+
hintStyle: const TextStyle(color: CronixColors.textMuted),
|
|
98
|
+
filled: true,
|
|
99
|
+
fillColor: widget.enabled ? CronixColors.surface : CronixColors.surfaceLight,
|
|
100
|
+
contentPadding: const EdgeInsets.all(12),
|
|
101
|
+
border: OutlineInputBorder(
|
|
102
|
+
borderRadius: CronixRadius.radiusMD,
|
|
103
|
+
borderSide: const BorderSide(color: CronixColors.border),
|
|
104
|
+
),
|
|
105
|
+
enabledBorder: OutlineInputBorder(
|
|
106
|
+
borderRadius: CronixRadius.radiusMD,
|
|
107
|
+
borderSide: const BorderSide(color: CronixColors.border),
|
|
108
|
+
),
|
|
109
|
+
focusedBorder: OutlineInputBorder(
|
|
110
|
+
borderRadius: CronixRadius.radiusMD,
|
|
111
|
+
borderSide: const BorderSide(color: CronixColors.accent, width: 2),
|
|
112
|
+
),
|
|
113
|
+
errorBorder: OutlineInputBorder(
|
|
114
|
+
borderRadius: CronixRadius.radiusMD,
|
|
115
|
+
borderSide: const BorderSide(color: CronixColors.error),
|
|
116
|
+
),
|
|
117
|
+
counterText: '',
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
if (widget.maxLength != null)
|
|
121
|
+
Align(
|
|
122
|
+
alignment: Alignment.centerRight,
|
|
123
|
+
child: Text(
|
|
124
|
+
'$_currentLength/${widget.maxLength}',
|
|
125
|
+
style: TextStyle(
|
|
126
|
+
color: _currentLength >= widget.maxLength!
|
|
127
|
+
? CronixColors.error
|
|
128
|
+
: CronixColors.textMuted,
|
|
129
|
+
fontSize: 12,
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
if (_errorText != null) ...[
|
|
134
|
+
const SizedBox(height: 4),
|
|
135
|
+
Text(
|
|
136
|
+
_errorText!,
|
|
137
|
+
style: const TextStyle(color: CronixColors.error, fontSize: 12),
|
|
138
|
+
),
|
|
139
|
+
],
|
|
140
|
+
],
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|