cronixui 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +16 -27
  2. package/package.json +2 -1
  3. package/packages/flutter/lib/cronixui.dart +41 -0
  4. package/packages/flutter/lib/src/tokens/colors.dart +34 -0
  5. package/packages/flutter/lib/src/tokens/spacing.dart +54 -0
  6. package/packages/flutter/lib/src/tokens/theme.dart +174 -0
  7. package/packages/flutter/lib/src/widgets/cn_accordion.dart +254 -0
  8. package/packages/flutter/lib/src/widgets/cn_alert.dart +137 -0
  9. package/packages/flutter/lib/src/widgets/cn_avatar.dart +98 -0
  10. package/packages/flutter/lib/src/widgets/cn_badge.dart +80 -0
  11. package/packages/flutter/lib/src/widgets/cn_breadcrumb.dart +88 -0
  12. package/packages/flutter/lib/src/widgets/cn_button.dart +137 -0
  13. package/packages/flutter/lib/src/widgets/cn_card.dart +99 -0
  14. package/packages/flutter/lib/src/widgets/cn_checkbox.dart +77 -0
  15. package/packages/flutter/lib/src/widgets/cn_command_palette.dart +299 -0
  16. package/packages/flutter/lib/src/widgets/cn_container.dart +131 -0
  17. package/packages/flutter/lib/src/widgets/cn_dropdown.dart +149 -0
  18. package/packages/flutter/lib/src/widgets/cn_file_input.dart +113 -0
  19. package/packages/flutter/lib/src/widgets/cn_footer.dart +108 -0
  20. package/packages/flutter/lib/src/widgets/cn_header.dart +173 -0
  21. package/packages/flutter/lib/src/widgets/cn_input.dart +142 -0
  22. package/packages/flutter/lib/src/widgets/cn_list.dart +150 -0
  23. package/packages/flutter/lib/src/widgets/cn_modal.dart +213 -0
  24. package/packages/flutter/lib/src/widgets/cn_nav.dart +157 -0
  25. package/packages/flutter/lib/src/widgets/cn_pagination.dart +193 -0
  26. package/packages/flutter/lib/src/widgets/cn_progress.dart +146 -0
  27. package/packages/flutter/lib/src/widgets/cn_radio.dart +133 -0
  28. package/packages/flutter/lib/src/widgets/cn_search.dart +183 -0
  29. package/packages/flutter/lib/src/widgets/cn_select.dart +244 -0
  30. package/packages/flutter/lib/src/widgets/cn_sidebar.dart +207 -0
  31. package/packages/flutter/lib/src/widgets/cn_skeleton.dart +136 -0
  32. package/packages/flutter/lib/src/widgets/cn_slider.dart +141 -0
  33. package/packages/flutter/lib/src/widgets/cn_spinner.dart +85 -0
  34. package/packages/flutter/lib/src/widgets/cn_stat.dart +135 -0
  35. package/packages/flutter/lib/src/widgets/cn_table.dart +136 -0
  36. package/packages/flutter/lib/src/widgets/cn_tabs.dart +229 -0
  37. package/packages/flutter/lib/src/widgets/cn_tag.dart +185 -0
  38. package/packages/flutter/lib/src/widgets/cn_textarea.dart +143 -0
  39. package/packages/flutter/lib/src/widgets/cn_toast.dart +121 -0
  40. package/packages/flutter/lib/src/widgets/cn_toggle.dart +78 -0
  41. package/packages/flutter/lib/src/widgets/cn_tooltip.dart +118 -0
  42. package/packages/flutter/pubspec.yaml +20 -0
