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,133 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../tokens/colors.dart';
3
+ import '../tokens/spacing.dart';
4
+
5
+ class CnRadio<T> extends StatelessWidget {
6
+ final T value;
7
+ final T? groupValue;
8
+ final ValueChanged<T?>? onChanged;
9
+ final String? label;
10
+ final bool enabled;
11
+
12
+ const CnRadio({
13
+ super.key,
14
+ required this.value,
15
+ required this.groupValue,
16
+ this.onChanged,
17
+ this.label,
18
+ this.enabled = true,
19
+ });
20
+
21
+ @override
22
+ Widget build(BuildContext context) {
23
+ final isSelected = value == groupValue;
24
+
25
+ final radio = GestureDetector(
26
+ onTap: enabled && onChanged != null
27
+ ? () => onChanged!(value)
28
+ : null,
29
+ child: Container(
30
+ width: 20,
31
+ height: 20,
32
+ decoration: BoxDecoration(
33
+ shape: BoxShape.circle,
34
+ border: Border.all(
35
+ color: isSelected
36
+ ? CronixColors.accent
37
+ : CronixColors.borderLight,
38
+ width: 2,
39
+ ),
40
+ ),
41
+ child: Center(
42
+ child: Container(
43
+ width: 10,
44
+ height: 10,
45
+ decoration: BoxDecoration(
46
+ shape: BoxShape.circle,
47
+ color: isSelected ? CronixColors.accent : Colors.transparent,
48
+ ),
49
+ ),
50
+ ),
51
+ ),
52
+ );
53
+
54
+ if (label != null) {
55
+ return InkWell(
56
+ onTap: enabled && onChanged != null
57
+ ? () => onChanged!(value)
58
+ : null,
59
+ borderRadius: CronixRadius.radiusSM,
60
+ child: Row(
61
+ mainAxisSize: MainAxisSize.min,
62
+ children: [
63
+ radio,
64
+ const SizedBox(width: 8),
65
+ Text(
66
+ label!,
67
+ style: TextStyle(
68
+ color: enabled ? CronixColors.text : CronixColors.textMuted,
69
+ fontSize: 14,
70
+ ),
71
+ ),
72
+ ],
73
+ ),
74
+ );
75
+ }
76
+
77
+ return radio;
78
+ }
79
+ }
80
+
81
+ class CnRadioGroup<T> extends StatelessWidget {
82
+ final T? groupValue;
83
+ final List<CnRadioOption<T>> options;
84
+ final ValueChanged<T?>? onChanged;
85
+ final Axis direction;
86
+ final bool enabled;
87
+
88
+ const CnRadioGroup({
89
+ super.key,
90
+ required this.groupValue,
91
+ required this.options,
92
+ this.onChanged,
93
+ this.direction = Axis.vertical,
94
+ this.enabled = true,
95
+ });
96
+
97
+ @override
98
+ Widget build(BuildContext context) {
99
+ final radios = options.map((option) {
100
+ return Padding(
101
+ padding: direction == Axis.vertical
102
+ ? const EdgeInsets.only(bottom: 8)
103
+ : const EdgeInsets.only(right: 16),
104
+ child: CnRadio<T>(
105
+ value: option.value,
106
+ groupValue: groupValue,
107
+ onChanged: onChanged,
108
+ label: option.label,
109
+ enabled: enabled,
110
+ ),
111
+ );
112
+ }).toList();
113
+
114
+ return direction == Axis.vertical
115
+ ? Column(
116
+ crossAxisAlignment: CrossAxisAlignment.start,
117
+ children: radios,
118
+ )
119
+ : Row(
120
+ children: radios,
121
+ );
122
+ }
123
+ }
124
+
125
+ class CnRadioOption<T> {
126
+ final T value;
127
+ final String label;
128
+
129
+ const CnRadioOption({
130
+ required this.value,
131
+ required this.label,
132
+ });
133
+ }
@@ -0,0 +1,183 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../tokens/colors.dart';
3
+ import '../tokens/spacing.dart';
4
+
5
+ class CnSearch extends StatefulWidget {
6
+ final String? placeholder;
7
+ final ValueChanged<String>? onChanged;
8
+ final ValueChanged<String>? onSubmitted;
9
+ final List<String>? suggestions;
10
+ final ValueChanged<String>? onSuggestionSelected;
11
+ final bool loading;
12
+ final IconData? searchIcon;
13
+ final IconData? clearIcon;
14
+ final TextEditingController? controller;
15
+ final FocusNode? focusNode;
16
+
17
+ const CnSearch({
18
+ super.key,
19
+ this.placeholder,
20
+ this.onChanged,
21
+ this.onSubmitted,
22
+ this.suggestions,
23
+ this.onSuggestionSelected,
24
+ this.loading = false,
25
+ this.searchIcon,
26
+ this.clearIcon,
27
+ this.controller,
28
+ this.focusNode,
29
+ });
30
+
31
+ @override
32
+ State<CnSearch> createState() => _CnSearchState();
33
+ }
34
+
35
+ class _CnSearchState extends State<CnSearch> {
36
+ late TextEditingController _controller;
37
+ late FocusNode _focusNode;
38
+ bool _showSuggestions = false;
39
+ final LayerLink _layerLink = LayerLink();
40
+ OverlayEntry? _overlayEntry;
41
+
42
+ @override
43
+ void initState() {
44
+ super.initState();
45
+ _controller = widget.controller ?? TextEditingController();
46
+ _focusNode = widget.focusNode ?? FocusNode();
47
+ _focusNode.addListener(_handleFocusChange);
48
+ }
49
+
50
+ void _handleFocusChange() {
51
+ if (_focusNode.hasFocus && widget.suggestions != null && widget.suggestions!.isNotEmpty) {
52
+ _showSuggestionsOverlay();
53
+ } else {
54
+ _hideSuggestionsOverlay();
55
+ }
56
+ }
57
+
58
+ void _showSuggestionsOverlay() {
59
+ if (_overlayEntry != null) return;
60
+ _overlayEntry = _createOverlayEntry();
61
+ Overlay.of(context).insert(_overlayEntry!);
62
+ }
63
+
64
+ void _hideSuggestionsOverlay() {
65
+ _overlayEntry?.remove();
66
+ _overlayEntry = null;
67
+ }
68
+
69
+ OverlayEntry _createOverlayEntry() {
70
+ return OverlayEntry(
71
+ builder: (context) => Positioned(
72
+ width: 300,
73
+ child: CompositedTransformFollower(
74
+ link: _layerLink,
75
+ showWhenUnlinked: false,
76
+ offset: const Offset(0, 48),
77
+ child: Material(
78
+ color: Colors.transparent,
79
+ child: Container(
80
+ constraints: const BoxConstraints(maxHeight: 240),
81
+ decoration: BoxDecoration(
82
+ color: CronixColors.surface,
83
+ borderRadius: CronixRadius.radiusMD,
84
+ border: Border.all(color: CronixColors.border),
85
+ ),
86
+ child: ListView.builder(
87
+ shrinkWrap: true,
88
+ padding: const EdgeInsets.symmetric(vertical: 4),
89
+ itemCount: widget.suggestions?.length ?? 0,
90
+ itemBuilder: (context, index) {
91
+ final suggestion = widget.suggestions![index];
92
+ return InkWell(
93
+ onTap: () {
94
+ widget.onSuggestionSelected?.call(suggestion);
95
+ _controller.text = suggestion;
96
+ _hideSuggestionsOverlay();
97
+ },
98
+ child: Padding(
99
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
100
+ child: Text(
101
+ suggestion,
102
+ style: const TextStyle(color: CronixColors.text),
103
+ ),
104
+ ),
105
+ );
106
+ },
107
+ ),
108
+ ),
109
+ ),
110
+ ),
111
+ ),
112
+ );
113
+ }
114
+
115
+ @override
116
+ void dispose() {
117
+ _focusNode.removeListener(_handleFocusChange);
118
+ if (widget.controller == null) _controller.dispose();
119
+ if (widget.focusNode == null) _focusNode.dispose();
120
+ _hideSuggestionsOverlay();
121
+ super.dispose();
122
+ }
123
+
124
+ @override
125
+ Widget build(BuildContext context) {
126
+ return CompositedTransformTarget(
127
+ link: _layerLink,
128
+ child: TextField(
129
+ controller: _controller,
130
+ focusNode: _focusNode,
131
+ onChanged: widget.onChanged,
132
+ onSubmitted: widget.onSubmitted,
133
+ style: const TextStyle(color: CronixColors.text),
134
+ decoration: InputDecoration(
135
+ hintText: widget.placeholder ?? 'Search...',
136
+ hintStyle: const TextStyle(color: CronixColors.textMuted),
137
+ filled: true,
138
+ fillColor: CronixColors.surface,
139
+ contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
140
+ border: OutlineInputBorder(
141
+ borderRadius: CronixRadius.radiusMD,
142
+ borderSide: const BorderSide(color: CronixColors.border),
143
+ ),
144
+ enabledBorder: OutlineInputBorder(
145
+ borderRadius: CronixRadius.radiusMD,
146
+ borderSide: const BorderSide(color: CronixColors.border),
147
+ ),
148
+ focusedBorder: OutlineInputBorder(
149
+ borderRadius: CronixRadius.radiusMD,
150
+ borderSide: const BorderSide(color: CronixColors.accent, width: 2),
151
+ ),
152
+ prefixIcon: widget.loading
153
+ ? const Padding(
154
+ padding: EdgeInsets.all(12),
155
+ child: SizedBox(
156
+ width: 16,
157
+ height: 16,
158
+ child: CircularProgressIndicator(strokeWidth: 2),
159
+ ),
160
+ )
161
+ : Icon(
162
+ widget.searchIcon ?? Icons.search,
163
+ color: CronixColors.textSecondary,
164
+ size: 20,
165
+ ),
166
+ suffixIcon: _controller.text.isNotEmpty
167
+ ? IconButton(
168
+ icon: Icon(
169
+ widget.clearIcon ?? Icons.clear,
170
+ color: CronixColors.textSecondary,
171
+ size: 18,
172
+ ),
173
+ onPressed: () {
174
+ _controller.clear();
175
+ widget.onChanged?.call('');
176
+ },
177
+ )
178
+ : null,
179
+ ),
180
+ ),
181
+ );
182
+ }
183
+ }
@@ -0,0 +1,244 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../tokens/colors.dart';
3
+ import '../tokens/spacing.dart';
4
+
5
+ class CnSelectOption<T> {
6
+ final T value;
7
+ final String label;
8
+ final IconData? icon;
9
+
10
+ const CnSelectOption({
11
+ required this.value,
12
+ required this.label,
13
+ this.icon,
14
+ });
15
+ }
16
+
17
+ class CnSelect<T> extends StatefulWidget {
18
+ final String? label;
19
+ final String? placeholder;
20
+ final T? value;
21
+ final List<CnSelectOption<T>> options;
22
+ final ValueChanged<T?>? onChanged;
23
+ final bool enabled;
24
+ final bool searchable;
25
+ final String? Function(T?)? validator;
26
+
27
+ const CnSelect({
28
+ super.key,
29
+ this.label,
30
+ this.placeholder,
31
+ this.value,
32
+ required this.options,
33
+ this.onChanged,
34
+ this.enabled = true,
35
+ this.searchable = false,
36
+ this.validator,
37
+ });
38
+
39
+ @override
40
+ State<CnSelect<T>> createState() => _CnSelectState<T>();
41
+ }
42
+
43
+ class _CnSelectState<T> extends State<CnSelect<T>> {
44
+ final LayerLink _layerLink = LayerLink();
45
+ OverlayEntry? _overlayEntry;
46
+ bool _isOpen = false;
47
+ String _searchQuery = '';
48
+ String? _errorText;
49
+
50
+ String get _selectedLabel {
51
+ if (widget.value == null) return widget.placeholder ?? 'Select...';
52
+ final option = widget.options.firstWhere(
53
+ (o) => o.value == widget.value,
54
+ orElse: () => CnSelectOption(value: widget.value, label: widget.placeholder ?? 'Select...'),
55
+ );
56
+ return option.label;
57
+ }
58
+
59
+ void _toggleDropdown() {
60
+ if (_isOpen) {
61
+ _closeDropdown();
62
+ } else {
63
+ _openDropdown();
64
+ }
65
+ }
66
+
67
+ void _openDropdown() {
68
+ _overlayEntry = _createOverlayEntry();
69
+ Overlay.of(context).insert(_overlayEntry!);
70
+ setState(() => _isOpen = true);
71
+ }
72
+
73
+ void _closeDropdown() {
74
+ _overlayEntry?.remove();
75
+ _overlayEntry = null;
76
+ setState(() => _isOpen = false);
77
+ }
78
+
79
+ void _selectOption(T value) {
80
+ widget.onChanged?.call(value);
81
+ if (widget.validator != null) {
82
+ setState(() => _errorText = widget.validator!(value));
83
+ }
84
+ _closeDropdown();
85
+ }
86
+
87
+ OverlayEntry _createOverlayEntry() {
88
+ final renderBox = context.findRenderObject() as RenderBox;
89
+ final size = renderBox.size;
90
+
91
+ final filteredOptions = _searchQuery.isEmpty
92
+ ? widget.options
93
+ : widget.options
94
+ .where((o) => o.label.toLowerCase().contains(_searchQuery.toLowerCase()))
95
+ .toList();
96
+
97
+ return OverlayEntry(
98
+ builder: (context) => Positioned(
99
+ width: size.width,
100
+ child: CompositedTransformFollower(
101
+ link: _layerLink,
102
+ showWhenUnlinked: false,
103
+ offset: Offset(0, size.height + 4),
104
+ child: Material(
105
+ color: Colors.transparent,
106
+ child: Container(
107
+ constraints: const BoxConstraints(maxHeight: 240),
108
+ decoration: BoxDecoration(
109
+ color: CronixColors.surface,
110
+ borderRadius: CronixRadius.radiusMD,
111
+ border: Border.all(color: CronixColors.border),
112
+ ),
113
+ child: Column(
114
+ mainAxisSize: MainAxisSize.min,
115
+ children: [
116
+ if (widget.searchable)
117
+ Padding(
118
+ padding: const EdgeInsets.all(8),
119
+ child: TextField(
120
+ onChanged: (value) => setState(() => _searchQuery = value),
121
+ style: const TextStyle(color: CronixColors.text),
122
+ decoration: InputDecoration(
123
+ hintText: 'Search...',
124
+ hintStyle: const TextStyle(color: CronixColors.textMuted),
125
+ filled: true,
126
+ fillColor: CronixColors.background,
127
+ contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
128
+ border: OutlineInputBorder(
129
+ borderRadius: CronixRadius.radiusSM,
130
+ borderSide: const BorderSide(color: CronixColors.border),
131
+ ),
132
+ prefixIcon: const Icon(Icons.search, color: CronixColors.textSecondary, size: 18),
133
+ ),
134
+ ),
135
+ ),
136
+ Flexible(
137
+ child: ListView.builder(
138
+ shrinkWrap: true,
139
+ padding: const EdgeInsets.symmetric(vertical: 4),
140
+ itemCount: filteredOptions.length,
141
+ itemBuilder: (context, index) {
142
+ final option = filteredOptions[index];
143
+ final isSelected = option.value == widget.value;
144
+ return InkWell(
145
+ onTap: () => _selectOption(option.value),
146
+ child: Container(
147
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
148
+ color: isSelected ? CronixColors.accent.withOpacity(0.2) : null,
149
+ child: Row(
150
+ children: [
151
+ if (option.icon != null) ...[
152
+ Icon(option.icon, size: 18, color: CronixColors.textSecondary),
153
+ const SizedBox(width: 8),
154
+ ],
155
+ Text(
156
+ option.label,
157
+ style: TextStyle(
158
+ color: isSelected ? CronixColors.accent : CronixColors.text,
159
+ fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400,
160
+ ),
161
+ ),
162
+ const Spacer(),
163
+ if (isSelected)
164
+ const Icon(Icons.check, size: 16, color: CronixColors.accent),
165
+ ],
166
+ ),
167
+ ),
168
+ );
169
+ },
170
+ ),
171
+ ),
172
+ ],
173
+ ),
174
+ ),
175
+ ),
176
+ ),
177
+ ),
178
+ );
179
+ }
180
+
181
+ @override
182
+ Widget build(BuildContext context) {
183
+ return Column(
184
+ crossAxisAlignment: CrossAxisAlignment.start,
185
+ mainAxisSize: MainAxisSize.min,
186
+ children: [
187
+ if (widget.label != null) ...[
188
+ Text(
189
+ widget.label!,
190
+ style: const TextStyle(
191
+ color: CronixColors.text,
192
+ fontSize: 14,
193
+ fontWeight: FontWeight.w500,
194
+ ),
195
+ ),
196
+ const SizedBox(height: 8),
197
+ ],
198
+ CompositedTransformTarget(
199
+ link: _layerLink,
200
+ child: GestureDetector(
201
+ onTap: widget.enabled ? _toggleDropdown : null,
202
+ child: Container(
203
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
204
+ decoration: BoxDecoration(
205
+ color: widget.enabled ? CronixColors.surface : CronixColors.surfaceLight,
206
+ borderRadius: CronixRadius.radiusMD,
207
+ border: Border.all(
208
+ color: _errorText != null
209
+ ? CronixColors.error
210
+ : CronixColors.border,
211
+ ),
212
+ ),
213
+ child: Row(
214
+ children: [
215
+ Expanded(
216
+ child: Text(
217
+ _selectedLabel,
218
+ style: TextStyle(
219
+ color: widget.value != null
220
+ ? CronixColors.text
221
+ : CronixColors.textMuted,
222
+ ),
223
+ ),
224
+ ),
225
+ Icon(
226
+ _isOpen ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
227
+ color: CronixColors.textSecondary,
228
+ ),
229
+ ],
230
+ ),
231
+ ),
232
+ ),
233
+ ),
234
+ if (_errorText != null) ...[
235
+ const SizedBox(height: 4),
236
+ Text(
237
+ _errorText!,
238
+ style: const TextStyle(color: CronixColors.error, fontSize: 12),
239
+ ),
240
+ ],
241
+ ],
242
+ );
243
+ }
244
+ }