kasy-cli 1.14.0 → 1.15.0

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 (52) hide show
  1. package/bin/kasy.js +18 -5
  2. package/lib/commands/ios.js +8 -2
  3. package/lib/commands/reset.js +100 -2
  4. package/lib/commands/splash.js +11 -0
  5. package/lib/scaffold/backends/api/pubspec.yaml.tpl +2 -2
  6. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +2 -2
  7. package/lib/utils/apple-release.js +30 -0
  8. package/lib/utils/checks.js +41 -2
  9. package/lib/utils/debug.js +75 -0
  10. package/lib/utils/friendly-error.js +91 -0
  11. package/lib/utils/i18n/messages-en.js +970 -0
  12. package/lib/utils/i18n/messages-es.js +968 -0
  13. package/lib/utils/i18n/messages-pt.js +968 -0
  14. package/lib/utils/i18n.js +21 -2818
  15. package/lib/utils/png-padding.js +120 -0
  16. package/package.json +8 -3
  17. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +12 -11
  18. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  19. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png +0 -0
  20. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png +0 -0
  21. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  22. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png +0 -0
  23. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png +0 -0
  24. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  25. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  26. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  27. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  28. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  29. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  30. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png +0 -0
  31. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png +0 -0
  32. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  33. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png +0 -0
  34. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png +0 -0
  35. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  36. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png +0 -0
  37. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png +0 -0
  38. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +18 -11
  39. package/templates/firebase/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +9 -0
  40. package/templates/firebase/assets/images/icon_android.png +0 -0
  41. package/templates/firebase/assets/images/icon_foreground_empty.png +0 -0
  42. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  43. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  44. package/templates/firebase/lib/components/components.dart +1 -0
  45. package/templates/firebase/lib/components/kasy_avatar.dart +88 -57
  46. package/templates/firebase/lib/components/kasy_avatar_presets.dart +116 -74
  47. package/templates/firebase/lib/components/kasy_tabs.dart +431 -0
  48. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -6
  49. package/templates/firebase/lib/features/home/home_components_page.dart +1 -1
  50. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +316 -93
  51. package/templates/firebase/pubspec.yaml +4 -2
  52. package/templates/firebase/web/index.html +6 -0
@@ -1,94 +1,136 @@
1
1
  import 'package:flutter/material.dart';
2
2
 
3
- /// Optional radial orb presets for [KasyAvatar] via `backgroundGradient` or
3
+ /// Linear gradient presets for [KasyAvatar] via `backgroundGradient` or
4
4
  /// [KasyAvatar.gradientFill].
5
5
  ///
