kasy-cli 1.14.0 → 1.16.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 (54) hide show
  1. package/bin/kasy.js +18 -5
  2. package/lib/commands/icon.js +29 -1
  3. package/lib/commands/ios.js +8 -2
  4. package/lib/commands/reset.js +100 -2
  5. package/lib/commands/run.js +61 -2
  6. package/lib/commands/splash.js +11 -0
  7. package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -2
  8. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -2
  9. package/lib/utils/apple-release.js +30 -0
  10. package/lib/utils/checks.js +41 -2
  11. package/lib/utils/debug.js +75 -0
  12. package/lib/utils/friendly-error.js +91 -0
  13. package/lib/utils/i18n/messages-en.js +977 -0
  14. package/lib/utils/i18n/messages-es.js +975 -0
  15. package/lib/utils/i18n/messages-pt.js +975 -0
  16. package/lib/utils/i18n.js +21 -2818
  17. package/lib/utils/png-padding.js +252 -0
  18. package/package.json +8 -3
  19. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +12 -11
  20. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  21. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png +0 -0
  22. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png +0 -0
  23. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  24. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png +0 -0
  25. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png +0 -0
  26. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  27. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  28. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  29. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  30. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  31. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  32. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png +0 -0
  33. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png +0 -0
  34. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  35. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png +0 -0
  36. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png +0 -0
  37. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  38. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png +0 -0
  39. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png +0 -0
  40. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +18 -11
  41. package/templates/firebase/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +9 -0
  42. package/templates/firebase/assets/images/icon_android.png +0 -0
  43. package/templates/firebase/assets/images/icon_foreground_empty.png +0 -0
  44. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  45. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  46. package/templates/firebase/lib/components/components.dart +1 -0
  47. package/templates/firebase/lib/components/kasy_avatar.dart +88 -57
  48. package/templates/firebase/lib/components/kasy_avatar_presets.dart +116 -74
  49. package/templates/firebase/lib/components/kasy_tabs.dart +431 -0
  50. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -6
  51. package/templates/firebase/lib/features/home/home_components_page.dart +1 -1
  52. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +316 -93
  53. package/templates/firebase/pubspec.yaml +4 -2
  54. package/templates/firebase/web/index.html +9 -0
@@ -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
  };