cronixui 1.0.6 → 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.
Files changed (169) hide show
  1. package/README.md +20 -5
  2. package/package.json +20 -5
  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
  43. package/packages/go/cronixui/cronixui.go +784 -237
  44. package/packages/go/cronixui/go.mod +32 -0
  45. package/packages/go/cronixui/go.sum +666 -0
  46. package/packages/python/cronixui/__init__.py +59 -3
  47. package/packages/python/cronixui/alert.py +61 -0
  48. package/packages/python/cronixui/avatar.py +50 -0
  49. package/packages/python/cronixui/badge.py +46 -0
  50. package/packages/python/cronixui/button.py +64 -0
  51. package/packages/python/cronixui/card.py +62 -0
  52. package/packages/python/cronixui/form.py +255 -0
  53. package/packages/python/cronixui/layout.py +143 -0
  54. package/packages/python/cronixui/list.py +51 -0
  55. package/packages/python/cronixui/loading.py +36 -0
  56. package/packages/python/cronixui/progress.py +90 -0
  57. package/packages/python/cronixui/table.py +48 -0
  58. package/packages/python/cronixui/tooltip.py +28 -0
  59. package/packages/react/src/components/Accordion.tsx +82 -0
  60. package/packages/react/src/components/Alert.tsx +80 -0
  61. package/packages/react/src/components/Avatar.tsx +54 -0
  62. package/packages/react/src/components/Badge.tsx +32 -0
  63. package/packages/react/src/components/Breadcrumb.tsx +50 -0
  64. package/packages/react/src/components/Button.tsx +47 -0
  65. package/packages/react/src/components/Card.tsx +69 -0
  66. package/packages/react/src/components/Checkbox.tsx +30 -0
  67. package/packages/react/src/components/CommandPalette.tsx +131 -0
  68. package/packages/react/src/components/Container.tsx +26 -0
  69. package/packages/react/src/components/Dropdown.tsx +88 -0
  70. package/packages/react/src/components/FileInput.tsx +86 -0
  71. package/packages/react/src/components/Footer.tsx +36 -0
  72. package/packages/react/src/components/FormGroup.tsx +36 -0
  73. package/packages/react/src/components/Header.tsx +29 -0
  74. package/packages/react/src/components/Input.tsx +54 -0
  75. package/packages/react/src/components/List.tsx +55 -0
  76. package/packages/react/src/components/Modal.tsx +89 -0
  77. package/packages/react/src/components/Nav.tsx +63 -0
  78. package/packages/react/src/components/Pagination.tsx +107 -0
  79. package/packages/react/src/components/Progress.tsx +49 -0
  80. package/packages/react/src/components/Radio.tsx +64 -0
  81. package/packages/react/src/components/Search.tsx +95 -0
  82. package/packages/react/src/components/Select.tsx +41 -0
  83. package/packages/react/src/components/Sidebar.tsx +64 -0
  84. package/packages/react/src/components/Skeleton.tsx +39 -0
  85. package/packages/react/src/components/Slider.tsx +32 -0
  86. package/packages/react/src/components/Spinner.tsx +24 -0
  87. package/packages/react/src/components/Stack.tsx +69 -0
  88. package/packages/react/src/components/Stat.tsx +35 -0
  89. package/packages/react/src/components/Table.tsx +90 -0
  90. package/packages/react/src/components/Tabs.tsx +85 -0
  91. package/packages/react/src/components/Tag.tsx +30 -0
  92. package/packages/react/src/components/Textarea.tsx +21 -0
  93. package/packages/react/src/components/Toast.tsx +134 -0
  94. package/packages/react/src/components/Toggle.tsx +58 -0
  95. package/packages/react/src/components/Tooltip.tsx +31 -0
  96. package/packages/react/src/components/Typography.tsx +66 -0
  97. package/packages/react/src/index.ts +40 -0
  98. package/packages/react/src/styles.css +2039 -0
  99. package/packages/react/src/tokens/index.ts +94 -0
  100. package/packages/rust/cronixui/src/colors.rs +135 -0
  101. package/packages/rust/cronixui/src/components/accordion.rs +47 -0
  102. package/packages/rust/cronixui/src/components/alert.rs +95 -0
  103. package/packages/rust/cronixui/src/components/avatar.rs +85 -0
  104. package/packages/rust/cronixui/src/components/badge.rs +35 -0
  105. package/packages/rust/cronixui/src/components/breadcrumb.rs +58 -0
  106. package/packages/rust/cronixui/src/components/button.rs +70 -0
  107. package/packages/rust/cronixui/src/components/card.rs +259 -0
  108. package/packages/rust/cronixui/src/components/command_palette.rs +254 -0
  109. package/packages/rust/cronixui/src/components/dropdown.rs +179 -0
  110. package/packages/rust/cronixui/src/components/file_input.rs +74 -0
  111. package/packages/rust/cronixui/src/components/input.rs +21 -0
  112. package/packages/rust/cronixui/src/components/list.rs +38 -0
  113. package/packages/rust/cronixui/src/components/mod.rs +51 -0
  114. package/packages/rust/cronixui/src/{modal.rs → components/modal.rs} +15 -1
  115. package/packages/rust/cronixui/src/components/nav.rs +19 -0
  116. package/packages/rust/cronixui/src/{pagination.rs → components/pagination.rs} +14 -13
  117. package/packages/rust/cronixui/src/components/progress.rs +50 -0
  118. package/packages/rust/cronixui/src/components/search.rs +185 -0
  119. package/packages/rust/cronixui/src/components/skeleton.rs +63 -0
  120. package/packages/rust/cronixui/src/components/spinner.rs +21 -0
  121. package/packages/rust/cronixui/src/components/table.rs +56 -0
  122. package/packages/rust/cronixui/src/components/tabs.rs +43 -0
  123. package/packages/rust/cronixui/src/components/toast.rs +69 -0
  124. package/packages/rust/cronixui/src/{toggle.rs → components/toggle.rs} +7 -5
  125. package/packages/rust/cronixui/src/components/tooltip.rs +11 -0
  126. package/packages/rust/cronixui/src/lib.rs +111 -64
  127. package/packages/rust/cronixui/src/tokens.rs +97 -127
  128. package/packages/web/src/variables.css +81 -81
  129. package/packages/go/cronixui/tokens.go +0 -129
  130. package/packages/python/cronixui/pyproject.toml +0 -11
  131. package/packages/react/src/components/Accordion.jsx +0 -50
  132. package/packages/react/src/components/Alert.jsx +0 -62
  133. package/packages/react/src/components/Avatar.jsx +0 -34
  134. package/packages/react/src/components/Badge.jsx +0 -15
  135. package/packages/react/src/components/Breadcrumb.jsx +0 -27
  136. package/packages/react/src/components/Button.jsx +0 -21
  137. package/packages/react/src/components/Card.jsx +0 -23
  138. package/packages/react/src/components/Checkbox.jsx +0 -27
  139. package/packages/react/src/components/CommandPalette.jsx +0 -93
  140. package/packages/react/src/components/Dropdown.jsx +0 -48
  141. package/packages/react/src/components/FileInput.jsx +0 -44
  142. package/packages/react/src/components/Input.jsx +0 -22
  143. package/packages/react/src/components/List.jsx +0 -29
  144. package/packages/react/src/components/Modal.jsx +0 -65
  145. package/packages/react/src/components/Nav.jsx +0 -50
  146. package/packages/react/src/components/Pagination.jsx +0 -81
  147. package/packages/react/src/components/Progress.jsx +0 -23
  148. package/packages/react/src/components/Radio.jsx +0 -50
  149. package/packages/react/src/components/Search.jsx +0 -70
  150. package/packages/react/src/components/Select.jsx +0 -33
  151. package/packages/react/src/components/Skeleton.jsx +0 -15
  152. package/packages/react/src/components/Slider.jsx +0 -29
  153. package/packages/react/src/components/Spinner.jsx +0 -5
  154. package/packages/react/src/components/Stat.jsx +0 -19
  155. package/packages/react/src/components/Table.jsx +0 -48
  156. package/packages/react/src/components/Tabs.jsx +0 -65
  157. package/packages/react/src/components/Tag.jsx +0 -19
  158. package/packages/react/src/components/Textarea.jsx +0 -17
  159. package/packages/react/src/components/Toast.jsx +0 -78
  160. package/packages/react/src/components/Toggle.jsx +0 -34
  161. package/packages/react/src/components/Tooltip.jsx +0 -12
  162. package/packages/react/src/index.d.ts +0 -103
  163. package/packages/react/src/index.js +0 -33
  164. package/packages/rust/cronixui/src/accordion.rs +0 -49
  165. package/packages/rust/cronixui/src/command_palette.rs +0 -62
  166. package/packages/rust/cronixui/src/dropdown.rs +0 -31
  167. package/packages/rust/cronixui/src/search.rs +0 -49
  168. package/packages/rust/cronixui/src/tabs.rs +0 -23
  169. package/packages/rust/cronixui/src/toast.rs +0 -70
@@ -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
+ }