@@ -0,0 +1,299 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../tokens/colors.dart';
3
+ import '../tokens/spacing.dart';
4
+
5
+ class CnCommandItem {
6
+ final String label;
7
+ final String? shortcut;
8
+ final IconData? icon;
9
+ final VoidCallback? onTap;
10
+ final String? category;
11
+
12
+ const CnCommandItem({
13
+ required this.label,
14
+ this.shortcut,
15
+ this.icon,
16
+ this.onTap,
17
+ this.category,
18
+ });
19
+ }
20
+
21
+ class CnCommandPalette extends StatefulWidget {
22
+ final List<CnCommandItem> commands;
23
+ final String? placeholder;
24
+ final VoidCallback? onClose;
25
+
26
+ const CnCommandPalette({
27
+ super.key,
28
+ required this.commands,
29
+ this.placeholder,
30
+ this.onClose,
31
+ });
32
+
33
+ static Future<void> show({
34
+ required BuildContext context,
35
+ required List<CnCommandItem> commands,
36
+ String? placeholder,
37
+ }) {
38
+ return showDialog(
39
+ context: context,
40
+ barrierColor: Colors.black87,
41
+ builder: (context) => CnCommandPalette(
42
+ commands: commands,
43
+ placeholder: placeholder,
44
+ ),
45
+ );
46
+ }
47
+
48
+ @override
49
+ State<CnCommandPalette> createState() => _CnCommandPaletteState();
50
+ }
51
+
52
+ class _CnCommandPaletteState extends State<CnCommandPalette> {
53
+ final TextEditingController _searchController = TextEditingController();
54
+ final FocusNode _focusNode = FocusNode();
55
+ String _query = '';
56
+ int _selectedIndex = 0;
57
+
58
+ List<CnCommandItem> get _filteredCommands {
59
+ if (_query.isEmpty) return widget.commands;
60
+ return widget.commands
61
+ .where((cmd) => cmd.label.toLowerCase().contains(_query.toLowerCase()))
62
+ .toList();
63
+ }
64
+
65
+ Map<String, List<CnCommandItem>> get _groupedCommands {
66
+ final groups = <String, List<CnCommandItem>>{};
67
+ for (final cmd in _filteredCommands) {
68
+ final category = cmd.category ?? 'General';
69
+ groups.putIfAbsent(category, () => []).add(cmd);
70
+ }
71
+ return groups;
72
+ }
73
+
74
+ @override
75
+ void initState() {
76
+ super.initState();
77
+ WidgetsBinding.instance.addPostFrameCallback((_) {
78
+ _focusNode.requestFocus();
79
+ });
80
+ }
81
+
82
+ @override
83
+ void dispose() {
84
+ _searchController.dispose();
85
+ _focusNode.dispose();
86
+ super.dispose();
87
+ }
88
+
89
+ void _handleKeyEvent(KeyEvent event) {
90
+ if (event is KeyDownEvent || event is RawKeyDownEvent) {
91
+ final rawEvent = event as RawKeyEvent;
92
+ if (rawEvent.logicalKey == LogicalKeyboardKey.arrowDown) {
93
+ setState(() {
94
+ _selectedIndex = (_selectedIndex + 1).clamp(0, _filteredCommands.length - 1);
95
+ });
96
+ } else if (rawEvent.logicalKey == LogicalKeyboardKey.arrowUp) {
97
+ setState(() {
98
+ _selectedIndex = (_selectedIndex - 1).clamp(0, _filteredCommands.length - 1);
99
+ });
100
+ } else if (rawEvent.logicalKey == LogicalKeyboardKey.enter) {
101
+ if (_filteredCommands.isNotEmpty && _selectedIndex < _filteredCommands.length) {
102
+ _filteredCommands[_selectedIndex].onTap?.call();
103
+ Navigator.of(context).pop();
104
+ }
105
+ } else if (rawEvent.logicalKey == LogicalKeyboardKey.escape) {
106
+ Navigator.of(context).pop();
107
+ }
108
+ }
109
+ }
110
+
111
+ @override
112
+ Widget build(BuildContext context) {
113
+ return RawKeyboardListener(
114
+ focusNode: FocusNode(),
115
+ onKey: _handleKeyEvent,
116
+ child: Dialog(
117
+ backgroundColor: Colors.transparent,
118
+ child: Container(
119
+ width: 560,
120
+ constraints: const BoxConstraints(maxHeight: 400),
121
+ decoration: BoxDecoration(
122
+ color: CronixColors.surface,
123
+ borderRadius: CronixRadius.radiusLG,
124
+ border: Border.all(color: CronixColors.border),
125
+ ),
126
+ child: Column(
127
+ mainAxisSize: MainAxisSize.min,
128
+ children: [
129
+ Padding(
130
+ padding: const EdgeInsets.all(16),
131
+ child: TextField(
132
+ controller: _searchController,
133
+ focusNode: _focusNode,
134
+ onChanged: (value) => setState(() => _query = value),
135
+ style: const TextStyle(color: CronixColors.text, fontSize: 16),
136
+ decoration: InputDecoration(
137
+ hintText: widget.placeholder ?? 'Type a command or search...',
138
+ hintStyle: const TextStyle(color: CronixColors.textMuted),
139
+ filled: true,
140
+ fillColor: CronixColors.background,
141
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
142
+ border: OutlineInputBorder(
143
+ borderRadius: CronixRadius.radiusMD,
144
+ borderSide: BorderSide.none,
145
+ ),
146
+ prefixIcon: const Icon(Icons.search, color: CronixColors.textSecondary),
147
+ ),
148
+ ),
149
+ ),
150
+ Flexible(
151
+ child: ListView.builder(
152
+ shrinkWrap: true,
153
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
154
+ itemCount: _groupedCommands.length,
155
+ itemBuilder: (context, groupIndex) {
156
+ final entry = _groupedCommands.entries.elementAt(groupIndex);
157
+ return Column(
158
+ crossAxisAlignment: CrossAxisAlignment.start,
159
+ children: [
160
+ Padding(
161
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
162
+ child: Text(
163
+ entry.key,
164
+ style: const TextStyle(
165
+ color: CronixColors.textMuted,
166
+ fontSize: 11,
167
+ fontWeight: FontWeight.w600,
168
+ letterSpacing: 0.5,
169
+ ),
170
+ ),
171
+ ),
172
+ ...entry.value.asMap().entries.map((itemEntry) {
173
+ final index = _filteredCommands.indexOf(itemEntry.value);
174
+ final isSelected = index == _selectedIndex;
175
+ return _CommandItemWidget(
176
+ item: itemEntry.value,
177
+ isSelected: isSelected,
178
+ onTap: () {
179
+ itemEntry.value.onTap?.call();
180
+ Navigator.of(context).pop();
181
+ },
182
+ );
183
+ }),
184
+ ],
185
+ );
186
+ },
187
+ ),
188
+ ),
189
+ Container(
190
+ padding: const EdgeInsets.all(12),
191
+ decoration: const BoxDecoration(
192
+ border: Border(
193
+ top: BorderSide(color: CronixColors.border),
194
+ ),
195
+ ),
196
+ child: Row(
197
+ children: [
198
+ _KeyboardHint(icon: Icons.arrow_upward, label: 'Up'),
199
+ _KeyboardHint(icon: Icons.arrow_downward, label: 'Down'),
200
+ _KeyboardHint(icon: Icons.keyboard_return, label: 'Select'),
201
+ _KeyboardHint(icon: Icons.close, label: 'Close'),
202
+ ],
203
+ ),
204
+ ),
205
+ ],
206
+ ),
207
+ ),
208
+ ),
209
+ );
210
+ }
211
+ }
212
+
213
+ class _CommandItemWidget extends StatelessWidget {
214
+ final CnCommandItem item;
215
+ final bool isSelected;
216
+ final VoidCallback onTap;
217
+
218
+ const _CommandItemWidget({
219
+ required this.item,
220
+ required this.isSelected,
221
+ required this.onTap,
222
+ });
223
+
224
+ @override
225
+ Widget build(BuildContext context) {
226
+ return InkWell(
227
+ onTap: onTap,
228
+ child: Container(
229
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
230
+ decoration: BoxDecoration(
231
+ color: isSelected ? CronixColors.accent.withOpacity(0.2) : null,
232
+ borderRadius: CronixRadius.radiusSM,
233
+ ),
234
+ child: Row(
235
+ children: [
236
+ if (item.icon != null) ...[
237
+ Icon(item.icon, size: 18, color: CronixColors.textSecondary),
238
+ const SizedBox(width: 12),
239
+ ],
240
+ Expanded(
241
+ child: Text(
242
+ item.label,
243
+ style: TextStyle(
244
+ color: CronixColors.text,
245
+ fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400,
246
+ ),
247
+ ),
248
+ ),
249
+ if (item.shortcut != null)
250
+ Container(
251
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
252
+ decoration: BoxDecoration(
253
+ color: CronixColors.border,
254
+ borderRadius: CronixRadius.radiusSM,
255
+ ),
256
+ child: Text(
257
+ item.shortcut!,
258
+ style: const TextStyle(
259
+ color: CronixColors.textSecondary,
260
+ fontSize: 11,
261
+ ),
262
+ ),
263
+ ),
264
+ ],
265
+ ),
266
+ ),
267
+ );
268
+ }
269
+ }
270
+
271
+ class _KeyboardHint extends StatelessWidget {
272
+ final IconData icon;
273
+ final String label;
274
+
275
+ const _KeyboardHint({
276
+ required this.icon,
277
+ required this.label,
278
+ });
279
+
280
+ @override
281
+ Widget build(BuildContext context) {
282
+ return Padding(
283
+ padding: const EdgeInsets.only(right: 16),
284
+ child: Row(
285
+ children: [
286
+ Icon(icon, size: 14, color: CronixColors.textMuted),
287
+ const SizedBox(width: 4),
288
+ Text(
289
+ label,
290
+ style: const TextStyle(
291
+ color: CronixColors.textMuted,
292
+ fontSize: 12,
293
+ ),
294
+ ),
295
+ ],
296
+ ),
297
+ );
298
+ }
299
+ }
@@ -0,0 +1,131 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../tokens/colors.dart';
3
+ import '../tokens/spacing.dart';
4
+
5
+ class CnContainer extends StatelessWidget {
6
+ final Widget? child;
7
+ final double? maxWidth;
8
+ final double? minWidth;
9
+ final EdgeInsetsGeometry? padding;
10
+ final EdgeInsetsGeometry? margin;
11
+ final Color? backgroundColor;
12
+ final bool centered;
13
+ final double? width;
14
+ final double? height;
15
+
16
+ const CnContainer({
17
+ super.key,
18
+ this.child,
19
+ this.maxWidth,
20
+ this.minWidth,
21
+ this.padding,
22
+ this.margin,
23
+ this.backgroundColor,
24
+ this.centered = false,
25
+ this.width,
26
+ this.height,
27
+ });
28
+
29
+ @override
30
+ Widget build(BuildContext context) {
31
+ Widget content = Container(
32
+ width: width,
33
+ height: height,
34
+ constraints: BoxConstraints(
35
+ maxWidth: maxWidth ?? 1200,
36
+ minWidth: minWidth ?? 0,
37
+ ),
38
+ padding: padding,
39
+ decoration: backgroundColor != null
40
+ ? BoxDecoration(color: backgroundColor)
41
+ : null,
42
+ child: child,
43
+ );
44
+
45
+ if (centered) {
46
+ content = Center(child: content);
47
+ }
48
+
49
+ if (margin != null) {
50
+ content = Padding(padding: margin!, child: content);
51
+ }
52
+
53
+ return content;
54
+ }
55
+ }
56
+
57
+ class CnResponsiveContainer extends StatelessWidget {
58
+ final Widget child;
59
+ final double xsMaxWidth;
60
+ final double smMaxWidth;
61
+ final double mdMaxWidth;
62
+ final double lgMaxWidth;
63
+ final double xlMaxWidth;
64
+ final EdgeInsetsGeometry? padding;
65
+
66
+ const CnResponsiveContainer({
67
+ super.key,
68
+ required this.child,
69
+ this.xsMaxWidth = 640,
70
+ this.smMaxWidth = 768,
71
+ this.mdMaxWidth = 1024,
72
+ this.lgMaxWidth = 1280,
73
+ this.xlMaxWidth = 1536,
74
+ this.padding,
75
+ });
76
+
77
+ @override
78
+ Widget build(BuildContext context) {
79
+ return LayoutBuilder(
80
+ builder: (context, constraints) {
81
+ double maxWidth;
82
+ if (constraints.maxWidth < 640) {
83
+ maxWidth = xsMaxWidth;
84
+ } else if (constraints.maxWidth < 768) {
85
+ maxWidth = smMaxWidth;
86
+ } else if (constraints.maxWidth < 1024) {
87
+ maxWidth = mdMaxWidth;
88
+ } else if (constraints.maxWidth < 1280) {
89
+ maxWidth = lgMaxWidth;
90
+ } else {
91
+ maxWidth = xlMaxWidth;
92
+ }
93
+
94
+ return Container(
95
+ width: double.infinity,
96
+ constraints: BoxConstraints(maxWidth: maxWidth),
97
+ padding: padding ?? EdgeInsets.symmetric(
98
+ horizontal: constraints.maxWidth < 640 ? 16 : 24,
99
+ ),
100
+ child: child,
101
+ );
102
+ },
103
+ );
104
+ }
105
+ }
106
+
107
+ class CnSection extends StatelessWidget {
108
+ final Widget? child;
109
+ final String? title;
110
+ final String? subtitle;
111
+ final EdgeInsetsGeometry? padding;
112
+ final double minHeight;
113
+
114
+ const CnSection({
115
+ super.key,
116
+ this.child,
117
+ this.title,
118
+ this.subtitle,
119
+ this.padding,
120
+ this.minHeight = 0,
121
+ });
122
+
123
+ @override
124
+ Widget build(BuildContext context) {
125
+ return Container(
126
+ constraints: BoxConstraints(minHeight: minHeight),
127
+ padding: padding ?? const EdgeInsets.symmetric(vertical: 48),
128
+ child: child,
129
+ );
130
+ }
131
+ }
@@ -0,0 +1,149 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../tokens/colors.dart';
3
+ import '../tokens/spacing.dart';
4
+
5
+ class CnDropdownItem {
6
+ final String label;
7
+ final IconData? icon;
8
+ final VoidCallback? onTap;
9
+ final bool enabled;
10
+ final bool danger;
11
+
12
+ const CnDropdownItem({
13
+ required this.label,
14
+ this.icon,
15
+ this.onTap,
16
+ this.enabled = true,
17
+ this.danger = false,
18
+ });
19
+ }
20
+
21
+ class CnDropdown extends StatefulWidget {
22
+ final Widget trigger;
23
+ final List<CnDropdownItem> items;
24
+ final double? width;
25
+ final Offset offset;
26
+ final bool closeOnSelect;
27
+
28
+ const CnDropdown({
29
+ super.key,
30
+ required this.trigger,
31
+ required this.items,
32
+ this.width,
33
+ this.offset = Offset.zero,
34
+ this.closeOnSelect = true,
35
+ });
36
+
37
+ @override
38
+ State<CnDropdown> createState() => _CnDropdownState();
39
+ }
40
+
41
+ class _CnDropdownState extends State<CnDropdown> {
42
+ final LayerLink _layerLink = LayerLink();
43
+ OverlayEntry? _overlayEntry;
44
+ bool _isOpen = false;
45
+
46
+ void _toggleDropdown() {
47
+ if (_isOpen) {
48
+ _closeDropdown();
49
+ } else {
50
+ _openDropdown();
51
+ }
52
+ }
53
+
54
+ void _openDropdown() {
55
+ _overlayEntry = _createOverlayEntry();
56
+ Overlay.of(context).insert(_overlayEntry!);
57
+ setState(() => _isOpen = true);
58
+ }
59
+
60
+ void _closeDropdown() {
61
+ _overlayEntry?.remove();
62
+ _overlayEntry = null;
63
+ setState(() => _isOpen = false);
64
+ }
65
+
66
+ void _onItemTap(CnDropdownItem item) {
67
+ if (!item.enabled) return;
68
+ item.onTap?.call();
69
+ if (widget.closeOnSelect) {
70
+ _closeDropdown();
71
+ }
72
+ }
73
+
74
+ OverlayEntry _createOverlayEntry() {
75
+ return OverlayEntry(
76
+ builder: (context) => Positioned(
77
+ width: widget.width,
78
+ child: CompositedTransformFollower(
79
+ link: _layerLink,
80
+ showWhenUnlinked: false,
81
+ offset: Offset(0, 40) + widget.offset,
82
+ child: Material(
83
+ color: Colors.transparent,
84
+ child: Container(
85
+ width: widget.width ?? 200,
86
+ constraints: const BoxConstraints(maxHeight: 300),
87
+ decoration: BoxDecoration(
88
+ color: CronixColors.surface,
89
+ borderRadius: CronixRadius.radiusMD,
90
+ border: Border.all(color: CronixColors.border),
91
+ ),
92
+ child: ClipRRect(
93
+ borderRadius: CronixRadius.radiusMD,
94
+ child: ListView.builder(
95
+ shrinkWrap: true,
96
+ padding: const EdgeInsets.symmetric(vertical: 4),
97
+ itemCount: widget.items.length,
98
+ itemBuilder: (context, index) {
99
+ final item = widget.items[index];
100
+ return InkWell(
101
+ onTap: item.enabled ? () => _onItemTap(item) : null,
102
+ child: Container(
103
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
104
+ child: Row(
105
+ children: [
106
+ if (item.icon != null) ...[
107
+ Icon(
108
+ item.icon,
109
+ size: 18,
110
+ color: item.danger
111
+ ? CronixColors.error
112
+ : CronixColors.textSecondary,
113
+ ),
114
+ const SizedBox(width: 8),
115
+ ],
116
+ Text(
117
+ item.label,
118
+ style: TextStyle(
119
+ color: item.enabled
120
+ ? (item.danger ? CronixColors.error : CronixColors.text)
121
+ : CronixColors.textMuted,
122
+ fontSize: 14,
123
+ ),
124
+ ),
125
+ ],
126
+ ),
127
+ ),
128
+ );
129
+ },
130
+ ),
131
+ ),
132
+ ),
133
+ ),
134
+ ),
135
+ ),
136
+ );
137
+ }
138
+
139
+ @override
140
+ Widget build(BuildContext context) {
141
+ return CompositedTransformTarget(
142
+ link: _layerLink,
143
+ child: GestureDetector(
144
+ onTap: _toggleDropdown,
145
+ child: widget.trigger,
146
+ ),
147
+ );
148
+ }
149
+ }
@@ -0,0 +1,113 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../tokens/colors.dart';
3
+ import '../tokens/spacing.dart';
4
+
5
+ class CnFileInput extends StatelessWidget {
6
+ final String? label;
7
+ final String buttonText;
8
+ final IconData? icon;
9
+ final VoidCallback? onPick;
10
+ final String? fileName;
11
+ final bool enabled;
12
+ final bool loading;
13
+ final List<String>? allowedExtensions;
14
+
15
+ const CnFileInput({
16
+ super.key,
17
+ this.label,
18
+ this.buttonText = 'Choose File',
19
+ this.icon,
20
+ this.onPick,
21
+ this.fileName,
22
+ this.enabled = true,
23
+ this.loading = false,
24
+ this.allowedExtensions,
25
+ });
26
+
27
+ @override
28
+ Widget build(BuildContext context) {
29
+ return Column(
30
+ crossAxisAlignment: CrossAxisAlignment.start,
31
+ mainAxisSize: MainAxisSize.min,
32
+ children: [
33
+ if (label != null) ...[
34
+ Text(
35
+ label!,
36
+ style: const TextStyle(
37
+ color: CronixColors.text,
38
+ fontSize: 14,
39
+ fontWeight: FontWeight.w500,
40
+ ),
41
+ ),
42
+ const SizedBox(height: 8),
43
+ ],
44
+ Container(
45
+ padding: const EdgeInsets.all(16),
46
+ decoration: BoxDecoration(
47
+ color: CronixColors.surface,
48
+ borderRadius: CronixRadius.radiusMD,
49
+ border: Border.all(color: CronixColors.border),
50
+ ),
51
+ child: Column(
52
+ children: [
53
+ Icon(
54
+ icon ?? Icons.cloud_upload_outlined,
55
+ size: 40,
56
+ color: CronixColors.textSecondary,
57
+ ),
58
+ const SizedBox(height: 12),
59
+ if (loading)
60
+ const CircularProgressIndicator(
61
+ strokeWidth: 2,
62
+ valueColor: AlwaysStoppedAnimation(CronixColors.accent),
63
+ )
64
+ else
65
+ ElevatedButton(
66
+ onPressed: enabled ? onPick : null,
67
+ style: ElevatedButton.styleFrom(
68
+ backgroundColor: CronixColors.accent,
69
+ foregroundColor: CronixColors.text,
70
+ shape: RoundedRectangleBorder(
71
+ borderRadius: CronixRadius.radiusMD,
72
+ ),
73
+ ),
74
+ child: Text(buttonText),
75
+ ),
76
+ if (fileName != null) ...[
77
+ const SizedBox(height: 8),
78
+ Row(
79
+ mainAxisAlignment: MainAxisAlignment.center,
80
+ children: [
81
+ const Icon(
82
+ Icons.insert_drive_file,
83
+ size: 16,
84
+ color: CronixColors.textSecondary,
85
+ ),
86
+ const SizedBox(width: 8),
87
+ Text(
88
+ fileName!,
89
+ style: const TextStyle(
90
+ color: CronixColors.text,
91
+ fontSize: 13,
92
+ ),
93
+ ),
94
+ ],
95
+ ),
96
+ ],
97
+ if (allowedExtensions != null) ...[
98
+ const SizedBox(height: 8),
99
+ Text(
100
+ 'Allowed: ${allowedExtensions!.join(', ')}',
101
+ style: const TextStyle(
102
+ color: CronixColors.textMuted,
103
+ fontSize: 12,
104
+ ),
105
+ ),
106
+ ],
107
+ ],
108
+ ),
109
+ ),
110
+ ],
111
+ );
112
+ }
113
+ }