6
- /// Remote photo URLs ([KasyAvatarDemoPhotos]) are for demos/catalog only (needs network).
7
- abstract final class KasyAvatarOrbGradients {
8
- static const RadialGradient sphereBlue = RadialGradient(
9
- center: Alignment(-0.42, -0.48),
10
- radius: 1.08,
11
- colors: [
12
- Color(0xFFE3F4FF),
13
- Color(0xFF64B5F6),
14
- Color(0xFF1E88E5),
15
- Color(0xFF0A3068),
16
- ],
17
- stops: [0.0, 0.32, 0.68, 1.0],
6
+ /// Matches the HeroUI Figma Kit V3 avatar palette exactly.
7
+ /// All gradients use 141° (upper-left → lower-right), flat from 0%→27%,
8
+ /// then transition to the second color at 100% — same as Figma specification.
9
+ abstract final class KasyAvatarGradients {
10
+ // 141° CSS angle → Flutter Alignment
11
+ // begin = upper-left region (-sin141°, cos141°) ≈ (-0.63, -0.78)
12
+ // end = lower-right region (+sin141°, -cos141°) ≈ (+0.63, +0.78)
13
+ static const Alignment _b = Alignment(-0.63, -0.78);
14
+ static const Alignment _e = Alignment(0.63, 0.78);
15
+
16
+ /// Cornflower blue → deep blue
17
+ static const LinearGradient blue = LinearGradient(
18
+ begin: _b,
19
+ end: _e,
20
+ colors: [Color(0xFF5D9BE7), Color(0xFF5D9BE7), Color(0xFF0026FF)],
21
+ stops: [0.0, 0.27, 1.0],
22
+ );
23
+
24
+ /// Cyan-blue → violet
25
+ static const LinearGradient sky = LinearGradient(
26
+ begin: _b,
27
+ end: _e,
28
+ colors: [Color(0xFF5DD0E7), Color(0xFF5DD0E7), Color(0xFF7300FF)],
29
+ stops: [0.0, 0.27, 1.0],
30
+ );
31
+
32
+ /// Aqua → deep blue
33
+ static const LinearGradient teal = LinearGradient(
34
+ begin: _b,
35
+ end: _e,
36
+ colors: [Color(0xFF5DE7E7), Color(0xFF5DE7E7), Color(0xFF001AFF)],
37
+ stops: [0.0, 0.27, 1.0],
38
+ );
39
+
40
+ /// Mint green → royal blue
41
+ static const LinearGradient emerald = LinearGradient(
42
+ begin: _b,
43
+ end: _e,
44
+ colors: [Color(0xFF5DE79D), Color(0xFF5DE79D), Color(0xFF0033FF)],
45
+ stops: [0.0, 0.27, 1.0],
46
+ );
47
+
48
+ /// Seafoam → dark forest green
49
+ static const LinearGradient green = LinearGradient(
50
+ begin: _b,
51
+ end: _e,
52
+ colors: [Color(0xFF52DEB0), Color(0xFF52DEB0), Color(0xFF0D5C45)],
53
+ stops: [0.0, 0.27, 1.0],
54
+ );
55
+
56
+ /// Turquoise → deep forest green
57
+ static const LinearGradient forest = LinearGradient(
58
+ begin: _b,
59
+ end: _e,
60
+ colors: [Color(0xFF5DD9AF), Color(0xFF5DD9AF), Color(0xFF094E39)],
61
+ stops: [0.0, 0.27, 1.0],
62
+ );
63
+
64
+ /// Golden yellow → deep red-orange
65
+ static const LinearGradient orange = LinearGradient(
66
+ begin: _b,
67
+ end: _e,
68
+ colors: [Color(0xFFE7BD5D), Color(0xFFE7BD5D), Color(0xFFFF1E00)],
69
+ stops: [0.0, 0.27, 1.0],
70
+ );
71
+
72
+ /// Salmon → scarlet
73
+ static const LinearGradient red = LinearGradient(
74
+ begin: _b,
75
+ end: _e,
76
+ colors: [Color(0xFFE7885D), Color(0xFFE7885D), Color(0xFFFF0004)],
77
+ stops: [0.0, 0.27, 1.0],
18
78
  );
19
79
 
20
- static const RadialGradient sphereFuchsia = RadialGradient(
21
- center: Alignment(-0.36, -0.46),
22
- radius: 1.1,
23
- colors: [
24
- Color(0xFFFFE4EE),
25
- Color(0xFFF48FB1),
26
- Color(0xFFE91E63),
27
- Color(0xFF880E4F),
28
- ],
29
- stops: [0.0, 0.35, 0.71, 1.0],
80
+ /// Soft purple cobalt blue
81
+ static const LinearGradient indigo = LinearGradient(
82
+ begin: _b,
83
+ end: _e,
84
+ colors: [Color(0xFFB45DE7), Color(0xFFB45DE7), Color(0xFF0055FF)],
85
+ stops: [0.0, 0.27, 1.0],
30
86
  );
31
87
 
32
- static const RadialGradient sphereSunset = RadialGradient(
33
- center: Alignment(-0.45, -0.38),
34
- radius: 1.06,
35
- colors: [
36
- Color(0xFFFFF8E1),
37
- Color(0xFFFFB74D),
38
- Color(0xFFFF6E40),
39
- Color(0xFFB71C1C),
40
- ],
41
- stops: [0.0, 0.33, 0.69, 1.0],
88
+ /// Medium purple crimson
89
+ static const LinearGradient purple = LinearGradient(
90
+ begin: _b,
91
+ end: _e,
92
+ colors: [Color(0xFF8D5DE7), Color(0xFF8D5DE7), Color(0xFFFF0009)],
93
+ stops: [0.0, 0.27, 1.0],
42
94
  );
43
95
 
44
- static const RadialGradient sphereTeal = RadialGradient(
45
- center: Alignment(-0.4, -0.42),
46
- radius: 1.07,
47
- colors: [
48
- Color(0xFFE0F7F4),
49
- Color(0xFF4DD0E1),
50
- Color(0xFF00838F),
51
- Color(0xFF004D40),
52
- ],
53
- stops: [0.0, 0.34, 0.7, 1.0],
96
+ /// Orchid pink deep magenta-red
97
+ static const LinearGradient rose = LinearGradient(
98
+ begin: _b,
99
+ end: _e,
100
+ colors: [Color(0xFFE75DCB), Color(0xFFE75DCB), Color(0xFFFF000D)],
101
+ stops: [0.0, 0.27, 1.0],
54
102
  );
55
103
 
56
- static const RadialGradient sphereViolet = RadialGradient(
57
- center: Alignment(-0.38, -0.5),
58
- radius: 1.09,
59
- colors: [
60
- Color(0xFFF3E5F5),
61
- Color(0xFFCE93D8),
62
- Color(0xFF8E24AA),
63
- Color(0xFF311B92),
64
- ],
65
- stops: [0.0, 0.36, 0.72, 1.0],
104
+ /// Hot pink coral red
105
+ static const LinearGradient hotPink = LinearGradient(
106
+ begin: _b,
107
+ end: _e,
108
+ colors: [Color(0xFFE43673), Color(0xFFE43673), Color(0xFFFB5059)],
109
+ stops: [0.0, 0.27, 1.0],
66
110
  );
67
111
 
68
- static const RadialGradient sphereAmber = RadialGradient(
69
- center: Alignment(-0.45, -0.35),
70
- radius: 1.06,
71
- colors: [
72
- Color(0xFFFFFDE7),
73
- Color(0xFFFFCA28),
74
- Color(0xFFFF8F00),
75
- Color(0xFFE65100),
76
- ],
77
- stops: [0.0, 0.33, 0.69, 1.0],
112
+ /// Silver near-black
113
+ static const LinearGradient silver = LinearGradient(
114
+ begin: _b,
115
+ end: _e,
116
+ colors: [Color(0xFF949494), Color(0xFF949494), Color(0xFF080808)],
117
+ stops: [0.0, 0.27, 1.0],
78
118
  );
79
119
 
80
- /// Dual-tone orb pairing well with initials on top.
81
- static const RadialGradient sphereGb = RadialGradient(
82
- center: Alignment(-0.28, -0.52),
83
- radius: 1.12,
84
- colors: [
85
- Color(0xFFFFCCBC),
86
- Color(0xFFFF4081),
87
- Color(0xFFB388FF),
88
- Color(0xFF4527A0),
89
- ],
90
- stops: [0.0, 0.3, 0.62, 1.0],
120
+ /// Mid-grey very dark grey
121
+ static const LinearGradient black = LinearGradient(
122
+ begin: _b,
123
+ end: _e,
124
+ colors: [Color(0xFF7F7F7F), Color(0xFF7F7F7F), Color(0xFF1F1F1F)],
125
+ stops: [0.0, 0.27, 1.0],
91
126
  );
127
+
128
+ /// All 14 presets in display order (matches Figma "Image Use" section).
129
+ static const List<LinearGradient> all = [
130
+ blue, sky, teal, emerald, green, forest,
131
+ orange, red, indigo, purple, rose, hotPink,
132
+ silver, black,
133
+ ];
92
134
  }
93
135
 
94
136
  /// Fixed Unsplash URLs for demos/catalog (network + permissions in prod).
@@ -0,0 +1,431 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:kasy_kit/core/theme/theme.dart';
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // Data model
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+
8
+ /// Data model for a single tab item.
9
+ class KasyTabItem {
10
+ final String label;
11
+ final IconData? icon;
12
+ final bool enabled;
13
+
14
+ const KasyTabItem({
15
+ required this.label,
16
+ this.icon,
17
+ this.enabled = true,
18
+ });
19
+ }
20
+
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ // Enums
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+
25
+ /// Visual style for [KasyTabs].
26
+ enum KasyTabsVariant {
27
+ /// Sliding white pill on a gray container.
28
+ primary,
29
+
30
+ /// Blue underline indicator with a bottom divider.
31
+ secondary,
32
+ }
33
+
34
+ /// Sizing mode for [KasyTabs].
35
+ enum KasyTabsMode {
36
+ /// Wraps content (intrinsic width tabs).
37
+ hug,
38
+
39
+ /// Each tab stretches to fill the available width equally.
40
+ fill,
41
+ }
42
+
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+ // KasyTabs
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+
47
+ /// Animated tab bar with primary (pill) and secondary (underline) variants.
48
+ ///
49
+ /// Usage:
50
+ /// ```dart
51
+ /// KasyTabs(
52
+ /// tabs: ['General', 'Appearance', 'Notifications'],
53
+ /// selectedIndex: _index,
54
+ /// onTabSelected: (i) => setState(() => _index = i),
55
+ /// )
56
+ /// ```
57
+ ///
58
+ /// Supports [KasyTabItem] for per-tab icons and disabled state. The short-form
59
+ /// constructor accepts plain [String] labels.
60
+ class KasyTabs extends StatefulWidget {
61
+ /// Plain-string convenience constructor.
62
+ ///
63
+ /// Converts each string to a [KasyTabItem] with default settings.
64
+ KasyTabs({
65
+ super.key,
66
+ required List<String> tabs,
67
+ required this.selectedIndex,
68
+ required this.onTabSelected,
69
+ this.variant = KasyTabsVariant.primary,
70
+ this.mode = KasyTabsMode.hug,
71
+ }) : items = tabs.map((l) => KasyTabItem(label: l)).toList();
72
+
73
+ /// Full constructor accepting [KasyTabItem] instances (supports icons/disabled).
74
+ const KasyTabs.items({
75
+ super.key,
76
+ required this.items,
77
+ required this.selectedIndex,
78
+ required this.onTabSelected,
79
+ this.variant = KasyTabsVariant.primary,
80
+ this.mode = KasyTabsMode.hug,
81
+ });
82
+
83
+ final List<KasyTabItem> items;
84
+
85
+ /// Index of the currently selected tab.
86
+ final int selectedIndex;
87
+
88
+ /// Called when the user taps an enabled tab.
89
+ final ValueChanged<int> onTabSelected;
90
+
91
+ final KasyTabsVariant variant;
92
+ final KasyTabsMode mode;
93
+
94
+ @override
95
+ State<KasyTabs> createState() => _KasyTabsState();
96
+ }
97
+
98
+ class _KasyTabsState extends State<KasyTabs> {
99
+ // One GlobalKey per tab to measure position/size after layout.
100
+ late List<GlobalKey> _keys;
101
+
102
+ // Indicator geometry (left offset, width) resolved from measured keys.
103
+ double _indicatorLeft = 0;
104
+ double _indicatorWidth = 0;
105
+
106
+ // Whether we have a valid measurement yet.
107
+ bool _measured = false;
108
+
109
+ @override
110
+ void initState() {
111
+ super.initState();
112
+ _keys = List.generate(
113
+ widget.items.length,
114
+ (_) => GlobalKey(),
115
+ );
116
+ WidgetsBinding.instance.addPostFrameCallback((_) => _measure());
117
+ }
118
+
119
+ @override
120
+ void didUpdateWidget(KasyTabs old) {
121
+ super.didUpdateWidget(old);
122
+ // Rebuild keys list if tab count changes.
123
+ if (old.items.length != widget.items.length) {
124
+ _keys = List.generate(
125
+ widget.items.length,
126
+ (_) => GlobalKey(),
127
+ );
128
+ _measured = false;
129
+ }
130
+ // Re-measure whenever selected index changes or keys were rebuilt.
131
+ WidgetsBinding.instance.addPostFrameCallback((_) => _measure());
132
+ }
133
+
134
+ void _measure() {
135
+ if (!mounted) return;
136
+ if (widget.items.isEmpty) return;
137
+
138
+ final int clampedIndex = widget.selectedIndex.clamp(
139
+ 0,
140
+ widget.items.length - 1,
141
+ );
142
+
143
+ final RenderBox? containerBox =
144
+ context.findRenderObject() as RenderBox?;
145
+ if (containerBox == null) return;
146
+
147
+ final GlobalKey key = _keys[clampedIndex];
148
+ final RenderBox? tabBox =
149
+ key.currentContext?.findRenderObject() as RenderBox?;
150
+ if (tabBox == null) return;
151
+
152
+ final Offset tabOffset = tabBox.localToGlobal(
153
+ Offset.zero,
154
+ ancestor: containerBox,
155
+ );
156
+
157
+ final double newLeft = tabOffset.dx;
158
+ final double newWidth = tabBox.size.width;
159
+
160
+ if (!_measured ||
161
+ (_indicatorLeft - newLeft).abs() > 0.1 ||
162
+ (_indicatorWidth - newWidth).abs() > 0.1) {
163
+ setState(() {
164
+ _indicatorLeft = newLeft;
165
+ _indicatorWidth = newWidth;
166
+ _measured = true;
167
+ });
168
+ }
169
+ }
170
+
171
+ @override
172
+ Widget build(BuildContext context) {
173
+ if (widget.items.isEmpty) return const SizedBox.shrink();
174
+
175
+ return widget.variant == KasyTabsVariant.primary
176
+ ? _buildPrimary(context)
177
+ : _buildSecondary(context);
178
+ }
179
+
180
+ // ── Primary (pill indicator) ───────────────────────────────────────────────
181
+
182
+ Widget _buildPrimary(BuildContext context) {
183
+ final KasyColors c = context.colors;
184
+ final bool isDark = context.isDark;
185
+
186
+ return DecoratedBox(
187
+ decoration: BoxDecoration(
188
+ color: c.avatarFallbackFill,
189
+ borderRadius: BorderRadius.circular(KasyRadius.full),
190
+ ),
191
+ child: Padding(
192
+ padding: const EdgeInsets.all(4),
193
+ child: Stack(
194
+ children: [
195
+ // Animated pill background.
196
+ if (_measured)
197
+ AnimatedPositioned(
198
+ duration: const Duration(milliseconds: 250),
199
+ curve: Curves.easeInOut,
200
+ left: _indicatorLeft,
201
+ top: 0,
202
+ bottom: 0,
203
+ width: _indicatorWidth,
204
+ child: DecoratedBox(
205
+ decoration: BoxDecoration(
206
+ color: c.surface,
207
+ borderRadius: BorderRadius.circular(KasyRadius.full),
208
+ boxShadow: [
209
+ BoxShadow(
210
+ color: Colors.black.withValues(
211
+ alpha: isDark ? 0.18 : 0.08,
212
+ ),
213
+ blurRadius: 8,
214
+ offset: const Offset(0, 2),
215
+ ),
216
+ BoxShadow(
217
+ color: Colors.black.withValues(
218
+ alpha: isDark ? 0.10 : 0.04,
219
+ ),
220
+ spreadRadius: 1,
221
+ ),
222
+ ],
223
+ ),
224
+ ),
225
+ ),
226
+ // Tab labels (on top of the pill).
227
+ Row(
228
+ mainAxisSize: widget.mode == KasyTabsMode.fill
229
+ ? MainAxisSize.max
230
+ : MainAxisSize.min,
231
+ children: List.generate(widget.items.length, (i) {
232
+ final KasyTabItem item = widget.items[i];
233
+ final bool selected = i == widget.selectedIndex;
234
+ return _PrimaryTab(
235
+ key: _keys[i],
236
+ item: item,
237
+ selected: selected,
238
+ expand: widget.mode == KasyTabsMode.fill,
239
+ onTap: item.enabled ? () => widget.onTabSelected(i) : null,
240
+ );
241
+ }),
242
+ ),
243
+ ],
244
+ ),
245
+ ),
246
+ );
247
+ }
248
+
249
+ // ── Secondary (underline indicator) ───────────────────────────────────────
250
+
251
+ Widget _buildSecondary(BuildContext context) {
252
+ final KasyColors c = context.colors;
253
+
254
+ return Stack(
255
+ clipBehavior: Clip.none,
256
+ children: [
257
+ // Full-width bottom divider.
258
+ Positioned(
259
+ left: 0,
260
+ right: 0,
261
+ bottom: 0,
262
+ child: DecoratedBox(
263
+ decoration: BoxDecoration(
264
+ color: c.outline.withValues(alpha: 0.2),
265
+ ),
266
+ child: const SizedBox(height: 1),
267
+ ),
268
+ ),
269
+ // Animated underline indicator.
270
+ if (_measured)
271
+ AnimatedPositioned(
272
+ duration: const Duration(milliseconds: 250),
273
+ curve: Curves.easeInOut,
274
+ left: _indicatorLeft,
275
+ bottom: 0,
276
+ width: _indicatorWidth,
277
+ height: 2,
278
+ child: DecoratedBox(
279
+ decoration: BoxDecoration(
280
+ color: c.primary,
281
+ borderRadius: BorderRadius.circular(1),
282
+ ),
283
+ ),
284
+ ),
285
+ // Tab labels.
286
+ Row(
287
+ mainAxisSize: widget.mode == KasyTabsMode.fill
288
+ ? MainAxisSize.max
289
+ : MainAxisSize.min,
290
+ children: List.generate(widget.items.length, (i) {
291
+ final KasyTabItem item = widget.items[i];
292
+ final bool selected = i == widget.selectedIndex;
293
+ return _SecondaryTab(
294
+ key: _keys[i],
295
+ item: item,
296
+ selected: selected,
297
+ expand: widget.mode == KasyTabsMode.fill,
298
+ onTap: item.enabled ? () => widget.onTabSelected(i) : null,
299
+ );
300
+ }),
301
+ ),
302
+ ],
303
+ );
304
+ }
305
+ }
306
+
307
+ // ─────────────────────────────────────────────────────────────────────────────
308
+ // Internal tab widgets
309
+ // ─────────────────────────────────────────────────────────────────────────────
310
+
311
+ class _PrimaryTab extends StatelessWidget {
312
+ const _PrimaryTab({
313
+ super.key,
314
+ required this.item,
315
+ required this.selected,
316
+ required this.expand,
317
+ required this.onTap,
318
+ });
319
+
320
+ final KasyTabItem item;
321
+ final bool selected;
322
+ final bool expand;
323
+ final VoidCallback? onTap;
324
+
325
+ Widget _tabContent(BuildContext context) {
326
+ final KasyColors c = context.colors;
327
+ final bool disabled = !item.enabled;
328
+
329
+ return GestureDetector(
330
+ onTap: onTap,
331
+ behavior: HitTestBehavior.opaque,
332
+ child: Padding(
333
+ padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
334
+ child: Row(
335
+ mainAxisSize: MainAxisSize.min,
336
+ mainAxisAlignment: MainAxisAlignment.center,
337
+ children: [
338
+ if (item.icon != null) ...[
339
+ Opacity(
340
+ opacity: disabled ? 0.4 : 1.0,
341
+ child: Icon(
342
+ item.icon,
343
+ size: 16,
344
+ color: selected ? c.onSurface : c.muted,
345
+ ),
346
+ ),
347
+ const SizedBox(width: 6),
348
+ ],
349
+ Opacity(
350
+ opacity: disabled ? 0.4 : 1.0,
351
+ child: Text(
352
+ item.label,
353
+ style: context.textTheme.labelLarge?.copyWith(
354
+ color: selected ? c.onSurface : c.muted,
355
+ fontWeight: selected ? FontWeight.w500 : FontWeight.w400,
356
+ ),
357
+ ),
358
+ ),
359
+ ],
360
+ ),
361
+ ),
362
+ );
363
+ }
364
+
365
+ @override
366
+ Widget build(BuildContext context) {
367
+ final Widget inner = _tabContent(context);
368
+ return expand ? Expanded(child: inner) : inner;
369
+ }
370
+ }
371
+
372
+ class _SecondaryTab extends StatelessWidget {
373
+ const _SecondaryTab({
374
+ super.key,
375
+ required this.item,
376
+ required this.selected,
377
+ required this.expand,
378
+ required this.onTap,
379
+ });
380
+
381
+ final KasyTabItem item;
382
+ final bool selected;
383
+ final bool expand;
384
+ final VoidCallback? onTap;
385
+
386
+ Widget _tabContent(BuildContext context) {
387
+ final KasyColors c = context.colors;
388
+ final bool disabled = !item.enabled;
389
+
390
+ return GestureDetector(
391
+ onTap: onTap,
392
+ behavior: HitTestBehavior.opaque,
393
+ child: Padding(
394
+ padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
395
+ child: Row(
396
+ mainAxisSize: MainAxisSize.min,
397
+ mainAxisAlignment: MainAxisAlignment.center,
398
+ children: [
399
+ if (item.icon != null) ...[
400
+ Opacity(
401
+ opacity: disabled ? 0.4 : 1.0,
402
+ child: Icon(
403
+ item.icon,
404
+ size: 16,
405
+ color: selected ? c.onSurface : c.muted,
406
+ ),
407
+ ),
408
+ const SizedBox(width: 6),
409
+ ],
410
+ Opacity(
411
+ opacity: disabled ? 0.4 : 1.0,
412
+ child: Text(
413
+ item.label,
414
+ style: context.textTheme.labelLarge?.copyWith(
415
+ color: selected ? c.onSurface : c.muted,
416
+ fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
417
+ ),
418
+ ),
419
+ ),
420
+ ],
421
+ ),
422
+ ),
423
+ );
424
+ }
425
+
426
+ @override
427
+ Widget build(BuildContext context) {
428
+ final Widget inner = _tabContent(context);
429
+ return expand ? Expanded(child: inner) : inner;
430
+ }
431
+ }
@@ -152,13 +152,19 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
152
152
  await HomeWidget.saveWidgetData<String>('isPro', data['isPro'] ?? 'false');
153
153
  await HomeWidget.saveWidgetData<String>('quote', data['quote'] ?? '');
154
154
 
155
- // On Android, saveWidgetData writes to SharedPreferences asynchronously.
156
- // Glance's HomeWidgetGlanceStateDefinition reads from the same prefs, but
157
- // a tight saveWidgetData→updateWidget sequence can race with the commit —
158
- // Glance occasionally recomposes with the previous values (most visible
159
- // right after a locale change). A small yield lets the writes settle.
155
+ // On Android, saveWidgetData writes via SharedPreferences.apply() which
156
+ // is asynchronous. Glance's HomeWidgetGlanceStateDefinition reads from
157
+ // the same prefs, but a tight saveWidgetData→updateWidget sequence can
158
+ // race the apply() flush — Glance recomposes with the previous values
159
+ // (most visible right after a locale change). Fire two updates spaced
160
+ // out by 400ms each so even slow devices catch the new state.
160
161
  if (!kIsWeb && Platform.isAndroid) {
161
- await Future<void>.delayed(const Duration(milliseconds: 120));
162
+ await Future<void>.delayed(const Duration(milliseconds: 400));
163
+ await HomeWidget.updateWidget(
164
+ name: _androidWidgetName,
165
+ iOSName: _iosWidgetName,
166
+ );
167
+ await Future<void>.delayed(const Duration(milliseconds: 400));
162
168
  }
163
169
 
164
170
  await HomeWidget.updateWidget(
@@ -184,6 +184,7 @@ const Set<String> _kReadyComponents = <String>{
184
184
  'Design System',
185
185
  'Dialog',
186
186
  'Hover',
187
+ 'Tabs',
187
188
  'TextArea',
188
189
  'TextField',
189
190
  'Sidebar',
@@ -218,7 +219,6 @@ const Set<String> _kWebReadyComponents = <String>{
218
219
 
219
220
  const Set<String> _kUrgentComponents = <String>{
220
221
  'Switch',
221
- 'Tabs',
222
222
  'Radio Group',
223
223
  'DatePicker',
224
224
  };